improve MUTUALLY AUTHENTICATE

This commit is contained in:
Michele Balistreri 2017-11-17 17:27:58 +03:00
parent 60f18b7afd
commit d8b862d58d
5 changed files with 169 additions and 28 deletions

View File

@ -32,7 +32,7 @@ opened.
* P2 = 0x00
* Data = An EC-256 public key on the SECP256k1 curve encoded as an uncompressed point.
* 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, 0x6A80 if the data is not a public key
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.
@ -61,7 +61,8 @@ guarantee authentication of the counterpart. The data sent by both parties is a
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.
commands can be sent. If the authentication fails with 0x6982, the OPEN SECURE CHANNEL command must be repeated to
generate new keys. This greatly slows down brute-force attempts.
### PAIR

View File

@ -80,7 +80,15 @@ public class SecureChannel {
}
Crypto.ecdh.init(scKeypair.getPrivate());
short len = Crypto.ecdh.generateSecret(apduBuffer, ISO7816.OFFSET_CDATA, apduBuffer[ISO7816.OFFSET_LC], secret, (short) 0);
short len;
try {
len = Crypto.ecdh.generateSecret(apduBuffer, ISO7816.OFFSET_CDATA, apduBuffer[ISO7816.OFFSET_LC], secret, (short) 0);
} catch(Exception e) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
return;
}
Crypto.random.generateData(apduBuffer, (short) 0, SC_SECRET_LENGTH);
Crypto.sha256.update(secret, (short) 0, len);
Crypto.sha256.update(pairingKeys, pairingKeyOff, SC_SECRET_LENGTH);
@ -111,6 +119,7 @@ public class SecureChannel {
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) {
scKey.clearKey();
ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);
}

View File

@ -60,6 +60,22 @@ public class SecureChannelSession {
}
}
/**
* Returns the public key
* @return the public key
*/
public byte[] getPublicKey() {
return publicKey;
}
/**
* Returns the pairing index
* @return the pairing index
*/
public byte getPairingIndex() {
return pairingIndex;
}
/**
* Establishes a Secure Channel with the card. The command parameters are the public key generated in the first step.
* The card returns a secret value which must be appended to the secret previously generated through the EC-DH
@ -73,44 +89,64 @@ public class SecureChannelSession {
*/
public void autoOpenSecureChannel(CardChannel apduChannel) throws CardException {
ResponseAPDU response = openSecureChannel(apduChannel, pairingIndex, publicKey);
byte[] salt = response.getData();
if (response.getSW() != 0x9000) {
throw new CardException("OPEN SECURE CHANNEL failed");
}
MessageDigest md;
processOpenSecureChannelResponse(response);
try {
md = MessageDigest.getInstance("SHA256", "BC");
md.update(secret);
md.update(pairingKey);
sessionKey = new SecretKeySpec(md.digest(salt), "AES");
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding", "BC");
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
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());
response = mutuallyAuthenticate(apduChannel);
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)) {
if(!verifyMutuallyAuthenticateResponse(response)) {
throw new CardException("Invalid authentication data from the card");
}
}
/**
* Processes the response from OPEN SECURE CHANNEL. This initialize the session key and cipher internally.
*
* @param response the card response
*/
public void processOpenSecureChannelResponse(ResponseAPDU response) {
try {
MessageDigest md = MessageDigest.getInstance("SHA256", "BC");
md.update(secret);
md.update(pairingKey);
sessionKey = new SecretKeySpec(md.digest(response.getData()), "AES");
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding", "BC");
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
}
/**
* Verify that the response from MUTUALLY AUTHENTICATE is correct.
*
* @param response the card response
* @return true if response is correct, false otherwise
*/
public boolean verifyMutuallyAuthenticateResponse(ResponseAPDU response) {
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA256", "BC");
} catch (Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
byte[] data = decryptAPDU(response.getData());
md.update(data, 0, SecureChannel.SC_SECRET_LENGTH);
byte[] digest = md.digest();
data = Arrays.copyOfRange(data, SecureChannel.SC_SECRET_LENGTH, data.length);
return Arrays.equals(data, digest);
}
/**
* Handles the entire pairing procedure in order to be able to use the secure channel
*
@ -189,6 +225,31 @@ public class SecureChannelSession {
return apduChannel.transmit(openSecureChannel);
}
/**
* Sends a MUTUALLY AUTHENTICATE APDU. The data is generated automatically
*
* @param apduChannel the apdu channel
* @return the raw card response
* @throws CardException communication error
*/
public ResponseAPDU mutuallyAuthenticate(CardChannel apduChannel) throws CardException {
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA256", "BC");
} catch (Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
byte[] data = new byte[SecureChannel.SC_SECRET_LENGTH];
random.nextBytes(data);
byte[] digest = md.digest(data);
data = Arrays.copyOf(data, SecureChannel.SC_SECRET_LENGTH * 2);
System.arraycopy(digest, 0, data, SecureChannel.SC_SECRET_LENGTH, SecureChannel.SC_SECRET_LENGTH);
return mutuallyAuthenticate(apduChannel, data);
}
/**
* Sends a MUTUALLY AUTHENTICATE APDU.
*

View File

@ -80,6 +80,13 @@ public class WalletAppletCommandSet {
return secureChannel.openSecureChannel(apduChannel, index, data);
}
/**
* Sends a MUTUALLY AUTHENTICATE APDU. Calls the corresponding method of the SecureChannel class.
*/
public ResponseAPDU mutuallyAuthenticate() throws CardException {
return secureChannel.mutuallyAuthenticate(apduChannel);
}
/**
* Sends a MUTUALLY AUTHENTICATE APDU. Calls the corresponding method of the SecureChannel class.
*/

View File

@ -108,14 +108,77 @@ public class WalletAppletTest {
ResponseAPDU response = cmdSet.select();
assertEquals(0x9000, response.getSW());
byte[] data = response.getData();
assertEquals(0x04, data[0]);
assertEquals((SecureChannel.SC_KEY_LENGTH * 2 / 8) + 1, data.length);
assertEquals(WalletApplet.TLV_APPLICATION_INFO_TEMPLATE, data[0]);
assertEquals(WalletApplet.TLV_UID, data[2]);
assertEquals(WalletApplet.TLV_PUB_KEY, data[20]);
}
@Test
@DisplayName("OPEN SECURE CHANNEL command")
void openSecureChannelTest() throws CardException {
// Wrong P1
ResponseAPDU response = cmdSet.openSecureChannel((byte)(secureChannel.getPairingIndex() + 1), new byte[65]);
assertEquals(0x6A86, response.getSW());
// Wrong data
response = cmdSet.openSecureChannel(secureChannel.getPairingIndex(), new byte[65]);
assertEquals(0x6A80, response.getSW());
// Good case
response = cmdSet.openSecureChannel(secureChannel.getPairingIndex(), secureChannel.getPublicKey());
assertEquals(0x9000, response.getSW());
assertEquals(SecureChannel.SC_SECRET_LENGTH, response.getData().length);
secureChannel.processOpenSecureChannelResponse(response);
// Send command before MUTUALLY AUTHENTICATE
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
assertEquals(0x6985, response.getSW());
// Perform mutual authentication
response = cmdSet.mutuallyAuthenticate();
assertEquals(0x9000, response.getSW());
assertTrue(secureChannel.verifyMutuallyAuthenticateResponse(response));
// Verify that the channel is open
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
assertEquals(0x9000, response.getSW());
}
@Test
@DisplayName("MUTUALLY AUTHENTICATE command")
void mutuallyAuthenticateTest() throws CardException {
// Mutual authentication before opening a Secure Channel
ResponseAPDU response = cmdSet.mutuallyAuthenticate();
assertEquals(0x6985, response.getSW());
response = cmdSet.openSecureChannel(secureChannel.getPairingIndex(), secureChannel.getPublicKey());
assertEquals(0x9000, response.getSW());
secureChannel.processOpenSecureChannelResponse(response);
// Wrong data format
response = cmdSet.mutuallyAuthenticate(new byte[63]);
assertEquals(0x6A80, response.getSW());
// Wrong authentication data
response = cmdSet.mutuallyAuthenticate(new byte[64]);
assertEquals(0x6982, response.getSW());
// Verify that after wrong authentication, the command does not work
response = cmdSet.mutuallyAuthenticate();
assertEquals(0x6985, response.getSW());
// Good case
cmdSet.autoOpenSecureChannel();
// MUTUALLY AUTHENTICATE has no effect on an already open secure channel
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
assertEquals(0x9000, response.getSW());
response = cmdSet.mutuallyAuthenticate();
assertEquals(0x6985, response.getSW());
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
assertEquals(0x9000, response.getSW());
}
@Test