From e15da6a7e0e6e4ffd85d7a45a8a7f2b259e8f915 Mon Sep 17 00:00:00 2001 From: Michele Balistreri Date: Fri, 19 Jan 2018 12:04:55 +0300 Subject: [PATCH] add ability to export current public key --- APPLICATION.MD | 19 ++++-- .../java/im/status/wallet/WalletApplet.java | 41 ++++++++++-- .../status/wallet/WalletAppletCommandSet.java | 6 +- .../im/status/wallet/WalletAppletTest.java | 65 ++++++++++++------- 4 files changed, 97 insertions(+), 34 deletions(-) diff --git a/APPLICATION.MD b/APPLICATION.MD index 4db7602..1bc2374 100644 --- a/APPLICATION.MD +++ b/APPLICATION.MD @@ -301,20 +301,31 @@ will work even if no PIN authentication has been performed. An empty sequence me * CLA = 0x80 * INS = 0xC2 -* P1 = 0x01 (Whisper key) -* P2 = 0x00 -* Response SW = 0x9000 on success, 0x6A86 if P1 is wrong +* P1 = key path index +* P2 = export options +* Response SW = 0x9000 on success, 0x6A86 if P1 or P2 are 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 +P1: +0x00 = Any key +0x01 = Whisper key (m/1/1) + +P2: +0x00 = private and public key +0x01 = public key only + Response Data format: - Tag 0xA1 = keypair template - Tag 0x80 = ECC public key component - - Tag 0x81 = ECC private key component + - Tag 0x81 = ECC private key component (if P2=0x00) 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. + +The special index 0x00 indicates any path, which means the current key will always be exported regardless of its actual +path. This works however only in combination with P2=0x01, so only the public key will be exported. diff --git a/src/main/java/im/status/wallet/WalletApplet.java b/src/main/java/im/status/wallet/WalletApplet.java index 0ff342a..69a173b 100644 --- a/src/main/java/im/status/wallet/WalletApplet.java +++ b/src/main/java/im/status/wallet/WalletApplet.java @@ -53,8 +53,12 @@ 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_ANY = 0x00; static final byte EXPORT_KEY_P1_WHISPER = 0x01; + static final byte EXPORT_KEY_P2_PRIVATE_AND_PUBLIC = 0x00; + static final byte EXPORT_KEY_P2_PUBLIC_ONLY = 0x01; + static final byte TLV_SIGNATURE_TEMPLATE = (byte) 0xA0; static final byte TLV_KEY_TEMPLATE = (byte) 0xA1; @@ -863,8 +867,28 @@ public class WalletApplet extends Applet { } byte[] toExport; + boolean publicOnly; + + switch (apduBuffer[ISO7816.OFFSET_P2]) { + case EXPORT_KEY_P2_PRIVATE_AND_PUBLIC: + publicOnly = false; + break; + case EXPORT_KEY_P2_PUBLIC_ONLY: + publicOnly = true; + break; + default: + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + return; + } switch (apduBuffer[ISO7816.OFFSET_P1]) { + case EXPORT_KEY_P1_ANY: + if (!publicOnly) { + ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); + } + + toExport = null; + break; case EXPORT_KEY_P1_WHISPER: toExport = WHISPER_KEY_PATH; break; @@ -873,7 +897,7 @@ public class WalletApplet extends Applet { return; } - if (!((keyPathLen == toExport.length) && (Util.arrayCompare(keyPath, (short) 0, toExport, (short) 0, keyPathLen) == 0))) { + if (!((toExport == null) || ((keyPathLen == toExport.length) && (Util.arrayCompare(keyPath, (short) 0, toExport, (short) 0, keyPathLen) == 0)))) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } @@ -886,11 +910,16 @@ public class WalletApplet extends Applet { 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); + + if (!publicOnly) { + apduBuffer[off++] = TLV_PRIV_KEY; + off++; + len = privateKey.getS(apduBuffer, off); + apduBuffer[(short) (off - 1)] = (byte) len; + off += len; + } + + len = (short) (off - SecureChannel.SC_OUT_OFFSET); apduBuffer[(SecureChannel.SC_OUT_OFFSET + 1)] = (byte) (len - 2); secureChannel.respond(apdu, (short) len, ISO7816.SW_NO_ERROR); diff --git a/src/test/java/im/status/wallet/WalletAppletCommandSet.java b/src/test/java/im/status/wallet/WalletAppletCommandSet.java index 8878922..bec1fd8 100644 --- a/src/test/java/im/status/wallet/WalletAppletCommandSet.java +++ b/src/test/java/im/status/wallet/WalletAppletCommandSet.java @@ -422,11 +422,13 @@ public class WalletAppletCommandSet { * Sends an EXPORT KEY APDU. The keyPathIndex is used as P1. Valid values are defined in the applet itself * * @param keyPathIndex the P1 parameter + * @param publicOnly the P2 parameter * @return the raw card response * @throws CardException communication error */ - public ResponseAPDU exportKey(byte keyPathIndex) throws CardException { - CommandAPDU exportKey = secureChannel.protectedCommand(0x80, WalletApplet.INS_EXPORT_KEY, keyPathIndex, 0x00, new byte[0]); + public ResponseAPDU exportKey(byte keyPathIndex, boolean publicOnly) throws CardException { + byte p2 = publicOnly ? WalletApplet.EXPORT_KEY_P2_PUBLIC_ONLY : WalletApplet.EXPORT_KEY_P2_PRIVATE_AND_PUBLIC; + CommandAPDU exportKey = secureChannel.protectedCommand(0x80, WalletApplet.INS_EXPORT_KEY, keyPathIndex, p2, new byte[0]); return secureChannel.transmit(apduChannel, exportKey); } } diff --git a/src/test/java/im/status/wallet/WalletAppletTest.java b/src/test/java/im/status/wallet/WalletAppletTest.java index b9bfb2a..f7173d8 100644 --- a/src/test/java/im/status/wallet/WalletAppletTest.java +++ b/src/test/java/im/status/wallet/WalletAppletTest.java @@ -804,13 +804,13 @@ public class WalletAppletTest { new Random().nextBytes(chainCode); // Security condition violation: SecureChannel not open - ResponseAPDU response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER); + ResponseAPDU response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER, false); assertEquals(0x6985, response.getSW()); cmdSet.autoOpenSecureChannel(); // Security condition violation: PIN not verified - response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER); + response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER, false); assertEquals(0x6985, response.getSW()); response = cmdSet.verifyPIN("000000"); @@ -819,36 +819,51 @@ public class WalletAppletTest { assertEquals(0x9000, response.getSW()); // Security condition violation: current key is not Whisper key - response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER); + response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER, false); assertEquals(0x6985, response.getSW()); response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, true, true, false); assertEquals(0x9000, response.getSW()); response = cmdSet.deriveKey(derivePublicKey(response.getData()), false, true, true); assertEquals(0x9000, response.getSW()); - response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER); + response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER, false); assertEquals(0x6985, response.getSW()); + + // Export current public key (wrong P2) + response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_ANY, false); + assertEquals(0x6985, response.getSW()); + + // Export current public key + response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_ANY, true); + assertEquals(0x9000, response.getSW()); + byte[] keyTemplate = response.getData(); + verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 1 }, true); + response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, false, true, false); assertEquals(0x9000, response.getSW()); response = cmdSet.deriveKey(derivePublicKey(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); + response = cmdSet.exportKey((byte) 2, false); assertEquals(0x6a86, response.getSW()); // Correct - response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER); + response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER, false); assertEquals(0x9000, response.getSW()); - byte[] keyTemplate = response.getData(); - verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 1, 1 }); + keyTemplate = response.getData(); + verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 1, 1 }, false); + + // Correct public only + response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER, true); + assertEquals(0x9000, response.getSW()); + keyTemplate = response.getData(); + verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 1, 1 }, true); // Reset response = cmdSet.deriveKey(new byte[] {}, true, false, false); assertEquals(0x9000, response.getSW()); - response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER); + response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER, false); assertEquals(0x6985, response.getSW()); } @@ -1096,22 +1111,28 @@ public class WalletAppletTest { } } - private void verifyExportedKey(byte[] keyTemplate, KeyPair keyPair, byte[] chainCode, int[] path) { + private void verifyExportedKey(byte[] keyTemplate, KeyPair keyPair, byte[] chainCode, int[] path, boolean publicOnly) { 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); + + if (publicOnly) { + assertEquals(pubKey.length + 2, keyTemplate[1]); + assertEquals(pubKey.length + 4, keyTemplate.length); + } else { + 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(tPrivKey, privateKey); + } } private DeterministicKey deriveKey(KeyPair keyPair, byte[] chainCode, int[] path) {