mirror of
https://github.com/status-im/status-keycard.git
synced 2025-01-13 07:14:11 +00:00
improve MUTUALLY AUTHENTICATE
This commit is contained in:
parent
60f18b7afd
commit
d8b862d58d
@ -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
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user