From b68c6bd27504bb784c368ca8a2e501ba833fd117 Mon Sep 17 00:00:00 2001 From: Michele Balistreri Date: Thu, 26 Oct 2017 14:15:40 +0300 Subject: [PATCH] implement EXPORT KEY --- APPLICATION.MD | 24 ++++- .../java/im/status/wallet/WalletApplet.java | 50 ++++++++++ .../status/wallet/WalletAppletCommandSet.java | 5 + .../im/status/wallet/WalletAppletTest.java | 99 +++++++++++++++++-- 4 files changed, 168 insertions(+), 10 deletions(-) diff --git a/APPLICATION.MD b/APPLICATION.MD index 8a4aab4..68057b0 100644 --- a/APPLICATION.MD +++ b/APPLICATION.MD @@ -269,4 +269,26 @@ On applet selection any pending signing session is aborted. * Preconditions: Secure Channel must be opened, user PIN must be verified Sets the given sequence of 32-bit integers as a PIN-less path. When the current derived key matches this path, SIGN -will work even if no PIN authentication has been performed. An empty sequence means that no PIN-less path is defined. \ No newline at end of file +will work even if no PIN authentication has been performed. An empty sequence means that no PIN-less path is defined. + +### EXPORT KEY + +* CLA = 0x80 +* INS = 0xC2 +* P1 = 0x01 (Whisper key) +* P2 = 0x00 +* Response SW = 0x9000 on success, 0x6A86 if P1 is wrong +* Response Data = key pair template +* Preconditions: Secure Channel must be opened, user PIN must be verified, the current key path must match the one of + the key selected through P1 + +Response Data format: +- Tag 0xA1 = keypair template + - Tag 0x80 = ECC public key component + - Tag 0x81 = ECC private key component + +This command exports the current public and private key if and only if the current key path matches the one of the key +selected by P1. P1 is only an index, the actual key path is stored immutably in the applet itself. At the moment only +the Whisper key (P1=0x01) can be exported and its key path is m/1/1. Other key paths could be added in the future, but +the last should remain as short as possible because of the security implications of revealing private keys to a possibly +compromised device. The current chain code is never exported to make it impossible to further derive keys off-card. \ No newline at end of file diff --git a/src/main/java/im/status/wallet/WalletApplet.java b/src/main/java/im/status/wallet/WalletApplet.java index 0892462..c25c30b 100644 --- a/src/main/java/im/status/wallet/WalletApplet.java +++ b/src/main/java/im/status/wallet/WalletApplet.java @@ -13,6 +13,7 @@ public class WalletApplet extends Applet { static final byte INS_GENERATE_MNEMONIC = (byte) 0xD2; static final byte INS_SIGN = (byte) 0xC0; static final byte INS_SET_PINLESS_PATH = (byte) 0xC1; + static final byte INS_EXPORT_KEY = (byte) 0xC2; static final byte PUK_LENGTH = 12; static final byte PUK_MAX_RETRIES = 5; @@ -47,6 +48,8 @@ public class WalletApplet extends Applet { static final byte GENERATE_MNEMONIC_P1_CS_MAX = 8; static final byte GENERATE_MNEMONIC_TMP_OFF = SecureChannel.SC_OUT_OFFSET + ((((GENERATE_MNEMONIC_P1_CS_MAX * 32) + GENERATE_MNEMONIC_P1_CS_MAX) / 11) * 2); + static final byte EXPORT_KEY_P1_WHISPER = 0x01; + static final byte TLV_SIGNATURE_TEMPLATE = (byte) 0xA0; static final byte TLV_KEY_TEMPLATE = (byte) 0xA1; @@ -64,6 +67,7 @@ public class WalletApplet extends Applet { static final byte TLV_PUBLIC_KEY_DERIVATION = (byte) 0xC3; private static final byte[] ASSISTED_DERIVATION_HASH = { (byte) 0xAA, (byte) 0x2D, (byte) 0xA9, (byte) 0x9D, (byte) 0x91, (byte) 0x8C, (byte) 0x7D, (byte) 0x95, (byte) 0xB8, (byte) 0x96, (byte) 0x89, (byte) 0x87, (byte) 0x3E, (byte) 0xAA, (byte) 0x37, (byte) 0x67, (byte) 0x25, (byte) 0x0C, (byte) 0xFF, (byte) 0x50, (byte) 0x13, (byte) 0x9A, (byte) 0x2F, (byte) 0x87, (byte) 0xBB, (byte) 0x4F, (byte) 0xCA, (byte) 0xB4, (byte) 0xAE, (byte) 0xC3, (byte) 0xE8, (byte) 0x90}; + private static final byte[] WHISPER_KEY_PATH = { 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01}; private OwnerPIN pin; private OwnerPIN puk; @@ -168,6 +172,9 @@ public class WalletApplet extends Applet { case INS_SET_PINLESS_PATH: setPinlessPath(apdu); break; + case INS_EXPORT_KEY: + exportKey(apdu); + break; default: ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); break; @@ -589,6 +596,49 @@ public class WalletApplet extends Applet { } } + private void exportKey(APDU apdu) { + apdu.setIncomingAndReceive(); + + if (!(secureChannel.isOpen() && pin.isValidated())) { + ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); + } + + byte[] apduBuffer = apdu.getBuffer(); + byte[] toExport; + + switch (apduBuffer[ISO7816.OFFSET_P1]) { + case EXPORT_KEY_P1_WHISPER: + toExport = WHISPER_KEY_PATH; + break; + default: + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + return; + } + + if (!((keyPathLen == toExport.length) && (Util.arrayCompare(keyPath, (short) 0, toExport, (short) 0, keyPathLen) == 0))) { + ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); + } + + short off = SecureChannel.SC_OUT_OFFSET; + + apduBuffer[off++] = TLV_KEY_TEMPLATE; + off++; + apduBuffer[off++] = TLV_PUB_KEY; + off++; + short len = publicKey.getW(apduBuffer, off); + apduBuffer[(short)(off - 1)] = (byte) len; + off += len; + apduBuffer[off++] = TLV_PRIV_KEY; + off++; + len = privateKey.getS(apduBuffer, off); + apduBuffer[(short)(off - 1)] = (byte) len; + len += (off - SecureChannel.SC_OUT_OFFSET); + apduBuffer[(SecureChannel.SC_OUT_OFFSET + 1)] = (byte) (len - 2); + + len = secureChannel.encryptAPDU(apduBuffer, (short) len); + apdu.setOutgoingAndSend(ISO7816.OFFSET_CDATA, len); + } + private void setPinlessPath(APDU apdu) { apdu.setIncomingAndReceive(); diff --git a/src/test/java/im/status/wallet/WalletAppletCommandSet.java b/src/test/java/im/status/wallet/WalletAppletCommandSet.java index 14870ad..e222246 100644 --- a/src/test/java/im/status/wallet/WalletAppletCommandSet.java +++ b/src/test/java/im/status/wallet/WalletAppletCommandSet.java @@ -200,4 +200,9 @@ public class WalletAppletCommandSet { CommandAPDU setPinlessPath = new CommandAPDU(0x80, WalletApplet.INS_SET_PINLESS_PATH, 0x00, 0x00, secureChannel.encryptAPDU(data)); return apduChannel.transmit(setPinlessPath); } + + public ResponseAPDU exportKey(byte keyPathIndex) throws CardException { + CommandAPDU exportKey = new CommandAPDU(0x80, WalletApplet.INS_EXPORT_KEY, keyPathIndex, 0x00, 256); + return apduChannel.transmit(exportKey); + } } diff --git a/src/test/java/im/status/wallet/WalletAppletTest.java b/src/test/java/im/status/wallet/WalletAppletTest.java index 9511843..c702019 100644 --- a/src/test/java/im/status/wallet/WalletAppletTest.java +++ b/src/test/java/im/status/wallet/WalletAppletTest.java @@ -531,7 +531,7 @@ public class WalletAppletTest { response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true); assertEquals(0x9000, response.getSW()); byte[] sig = secureChannel.decryptAPDU(response.getData()); - byte[] keyData = extractPublicKey(sig); + byte[] keyData = extractPublicKeyFromSignature(sig); sig = extractSignature(sig); assertEquals((SecureChannel.SC_KEY_LENGTH * 2 / 8) + 1, keyData.length); signature.update(data); @@ -627,6 +627,63 @@ public class WalletAppletTest { assertEquals(0x6985, response.getSW()); } + @Test + @DisplayName("EXPORT KEY command") + void exportKey() throws Exception { + KeyPairGenerator g = keypairGenerator(); + KeyPair keyPair = g.generateKeyPair(); + byte[] chainCode = new byte[32]; + new Random().nextBytes(chainCode); + + // Security condition violation: SecureChannel not open + ResponseAPDU response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER); + assertEquals(0x6985, response.getSW()); + + cmdSet.openSecureChannel(); + + // Security condition violation: PIN not verified + response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER); + assertEquals(0x6985, response.getSW()); + + response = cmdSet.verifyPIN("000000"); + assertEquals(0x9000, response.getSW()); + response = cmdSet.loadKey(keyPair, false, chainCode); + assertEquals(0x9000, response.getSW()); + + // Security condition violation: current key is not Whisper key + response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER); + assertEquals(0x6985, response.getSW()); + + response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, true, true, false); + assertEquals(0x9000, response.getSW()); + response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true); + assertEquals(0x9000, response.getSW()); + response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER); + assertEquals(0x6985, response.getSW()); + response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, false, true, false); + assertEquals(0x9000, response.getSW()); + response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true); + assertEquals(0x9000, response.getSW()); + + // Wrong P1 + response = cmdSet.exportKey((byte) 0); + assertEquals(0x6a86, response.getSW()); + response = cmdSet.exportKey((byte) 2); + assertEquals(0x6a86, response.getSW()); + + // Correct + response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER); + assertEquals(0x9000, response.getSW()); + byte[] keyTemplate = secureChannel.decryptAPDU(response.getData()); + verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 1, 1 }); + + // Reset + response = cmdSet.deriveKey(new byte[] {}, true, false, false); + assertEquals(0x9000, response.getSW()); + response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER); + assertEquals(0x6985, response.getSW()); + } + @Test @DisplayName("SIGN data (unused for the current scenario)") @Tag("manual") @@ -785,7 +842,7 @@ public class WalletAppletTest { return Arrays.copyOfRange(sig, off, off + sig[off + 1] + 2); } - private byte[] extractPublicKey(byte[] sig) { + private byte[] extractPublicKeyFromSignature(byte[] sig) { assertEquals(WalletApplet.TLV_SIGNATURE_TEMPLATE, sig[0]); assertEquals((byte) 0x81, sig[1]); assertEquals(WalletApplet.TLV_PUB_KEY, sig[3]); @@ -838,17 +895,13 @@ public class WalletAppletTest { } private void verifyKeyDerivation(KeyPair keyPair, byte[] chainCode, int[] path) throws Exception { - DeterministicKey key = HDKeyDerivation.createMasterPrivKeyFromBytes(((org.bouncycastle.jce.interfaces.ECPrivateKey) keyPair.getPrivate()).getD().toByteArray(), chainCode); - - for (int i : path) { - key = HDKeyDerivation.deriveChildKey(key, new ChildNumber(i)); - } + DeterministicKey key = deriveKey(keyPair, chainCode, path); byte[] hash = Hash.sha3(new byte[8]); ResponseAPDU resp = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH, true, true); assertEquals(0x9000, resp.getSW()); byte[] sig = secureChannel.decryptAPDU(resp.getData()); - byte[] publicKey = extractPublicKey(sig); + byte[] publicKey = extractPublicKeyFromSignature(sig); sig = extractSignature(sig); assertTrue(key.verify(hash, sig)); @@ -867,6 +920,34 @@ public class WalletAppletTest { } } + private void verifyExportedKey(byte[] keyTemplate, KeyPair keyPair, byte[] chainCode, int[] path) { + ECKey key = deriveKey(keyPair, chainCode, path).decompress(); + assertEquals(WalletApplet.TLV_KEY_TEMPLATE, keyTemplate[0]); + assertEquals(WalletApplet.TLV_PUB_KEY, keyTemplate[2]); + byte[] pubKey = Arrays.copyOfRange(keyTemplate, 4, 4 + keyTemplate[3]); + assertEquals(WalletApplet.TLV_PRIV_KEY, keyTemplate[4 + pubKey.length]); + byte[] privateKey = Arrays.copyOfRange(keyTemplate, 6 + pubKey.length, 6 + pubKey.length + keyTemplate[5 + pubKey.length]); + + byte[] tPrivKey = key.getPrivKey().toByteArray(); + + if (tPrivKey[0] == 0x00) { + tPrivKey = Arrays.copyOfRange(tPrivKey, 1, tPrivKey.length); + } + + assertArrayEquals(key.getPubKey(), pubKey); + assertArrayEquals(tPrivKey, privateKey); + } + + private DeterministicKey deriveKey(KeyPair keyPair, byte[] chainCode, int[] path) { + DeterministicKey key = HDKeyDerivation.createMasterPrivKeyFromBytes(((org.bouncycastle.jce.interfaces.ECPrivateKey) keyPair.getPrivate()).getD().toByteArray(), chainCode); + + for (int i : path) { + key = HDKeyDerivation.deriveChildKey(key, new ChildNumber(i)); + } + + return key; + } + private byte[] derivePublicKey(byte[] data) { byte[] pubKey = Arrays.copyOfRange(data, 3, 4 + data[3]); byte[] signature = Arrays.copyOfRange(data, 4 + data[3], data.length); @@ -909,7 +990,7 @@ public class WalletAppletTest { Method recoverFromSignature = Sign.class.getDeclaredMethod("recoverFromSignature", int.class, ecdsaSignature, byte[].class); recoverFromSignature.setAccessible(true); - byte[] pubData = extractPublicKey(respData); + byte[] pubData = extractPublicKeyFromSignature(respData); BigInteger publicKey = new BigInteger(Arrays.copyOfRange(pubData, 1, pubData.length)); int recId = -1;