Add the MUTUALLY AUTHENTICATE command

This commit is contained in:
Michele Balistreri 2017-11-17 16:12:28 +03:00
parent 09fe778d85
commit 60f18b7afd
7 changed files with 152 additions and 30 deletions

View File

@ -64,6 +64,10 @@ which must be used by the client to establish the Secure Channel.
The OPEN SECURE CHANNEL command is as specified in the [SECURE_CHANNEL.MD](SECURE_CHANNEL.MD). The OPEN SECURE CHANNEL command is as specified in the [SECURE_CHANNEL.MD](SECURE_CHANNEL.MD).
### MUTUALLY AUTHENTICATE
The MUTUALLY AUTHENTICATE command is as specified in the [SECURE_CHANNEL.MD](SECURE_CHANNEL.MD).
### PAIR ### PAIR
The PAIR command is as specified in the [SECURE_CHANNEL.MD](SECURE_CHANNEL.MD). The shared secret is the SHA-256 of the The PAIR command is as specified in the [SECURE_CHANNEL.MD](SECURE_CHANNEL.MD). The shared secret is the SHA-256 of the

View File

@ -15,6 +15,8 @@ A short description of establishing a session is as follows
generate a shared 256-bit secret (more details below). generate a shared 256-bit secret (more details below).
3. The generated secret is used as an AES key to encrypt all further communication. CBC mode is used with a random IV 3. The generated secret is used as an AES key to encrypt all further communication. CBC mode is used with a random IV
generated for each APDU and prepended to the APDU payload. Both command and responses are encrypted. generated for each APDU and prepended to the APDU payload. Both command and responses are encrypted.
4. The client sends a MUTUALLY AUTHENTICATE command to verify that the keys are matching and thus the secure channel is
successfully established.
The EC keyset used by the card for the EC-DH algorithm is generated on-card on applet installation and is not used The EC keyset used by the card for the EC-DH algorithm is generated on-card on applet installation and is not used
for anything else. The EC keyset used by the client is generated every time a new secure channel session must be for anything else. The EC keyset used by the client is generated every time a new secure channel session must be
@ -32,7 +34,7 @@ opened.
* Response Data = A 256-bit salt * Response Data = A 256-bit salt
* Response SW = 0x9000 on success, 0x6A86 if P1 is invalid * Response SW = 0x9000 on success, 0x6A86 if P1 is invalid
This APDU is sent to establish a Secure Channel session. A session is aborted when the application is deselected, This APDU is the first step to establish a Secure Channel session. A session is aborted when the application is deselected,
either directly or because of a card reset/tear. This APDU and its response are not encrypted. either directly or because of a card reset/tear. This APDU and its response are not encrypted.
The card generates a random 256-bit salt which is sent to the client. Both the client and the card do the following The card generates a random 256-bit salt which is sent to the client. Both the client and the card do the following
@ -43,12 +45,28 @@ for key derivation
calculated. calculated.
3. The output of the SHA-256 algorithm is used as the AES key for further communication. 3. The output of the SHA-256 algorithm is used as the AES key for further communication.
TODO: define a second step where client and card mutually verify that they have the same keys and thus are authenticated ### MUTUALLY AUTHENTICATE
* CLA = 0x80
* INS = 0x11
* P1 = 0x00
* P2 = 0x00
* Data = 256-bit random number and its SHA-256 hash
* Response Data = 256-bit random number and its SHA-256 hash
* Response SW = 0x9000 on success, 0x6985 if the previous successfully executed APDU was not OPEN SECURE CHANNEL, 0x6982
if authentication failed, 0x6A80 if the data is not exactly 64 bytes long
This APDU allows both parties to verify that the keys generated in the OPEN SECURE CHANNEL step are matching and thus
guarantee authentication of the counterpart. The data sent by both parties is a 32-bit random number followed by its own
SHA-256 hash. The APDU data is sent encrypted with the keys generated in the OPEN SECURE CHANNEL step. Each party must
verify that the hash indeed matches the value sent. If this is true, then the decryption was correct, meaning that the
keys are matching. Only after this step has been executed the secure channel can be considered to be open and other
commands can be sent.
### PAIR ### PAIR
* CLA = 0x80 * CLA = 0x80
* INS = 0x11 * INS = 0x12
* P1 = pairing phase * P1 = pairing phase
* P2 = 0x00 * P2 = 0x00
* Data = see below * Data = see below
@ -89,7 +107,7 @@ happens depend on the specific applet.
### UNPAIR ### UNPAIR
* CLA = 0x80 * CLA = 0x80
* INS = 0x12 * INS = 0x13
* P1 = the index to unpair * P1 = the index to unpair
* P2 = 0x00 * P2 = 0x00
* Data = the same index as in P1 * Data = the same index as in P1

View File

@ -15,8 +15,9 @@ public class SecureChannel {
public static final short SC_OUT_OFFSET = ISO7816.OFFSET_CDATA + (SC_BLOCK_SIZE * 2); public static final short SC_OUT_OFFSET = ISO7816.OFFSET_CDATA + (SC_BLOCK_SIZE * 2);
public static final byte INS_OPEN_SECURE_CHANNEL = 0x10; public static final byte INS_OPEN_SECURE_CHANNEL = 0x10;
public static final byte INS_PAIR = 0x11; public static final byte INS_MUTUALLY_AUTHENTICATE = 0x11;
public static final byte INS_UNPAIR = 0x12; public static final byte INS_PAIR = 0x12;
public static final byte INS_UNPAIR = 0x13;
public static final byte PAIR_P1_FIRST_STEP = 0x00; public static final byte PAIR_P1_FIRST_STEP = 0x00;
public static final byte PAIR_P1_LAST_STEP = 0x01; public static final byte PAIR_P1_LAST_STEP = 0x01;
@ -34,6 +35,7 @@ public class SecureChannel {
private byte[] pairingKeys; private byte[] pairingKeys;
private short preassignedPairingOffset = -1; private short preassignedPairingOffset = -1;
private boolean mutuallyAuthenticated = false;
/** /**
* Instantiates a Secure Channel. All memory allocations needed for the secure channel are performed here. The keypair * Instantiates a Secure Channel. All memory allocations needed for the secure channel are performed here. The keypair
@ -64,6 +66,7 @@ public class SecureChannel {
*/ */
public void openSecureChannel(APDU apdu) { public void openSecureChannel(APDU apdu) {
preassignedPairingOffset = -1; preassignedPairingOffset = -1;
mutuallyAuthenticated = false;
apdu.setIncomingAndReceive(); apdu.setIncomingAndReceive();
byte[] apduBuffer = apdu.getBuffer(); byte[] apduBuffer = apdu.getBuffer();
@ -86,6 +89,39 @@ public class SecureChannel {
apdu.setOutgoingAndSend((short) 0, SC_SECRET_LENGTH); apdu.setOutgoingAndSend((short) 0, SC_SECRET_LENGTH);
} }
/**
* Processes the MUTUALLY AUTHENTICATE command.
*
* @param apdu the JCRE-owned APDU object.
*/
public void mutuallyAuthenticate(APDU apdu) {
if (!scKey.isInitialized() || mutuallyAuthenticated) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
apdu.setIncomingAndReceive();
byte[] apduBuffer = apdu.getBuffer();
short len = decryptAPDU(apduBuffer);
if (len != (short) (SC_SECRET_LENGTH * 2)) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
Crypto.sha256.doFinal(apduBuffer, ISO7816.OFFSET_CDATA, SC_SECRET_LENGTH, apduBuffer, ISO7816.OFFSET_CDATA);
if (Util.arrayCompare(apduBuffer, ISO7816.OFFSET_CDATA, apduBuffer, (short) (ISO7816.OFFSET_CDATA + SC_SECRET_LENGTH), SC_SECRET_LENGTH) != 0) {
ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);
}
mutuallyAuthenticated = true;
Crypto.random.generateData(apduBuffer, SC_OUT_OFFSET, SC_SECRET_LENGTH);
Crypto.sha256.doFinal(apduBuffer, SC_OUT_OFFSET, SC_SECRET_LENGTH, apduBuffer, (short) (SC_OUT_OFFSET + SC_SECRET_LENGTH));
len = encryptAPDU(apduBuffer, len);
apdu.setOutgoingAndSend(ISO7816.OFFSET_CDATA, len);
}
/** /**
* Processes the PAIR command. * Processes the PAIR command.
* *
@ -243,7 +279,7 @@ public class SecureChannel {
* @return whether a secure channel is currently established or not. * @return whether a secure channel is currently established or not.
*/ */
public boolean isOpen() { public boolean isOpen() {
return scKey.isInitialized(); return scKey.isInitialized() && mutuallyAuthenticated;
} }
/** /**

View File

@ -185,6 +185,9 @@ public class WalletApplet extends Applet {
case SecureChannel.INS_OPEN_SECURE_CHANNEL: case SecureChannel.INS_OPEN_SECURE_CHANNEL:
secureChannel.openSecureChannel(apdu); secureChannel.openSecureChannel(apdu);
break; break;
case SecureChannel.INS_MUTUALLY_AUTHENTICATE:
secureChannel.mutuallyAuthenticate(apdu);
break;
case SecureChannel.INS_PAIR: case SecureChannel.INS_PAIR:
secureChannel.pair(apdu); secureChannel.pair(apdu);
break; break;

View File

@ -71,13 +71,18 @@ public class SecureChannelSession {
* @return the card response * @return the card response
* @throws CardException communication error * @throws CardException communication error
*/ */
public ResponseAPDU openSecureChannel(CardChannel apduChannel) throws CardException { public void autoOpenSecureChannel(CardChannel apduChannel) throws CardException {
CommandAPDU openSecureChannel = new CommandAPDU(0x80, SecureChannel.INS_OPEN_SECURE_CHANNEL, pairingIndex, 0, publicKey); ResponseAPDU response = openSecureChannel(apduChannel, pairingIndex, publicKey);
ResponseAPDU response = apduChannel.transmit(openSecureChannel);
byte[] salt = response.getData(); byte[] salt = response.getData();
if (response.getSW() != 0x9000) {
throw new CardException("OPEN SECURE CHANNEL failed");
}
MessageDigest md;
try { try {
MessageDigest md = MessageDigest.getInstance("SHA256", "BC"); md = MessageDigest.getInstance("SHA256", "BC");
md.update(secret); md.update(secret);
md.update(pairingKey); md.update(pairingKey);
sessionKey = new SecretKeySpec(md.digest(salt), "AES"); sessionKey = new SecretKeySpec(md.digest(salt), "AES");
@ -86,7 +91,24 @@ public class SecureChannelSession {
throw new RuntimeException("Is BouncyCastle in the classpath?", e); throw new RuntimeException("Is BouncyCastle in the classpath?", e);
} }
return response; random.nextBytes(salt);
byte[] digest = md.digest(salt);
salt = Arrays.copyOf(salt, SecureChannel.SC_SECRET_LENGTH * 2);
System.arraycopy(digest, 0, salt, SecureChannel.SC_SECRET_LENGTH, SecureChannel.SC_SECRET_LENGTH);
response = mutuallyAuthenticate(apduChannel, salt);
salt = decryptAPDU(response.getData());
if (response.getSW() != 0x9000) {
throw new CardException("MUTUALLY AUTHENTICATE failed");
}
md.update(salt, 0, SecureChannel.SC_SECRET_LENGTH);
digest = md.digest();
salt = Arrays.copyOfRange(salt, SecureChannel.SC_SECRET_LENGTH, salt.length);
if (!Arrays.equals(salt, digest)) {
throw new CardException("Invalid authentication data from the card");
}
} }
/** /**
@ -153,6 +175,33 @@ public class SecureChannelSession {
} }
} }
/**
* Sends a OPEN SECURE CHANNEL APDU.
*
* @param apduChannel the apdu channel
* @param index the P1 parameter
* @param data the data
* @return the raw card response
* @throws CardException communication error
*/
public ResponseAPDU openSecureChannel(CardChannel apduChannel, byte index, byte[] data) throws CardException {
CommandAPDU openSecureChannel = new CommandAPDU(0x80, SecureChannel.INS_OPEN_SECURE_CHANNEL, index, 0, data);
return apduChannel.transmit(openSecureChannel);
}
/**
* Sends a MUTUALLY AUTHENTICATE APDU.
*
* @param apduChannel the apdu channel
* @param data the data
* @return the raw card response
* @throws CardException communication error
*/
public ResponseAPDU mutuallyAuthenticate(CardChannel apduChannel, byte[] data) throws CardException {
CommandAPDU mutuallyAuthenticate = new CommandAPDU(0x80, SecureChannel.INS_MUTUALLY_AUTHENTICATE, 0, 0, encryptAPDU(data));
return apduChannel.transmit(mutuallyAuthenticate);
}
/** /**
* Sends a PAIR APDU. * Sends a PAIR APDU.
* *

View File

@ -51,8 +51,8 @@ public class WalletAppletCommandSet {
* @return the raw card response * @return the raw card response
* @throws CardException communication error * @throws CardException communication error
*/ */
public ResponseAPDU openSecureChannel() throws CardException { public void autoOpenSecureChannel() throws CardException {
return secureChannel.openSecureChannel(apduChannel); secureChannel.autoOpenSecureChannel(apduChannel);
} }
/** /**
@ -73,6 +73,20 @@ public class WalletAppletCommandSet {
secureChannel.autoUnpair(apduChannel); secureChannel.autoUnpair(apduChannel);
} }
/**
* Sends a OPEN SECURE CHANNEL APDU. Calls the corresponding method of the SecureChannel class.
*/
public ResponseAPDU openSecureChannel(byte index, byte[] data) throws CardException {
return secureChannel.openSecureChannel(apduChannel, index, data);
}
/**
* Sends a MUTUALLY AUTHENTICATE APDU. Calls the corresponding method of the SecureChannel class.
*/
public ResponseAPDU mutuallyAuthenticate(byte[] data) throws CardException {
return secureChannel.mutuallyAuthenticate(apduChannel, data);
}
/** /**
* Sends a PAIR APDU. Calls the corresponding method of the SecureChannel class. * Sends a PAIR APDU. Calls the corresponding method of the SecureChannel class.
*/ */

View File

@ -115,9 +115,7 @@ public class WalletAppletTest {
@Test @Test
@DisplayName("OPEN SECURE CHANNEL command") @DisplayName("OPEN SECURE CHANNEL command")
void openSecureChannelTest() throws CardException { void openSecureChannelTest() throws CardException {
ResponseAPDU response = cmdSet.openSecureChannel(); cmdSet.autoOpenSecureChannel();
assertEquals(0x9000, response.getSW());
assertEquals(SecureChannel.SC_SECRET_LENGTH, response.getData().length);
} }
@Test @Test
@ -126,7 +124,7 @@ public class WalletAppletTest {
// Security condition violation: SecureChannel not open // Security condition violation: SecureChannel not open
ResponseAPDU response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION); ResponseAPDU response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
assertEquals(0x6985, response.getSW()); assertEquals(0x6985, response.getSW());
cmdSet.openSecureChannel(); cmdSet.autoOpenSecureChannel();
// Good case. Since the order of test execution is undefined, the test cannot know if the keys are initialized or not. // Good case. Since the order of test execution is undefined, the test cannot know if the keys are initialized or not.
// Additionally, support for public key derivation is hw dependent. // Additionally, support for public key derivation is hw dependent.
@ -163,7 +161,7 @@ public class WalletAppletTest {
ResponseAPDU response = cmdSet.verifyPIN("000000"); ResponseAPDU response = cmdSet.verifyPIN("000000");
assertEquals(0x6985, response.getSW()); assertEquals(0x6985, response.getSW());
cmdSet.openSecureChannel(); cmdSet.autoOpenSecureChannel();
// Wrong PIN // Wrong PIN
response = cmdSet.verifyPIN("123456"); response = cmdSet.verifyPIN("123456");
@ -198,7 +196,7 @@ public class WalletAppletTest {
ResponseAPDU response = cmdSet.changePIN("123456"); ResponseAPDU response = cmdSet.changePIN("123456");
assertEquals(0x6985, response.getSW()); assertEquals(0x6985, response.getSW());
cmdSet.openSecureChannel(); cmdSet.autoOpenSecureChannel();
// Security condition violation: PIN n ot verified // Security condition violation: PIN n ot verified
response = cmdSet.changePIN("123456"); response = cmdSet.changePIN("123456");
@ -242,7 +240,7 @@ public class WalletAppletTest {
ResponseAPDU response = cmdSet.unblockPIN("123456789012", "000000"); ResponseAPDU response = cmdSet.unblockPIN("123456789012", "000000");
assertEquals(0x6985, response.getSW()); assertEquals(0x6985, response.getSW());
cmdSet.openSecureChannel(); cmdSet.autoOpenSecureChannel();
// Condition violation: PIN is not blocked // Condition violation: PIN is not blocked
response = cmdSet.unblockPIN("123456789012", "000000"); response = cmdSet.unblockPIN("123456789012", "000000");
@ -294,7 +292,7 @@ public class WalletAppletTest {
ResponseAPDU response = cmdSet.loadKey(keyPair); ResponseAPDU response = cmdSet.loadKey(keyPair);
assertEquals(0x6985, response.getSW()); assertEquals(0x6985, response.getSW());
cmdSet.openSecureChannel(); cmdSet.autoOpenSecureChannel();
int publicKeyDerivationSW = cmdSet.getPublicKeyDerivationSupport() ? 0x9000 : 0x6a81; int publicKeyDerivationSW = cmdSet.getPublicKeyDerivationSupport() ? 0x9000 : 0x6a81;
@ -351,7 +349,7 @@ public class WalletAppletTest {
// Security condition violation: SecureChannel not open // Security condition violation: SecureChannel not open
ResponseAPDU response = cmdSet.generateMnemonic(4); ResponseAPDU response = cmdSet.generateMnemonic(4);
assertEquals(0x6985, response.getSW()); assertEquals(0x6985, response.getSW());
cmdSet.openSecureChannel(); cmdSet.autoOpenSecureChannel();
// Wrong P1 (too short, too long) // Wrong P1 (too short, too long)
response = cmdSet.generateMnemonic(3); response = cmdSet.generateMnemonic(3);
@ -389,7 +387,7 @@ public class WalletAppletTest {
ResponseAPDU response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x00}); ResponseAPDU response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x00});
assertEquals(0x6985, response.getSW()); assertEquals(0x6985, response.getSW());
cmdSet.openSecureChannel(); cmdSet.autoOpenSecureChannel();
boolean autonomousDerivation = cmdSet.getPublicKeyDerivationSupport(); boolean autonomousDerivation = cmdSet.getPublicKeyDerivationSupport();
// Security condition violation: PIN is not verified // Security condition violation: PIN is not verified
@ -509,7 +507,7 @@ public class WalletAppletTest {
ResponseAPDU response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true); ResponseAPDU response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
assertEquals(0x6985, response.getSW()); assertEquals(0x6985, response.getSW());
cmdSet.openSecureChannel(); cmdSet.autoOpenSecureChannel();
// Security condition violation: PIN not verified // Security condition violation: PIN not verified
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true,true); response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true,true);
@ -558,7 +556,7 @@ public class WalletAppletTest {
ResponseAPDU response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}); ResponseAPDU response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02});
assertEquals(0x6985, response.getSW()); assertEquals(0x6985, response.getSW());
cmdSet.openSecureChannel(); cmdSet.autoOpenSecureChannel();
// Security condition violation: PIN not verified // Security condition violation: PIN not verified
response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}); response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02});
@ -644,7 +642,7 @@ public class WalletAppletTest {
ResponseAPDU response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER); ResponseAPDU response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER);
assertEquals(0x6985, response.getSW()); assertEquals(0x6985, response.getSW());
cmdSet.openSecureChannel(); cmdSet.autoOpenSecureChannel();
// Security condition violation: PIN not verified // Security condition violation: PIN not verified
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER); response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER);
@ -698,7 +696,7 @@ public class WalletAppletTest {
byte[] smallData = Arrays.copyOf(data, 20); byte[] smallData = Arrays.copyOf(data, 20);
r.nextBytes(data); r.nextBytes(data);
cmdSet.openSecureChannel(); cmdSet.autoOpenSecureChannel();
ResponseAPDU response = cmdSet.verifyPIN("000000"); ResponseAPDU response = cmdSet.verifyPIN("000000");
assertEquals(0x9000, response.getSW()); assertEquals(0x9000, response.getSW());
@ -798,7 +796,7 @@ public class WalletAppletTest {
Credentials wallet2 = WalletUtils.loadCredentials("testwallet", "testwallets/wallet2.json"); Credentials wallet2 = WalletUtils.loadCredentials("testwallet", "testwallets/wallet2.json");
// Load keys on card // Load keys on card
cmdSet.openSecureChannel(); cmdSet.autoOpenSecureChannel();
ResponseAPDU response = cmdSet.verifyPIN("000000"); ResponseAPDU response = cmdSet.verifyPIN("000000");
assertEquals(0x9000, response.getSW()); assertEquals(0x9000, response.getSW());
response = cmdSet.loadKey(wallet1.getEcKeyPair()); response = cmdSet.loadKey(wallet1.getEcKeyPair());
@ -874,7 +872,7 @@ public class WalletAppletTest {
private void resetAndSelectAndOpenSC() throws CardException { private void resetAndSelectAndOpenSC() throws CardException {
reset(); reset();
cmdSet.select(); cmdSet.select();
cmdSet.openSecureChannel(); cmdSet.autoOpenSecureChannel();
} }
private void assertMnemonic(int expectedLength, byte[] data) { private void assertMnemonic(int expectedLength, byte[] data) {