From d8b862d58d3d147f8bd46d080088337a1aaa2e2b Mon Sep 17 00:00:00 2001 From: Michele Balistreri Date: Fri, 17 Nov 2017 17:27:58 +0300 Subject: [PATCH] improve MUTUALLY AUTHENTICATE --- SECURE_CHANNEL.MD | 5 +- .../java/im/status/wallet/SecureChannel.java | 11 +- .../status/wallet/SecureChannelSession.java | 107 ++++++++++++++---- .../status/wallet/WalletAppletCommandSet.java | 7 ++ .../im/status/wallet/WalletAppletTest.java | 67 ++++++++++- 5 files changed, 169 insertions(+), 28 deletions(-) diff --git a/SECURE_CHANNEL.MD b/SECURE_CHANNEL.MD index edac9e7..2162477 100644 --- a/SECURE_CHANNEL.MD +++ b/SECURE_CHANNEL.MD @@ -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 diff --git a/src/main/java/im/status/wallet/SecureChannel.java b/src/main/java/im/status/wallet/SecureChannel.java index 7a61c0b..c6baf38 100644 --- a/src/main/java/im/status/wallet/SecureChannel.java +++ b/src/main/java/im/status/wallet/SecureChannel.java @@ -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); } diff --git a/src/test/java/im/status/wallet/SecureChannelSession.java b/src/test/java/im/status/wallet/SecureChannelSession.java index 8ae2879..ad08d36 100644 --- a/src/test/java/im/status/wallet/SecureChannelSession.java +++ b/src/test/java/im/status/wallet/SecureChannelSession.java @@ -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. * diff --git a/src/test/java/im/status/wallet/WalletAppletCommandSet.java b/src/test/java/im/status/wallet/WalletAppletCommandSet.java index 3c1891a..bbe1c99 100644 --- a/src/test/java/im/status/wallet/WalletAppletCommandSet.java +++ b/src/test/java/im/status/wallet/WalletAppletCommandSet.java @@ -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. */ diff --git a/src/test/java/im/status/wallet/WalletAppletTest.java b/src/test/java/im/status/wallet/WalletAppletTest.java index 8c79f3b..a69d9e7 100644 --- a/src/test/java/im/status/wallet/WalletAppletTest.java +++ b/src/test/java/im/status/wallet/WalletAppletTest.java @@ -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