From 60f18b7afde4e8c280f7aace02d821dddfa0365c Mon Sep 17 00:00:00 2001 From: Michele Balistreri Date: Fri, 17 Nov 2017 16:12:28 +0300 Subject: [PATCH] Add the MUTUALLY AUTHENTICATE command --- APPLICATION.MD | 4 ++ SECURE_CHANNEL.MD | 26 ++++++-- .../java/im/status/wallet/SecureChannel.java | 42 ++++++++++++- .../java/im/status/wallet/WalletApplet.java | 3 + .../status/wallet/SecureChannelSession.java | 59 +++++++++++++++++-- .../status/wallet/WalletAppletCommandSet.java | 18 +++++- .../im/status/wallet/WalletAppletTest.java | 30 +++++----- 7 files changed, 152 insertions(+), 30 deletions(-) diff --git a/APPLICATION.MD b/APPLICATION.MD index 990d4bb..c338f47 100644 --- a/APPLICATION.MD +++ b/APPLICATION.MD @@ -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). +### MUTUALLY AUTHENTICATE + +The MUTUALLY AUTHENTICATE command is as specified in the [SECURE_CHANNEL.MD](SECURE_CHANNEL.MD). + ### PAIR The PAIR command is as specified in the [SECURE_CHANNEL.MD](SECURE_CHANNEL.MD). The shared secret is the SHA-256 of the diff --git a/SECURE_CHANNEL.MD b/SECURE_CHANNEL.MD index 3f17c53..edac9e7 100644 --- a/SECURE_CHANNEL.MD +++ b/SECURE_CHANNEL.MD @@ -15,6 +15,8 @@ A short description of establishing a session is as follows 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 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 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 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. 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. 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 * CLA = 0x80 -* INS = 0x11 +* INS = 0x12 * P1 = pairing phase * P2 = 0x00 * Data = see below @@ -89,7 +107,7 @@ happens depend on the specific applet. ### UNPAIR * CLA = 0x80 -* INS = 0x12 +* INS = 0x13 * P1 = the index to unpair * P2 = 0x00 * Data = the same index as in P1 diff --git a/src/main/java/im/status/wallet/SecureChannel.java b/src/main/java/im/status/wallet/SecureChannel.java index ec09902..7a61c0b 100644 --- a/src/main/java/im/status/wallet/SecureChannel.java +++ b/src/main/java/im/status/wallet/SecureChannel.java @@ -15,8 +15,9 @@ public class SecureChannel { 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_PAIR = 0x11; - public static final byte INS_UNPAIR = 0x12; + public static final byte INS_MUTUALLY_AUTHENTICATE = 0x11; + 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_LAST_STEP = 0x01; @@ -34,6 +35,7 @@ public class SecureChannel { private byte[] pairingKeys; 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 @@ -64,6 +66,7 @@ public class SecureChannel { */ public void openSecureChannel(APDU apdu) { preassignedPairingOffset = -1; + mutuallyAuthenticated = false; apdu.setIncomingAndReceive(); byte[] apduBuffer = apdu.getBuffer(); @@ -86,6 +89,39 @@ public class SecureChannel { 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. * @@ -243,7 +279,7 @@ public class SecureChannel { * @return whether a secure channel is currently established or not. */ public boolean isOpen() { - return scKey.isInitialized(); + return scKey.isInitialized() && mutuallyAuthenticated; } /** diff --git a/src/main/java/im/status/wallet/WalletApplet.java b/src/main/java/im/status/wallet/WalletApplet.java index f0b0e0d..32c7fa7 100644 --- a/src/main/java/im/status/wallet/WalletApplet.java +++ b/src/main/java/im/status/wallet/WalletApplet.java @@ -185,6 +185,9 @@ public class WalletApplet extends Applet { case SecureChannel.INS_OPEN_SECURE_CHANNEL: secureChannel.openSecureChannel(apdu); break; + case SecureChannel.INS_MUTUALLY_AUTHENTICATE: + secureChannel.mutuallyAuthenticate(apdu); + break; case SecureChannel.INS_PAIR: secureChannel.pair(apdu); break; diff --git a/src/test/java/im/status/wallet/SecureChannelSession.java b/src/test/java/im/status/wallet/SecureChannelSession.java index 97a7438..8ae2879 100644 --- a/src/test/java/im/status/wallet/SecureChannelSession.java +++ b/src/test/java/im/status/wallet/SecureChannelSession.java @@ -71,13 +71,18 @@ public class SecureChannelSession { * @return the card response * @throws CardException communication error */ - public ResponseAPDU openSecureChannel(CardChannel apduChannel) throws CardException { - CommandAPDU openSecureChannel = new CommandAPDU(0x80, SecureChannel.INS_OPEN_SECURE_CHANNEL, pairingIndex, 0, publicKey); - ResponseAPDU response = apduChannel.transmit(openSecureChannel); + 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; + try { - MessageDigest md = MessageDigest.getInstance("SHA256", "BC"); + md = MessageDigest.getInstance("SHA256", "BC"); md.update(secret); md.update(pairingKey); sessionKey = new SecretKeySpec(md.digest(salt), "AES"); @@ -86,7 +91,24 @@ public class SecureChannelSession { 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. * diff --git a/src/test/java/im/status/wallet/WalletAppletCommandSet.java b/src/test/java/im/status/wallet/WalletAppletCommandSet.java index 65e5e5f..3c1891a 100644 --- a/src/test/java/im/status/wallet/WalletAppletCommandSet.java +++ b/src/test/java/im/status/wallet/WalletAppletCommandSet.java @@ -51,8 +51,8 @@ public class WalletAppletCommandSet { * @return the raw card response * @throws CardException communication error */ - public ResponseAPDU openSecureChannel() throws CardException { - return secureChannel.openSecureChannel(apduChannel); + public void autoOpenSecureChannel() throws CardException { + secureChannel.autoOpenSecureChannel(apduChannel); } /** @@ -73,6 +73,20 @@ public class WalletAppletCommandSet { 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. */ diff --git a/src/test/java/im/status/wallet/WalletAppletTest.java b/src/test/java/im/status/wallet/WalletAppletTest.java index d1db6d1..8c79f3b 100644 --- a/src/test/java/im/status/wallet/WalletAppletTest.java +++ b/src/test/java/im/status/wallet/WalletAppletTest.java @@ -115,9 +115,7 @@ public class WalletAppletTest { @Test @DisplayName("OPEN SECURE CHANNEL command") void openSecureChannelTest() throws CardException { - ResponseAPDU response = cmdSet.openSecureChannel(); - assertEquals(0x9000, response.getSW()); - assertEquals(SecureChannel.SC_SECRET_LENGTH, response.getData().length); + cmdSet.autoOpenSecureChannel(); } @Test @@ -126,7 +124,7 @@ public class WalletAppletTest { // Security condition violation: SecureChannel not open ResponseAPDU response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION); 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. // Additionally, support for public key derivation is hw dependent. @@ -163,7 +161,7 @@ public class WalletAppletTest { ResponseAPDU response = cmdSet.verifyPIN("000000"); assertEquals(0x6985, response.getSW()); - cmdSet.openSecureChannel(); + cmdSet.autoOpenSecureChannel(); // Wrong PIN response = cmdSet.verifyPIN("123456"); @@ -198,7 +196,7 @@ public class WalletAppletTest { ResponseAPDU response = cmdSet.changePIN("123456"); assertEquals(0x6985, response.getSW()); - cmdSet.openSecureChannel(); + cmdSet.autoOpenSecureChannel(); // Security condition violation: PIN n ot verified response = cmdSet.changePIN("123456"); @@ -242,7 +240,7 @@ public class WalletAppletTest { ResponseAPDU response = cmdSet.unblockPIN("123456789012", "000000"); assertEquals(0x6985, response.getSW()); - cmdSet.openSecureChannel(); + cmdSet.autoOpenSecureChannel(); // Condition violation: PIN is not blocked response = cmdSet.unblockPIN("123456789012", "000000"); @@ -294,7 +292,7 @@ public class WalletAppletTest { ResponseAPDU response = cmdSet.loadKey(keyPair); assertEquals(0x6985, response.getSW()); - cmdSet.openSecureChannel(); + cmdSet.autoOpenSecureChannel(); int publicKeyDerivationSW = cmdSet.getPublicKeyDerivationSupport() ? 0x9000 : 0x6a81; @@ -351,7 +349,7 @@ public class WalletAppletTest { // Security condition violation: SecureChannel not open ResponseAPDU response = cmdSet.generateMnemonic(4); assertEquals(0x6985, response.getSW()); - cmdSet.openSecureChannel(); + cmdSet.autoOpenSecureChannel(); // Wrong P1 (too short, too long) response = cmdSet.generateMnemonic(3); @@ -389,7 +387,7 @@ public class WalletAppletTest { ResponseAPDU response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x00}); assertEquals(0x6985, response.getSW()); - cmdSet.openSecureChannel(); + cmdSet.autoOpenSecureChannel(); boolean autonomousDerivation = cmdSet.getPublicKeyDerivationSupport(); // 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); assertEquals(0x6985, response.getSW()); - cmdSet.openSecureChannel(); + cmdSet.autoOpenSecureChannel(); // Security condition violation: PIN not verified 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}); assertEquals(0x6985, response.getSW()); - cmdSet.openSecureChannel(); + cmdSet.autoOpenSecureChannel(); // Security condition violation: PIN not verified 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); assertEquals(0x6985, response.getSW()); - cmdSet.openSecureChannel(); + cmdSet.autoOpenSecureChannel(); // Security condition violation: PIN not verified response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER); @@ -698,7 +696,7 @@ public class WalletAppletTest { byte[] smallData = Arrays.copyOf(data, 20); r.nextBytes(data); - cmdSet.openSecureChannel(); + cmdSet.autoOpenSecureChannel(); ResponseAPDU response = cmdSet.verifyPIN("000000"); assertEquals(0x9000, response.getSW()); @@ -798,7 +796,7 @@ public class WalletAppletTest { Credentials wallet2 = WalletUtils.loadCredentials("testwallet", "testwallets/wallet2.json"); // Load keys on card - cmdSet.openSecureChannel(); + cmdSet.autoOpenSecureChannel(); ResponseAPDU response = cmdSet.verifyPIN("000000"); assertEquals(0x9000, response.getSW()); response = cmdSet.loadKey(wallet1.getEcKeyPair()); @@ -874,7 +872,7 @@ public class WalletAppletTest { private void resetAndSelectAndOpenSC() throws CardException { reset(); cmdSet.select(); - cmdSet.openSecureChannel(); + cmdSet.autoOpenSecureChannel(); } private void assertMnemonic(int expectedLength, byte[] data) {