From d7780808990fcf2c4fe9a90331808c9016231366 Mon Sep 17 00:00:00 2001 From: Michele Balistreri Date: Thu, 26 Oct 2017 12:11:49 +0300 Subject: [PATCH] implement PIN-less path --- APPLICATION.MD | 29 ++++-- .../java/im/status/wallet/WalletApplet.java | 38 +++++++- .../status/wallet/WalletAppletCommandSet.java | 5 ++ .../im/status/wallet/WalletAppletTest.java | 89 +++++++++++++++++++ 4 files changed, 151 insertions(+), 10 deletions(-) diff --git a/APPLICATION.MD b/APPLICATION.MD index 6bf7398..8a4aab4 100644 --- a/APPLICATION.MD +++ b/APPLICATION.MD @@ -20,7 +20,7 @@ and passed as an installation parameter to the applet according to the JavaCard to unblock the applet using the PUK, the PUK is blocked, meaning the the wallet is lost. After authentication, the user remains authenticated until the application is either deselected or the card is reset. -Authentication with PIN is a requirement for all further commands to succeed. +Authentication with PIN is a requirement for most commands to succeed. The PIN can be changed by the user after authentication. @@ -32,7 +32,9 @@ specifications. This keyset is used to sign transactions. When the applet is fir signing will fail. It is necessary to first load the keyset in order for the application to be fully operational. Signing of transactions is done by uploading the data in blocks no larger than 255 bytes (including the overhead caused -by the Secure Channel). Segmentation must be handled at the application protocol. +by the Secure Channel). Segmentation must be handled at the application protocol. Another option is to sign the hash +of the transaction, with the hash being calculated off-card. Signing generally requires the PIN to be authenticated, +however the user can set a special key path which requires no authentication. ## APDUs @@ -57,6 +59,7 @@ 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). ### GET STATUS + * CLA = 0x80 * INS = 0xF2 * P1 = 0x00 for application status, 0x01 for key path status @@ -165,7 +168,7 @@ signing sessions, if any. Unless a DERIVE KEY is sent, a subsequent SIGN command * Response SW = 0x9000 on success, 0x6A80 if the format is invalid, 0x6A81 if public key derivation is not supported and bit 0 of P1 is set, 0x6A86 if P2 = 0x01 and bit 0 of P1 is not set. * Response Data = On assisted derivation and P2 = 0x01 the key derivation template. Empty otherwise. -* Preconditions: Secure Channel must be opened, user PIN must be verified, an extended keyset must be loaded +* Preconditions: Secure Channel must be opened, user PIN must be verified (if no PIN-less key is defined), an extended keyset must be loaded This command is used before a signing session to generate a private key according to the [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) specifications. This command always aborts open signing sessions, if any. The generated key is used for all subsequent @@ -181,7 +184,7 @@ P1: * bit 7 = if 0 derive from master keys, if 1 derive from current keys P2: -* 0x00 = data is 32 a sequence of 32-bit integers +* 0x00 = data is a sequence of 32-bit integers * 0x01 = data is a public key Response Data format: @@ -216,14 +219,13 @@ human-readable mnemonic. Each integer can have a value from 0 to 2047. * Data = the data to sign * Response = if P2 indicates last segment, the public key and the signature are returned * Response SW = 0x9000 on success, 0x6A86 if P2 is invalid -* Preconditions: Secure Channel must be opened, user PIN must be verified, a valid keypair must be loaded +* Preconditions: Secure Channel must be opened, user PIN must be verified (or a PIN-less key must be active), a valid keypair must be loaded P1: * 0x00 = transaction data * 0x01 = precomputed hash P2: - * bit 0 = if 1 first block, if 0 other block * bit 1-6 = reserved * bit 7 = if 0 more blocks, if 1 last block @@ -254,4 +256,17 @@ be returned. This segmentation scheme allows resuming signature sessions if other commands must be sent in between and at the same time avoid generating signatures over partial data, since both the first and the last block are marked. -On applet selection any pending signing session is aborted. \ No newline at end of file +On applet selection any pending signing session is aborted. + +### SET PINLESS PATH + +* CLA = 0x80 +* INS = 0xC1 +* P1 = 0x00 +* P2 = 0x00 +* Data = a sequence of 32-bit integers +* Response SW = 0x9000 on success, 0x6A80 if data is invalid +* 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 diff --git a/src/main/java/im/status/wallet/WalletApplet.java b/src/main/java/im/status/wallet/WalletApplet.java index 0470edb..0892462 100644 --- a/src/main/java/im/status/wallet/WalletApplet.java +++ b/src/main/java/im/status/wallet/WalletApplet.java @@ -12,11 +12,13 @@ public class WalletApplet extends Applet { static final byte INS_DERIVE_KEY = (byte) 0xD1; 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 PUK_LENGTH = 12; static final byte PUK_MAX_RETRIES = 5; static final byte PIN_LENGTH = 6; static final byte PIN_MAX_RETRIES = 3; + static final short KEY_PATH_MAX_DEPTH = 10; static final short EC_KEY_SIZE = 256; static final short CHAIN_CODE_SIZE = 32; @@ -62,7 +64,6 @@ 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 short KEY_PATH_MAX_DEPTH = 10; private OwnerPIN pin; private OwnerPIN puk; @@ -80,6 +81,9 @@ public class WalletApplet extends Applet { private byte[] keyPath; private short keyPathLen; + private byte[] pinlessPath; + private short pinlessPathLen; + private Signature signature; private boolean signInProgress; private boolean expectPublicKey; @@ -109,6 +113,7 @@ public class WalletApplet extends Applet { masterChainCode = new byte[32]; chainCode = new byte[32]; keyPath = new byte[KEY_PATH_MAX_DEPTH * 4]; + pinlessPath = new byte[KEY_PATH_MAX_DEPTH * 4]; publicKey = (ECPublicKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PUBLIC, EC_KEY_SIZE, false); privateKey = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, EC_KEY_SIZE, false); @@ -160,6 +165,9 @@ public class WalletApplet extends Applet { case INS_SIGN: sign(apdu); break; + case INS_SET_PINLESS_PATH: + setPinlessPath(apdu); + break; default: ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); break; @@ -391,7 +399,7 @@ public class WalletApplet extends Applet { } private void deriveKey(APDU apdu) { - if (!(secureChannel.isOpen() && pin.isValidated() && isExtended)) { + if (!(secureChannel.isOpen() && (pin.isValidated() || (pinlessPathLen > 0)) && isExtended)) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } @@ -539,7 +547,7 @@ public class WalletApplet extends Applet { private void sign(APDU apdu) { apdu.setIncomingAndReceive(); - if (!(secureChannel.isOpen() && pin.isValidated() && privateKey.isInitialized() && !expectPublicKey)) { + if (!(secureChannel.isOpen() && (pin.isValidated() || isPinless()) && privateKey.isInitialized() && !expectPublicKey)) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } @@ -581,6 +589,26 @@ public class WalletApplet extends Applet { } } + private void setPinlessPath(APDU apdu) { + apdu.setIncomingAndReceive(); + + if (!(secureChannel.isOpen() && pin.isValidated())) { + ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); + } + + byte[] apduBuffer = apdu.getBuffer(); + short len = secureChannel.decryptAPDU(apduBuffer); + + if (((short) (len % 4) != 0) || (len > pinlessPath.length)) { + ISOException.throwIt(ISO7816.SW_WRONG_DATA); + } + + JCSystem.beginTransaction(); + pinlessPathLen = len; + Util.arrayCopy(apduBuffer, ISO7816.OFFSET_CDATA, pinlessPath, (short) 0, len); + JCSystem.commitTransaction(); + } + private boolean allDigits(byte[] buffer, short off, short len) { while(len > 0) { len--; @@ -594,4 +622,8 @@ public class WalletApplet extends Applet { return true; } + + private boolean isPinless() { + return (pinlessPathLen > 0) && (pinlessPathLen == keyPathLen) && (Util.arrayCompare(keyPath, (short) 0, pinlessPath, (short) 0, keyPathLen) == 0); + } } diff --git a/src/test/java/im/status/wallet/WalletAppletCommandSet.java b/src/test/java/im/status/wallet/WalletAppletCommandSet.java index 423db96..14870ad 100644 --- a/src/test/java/im/status/wallet/WalletAppletCommandSet.java +++ b/src/test/java/im/status/wallet/WalletAppletCommandSet.java @@ -195,4 +195,9 @@ public class WalletAppletCommandSet { CommandAPDU deriveKey = new CommandAPDU(0x80, WalletApplet.INS_DERIVE_KEY, p1, p2, secureChannel.encryptAPDU(data)); return apduChannel.transmit(deriveKey); } + + public ResponseAPDU setPinlessPath(byte [] data) throws CardException { + CommandAPDU setPinlessPath = new CommandAPDU(0x80, WalletApplet.INS_SET_PINLESS_PATH, 0x00, 0x00, secureChannel.encryptAPDU(data)); + return apduChannel.transmit(setPinlessPath); + } } diff --git a/src/test/java/im/status/wallet/WalletAppletTest.java b/src/test/java/im/status/wallet/WalletAppletTest.java index 3e5c76d..9511843 100644 --- a/src/test/java/im/status/wallet/WalletAppletTest.java +++ b/src/test/java/im/status/wallet/WalletAppletTest.java @@ -538,6 +538,95 @@ public class WalletAppletTest { assertTrue(signature.verify(sig)); } + @Test + @DisplayName("SET PINLESS PATH command") + void setPinlessPathTest() throws Exception { + byte[] data = "some data to be hashed".getBytes(); + byte[] hash = sha256(data); + + 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.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}); + assertEquals(0x6985, response.getSW()); + + cmdSet.openSecureChannel(); + + // Security condition violation: PIN not verified + response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}); + assertEquals(0x6985, response.getSW()); + + response = cmdSet.verifyPIN("000000"); + assertEquals(0x9000, response.getSW()); + response = cmdSet.loadKey(keyPair, false, chainCode); + assertEquals(0x9000, response.getSW()); + + // Wrong data + response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00}); + assertEquals(0x6a80, response.getSW()); + response = cmdSet.setPinlessPath(new byte[(WalletApplet.KEY_PATH_MAX_DEPTH + 1)* 4]); + assertEquals(0x6a80, response.getSW()); + + // Correct + response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}); + assertEquals(0x9000, response.getSW()); + + // Verify that only PINless path can be used without PIN + resetAndSelectAndOpenSC(); + response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true); + assertEquals(0x6985, response.getSW()); + response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, true, true, false); + assertEquals(0x9000, response.getSW()); + response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true); + assertEquals(0x9000, 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()); + response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true); + assertEquals(0x6985, response.getSW()); + response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, false, true, false); + assertEquals(0x9000, response.getSW()); + response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true); + assertEquals(0x9000, response.getSW()); + response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true); + assertEquals(0x9000, response.getSW()); + + // Verify changing path + response = cmdSet.verifyPIN("000000"); + assertEquals(0x9000, response.getSW()); + response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01}); + assertEquals(0x9000, response.getSW()); + resetAndSelectAndOpenSC(); + response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true); + assertEquals(0x6985, response.getSW()); + assertEquals(0x6985, response.getSW()); + response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, true, true, false); + assertEquals(0x9000, response.getSW()); + response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true); + assertEquals(0x9000, 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()); + response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true); + assertEquals(0x9000, response.getSW()); + + // Reset + response = cmdSet.verifyPIN("000000"); + assertEquals(0x9000, response.getSW()); + response = cmdSet.setPinlessPath(new byte[] {}); + assertEquals(0x9000, response.getSW()); + resetAndSelectAndOpenSC(); + response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true); + assertEquals(0x6985, response.getSW()); + response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, true, true, false); + assertEquals(0x6985, response.getSW()); + } + @Test @DisplayName("SIGN data (unused for the current scenario)") @Tag("manual")