diff --git a/APPLICATION.MD b/APPLICATION.MD index 48884b1..6bf7398 100644 --- a/APPLICATION.MD +++ b/APPLICATION.MD @@ -59,19 +59,23 @@ The OPEN SECURE CHANNEL command is as specified in the [SECURE_CHANNEL.MD](SECUR ### GET STATUS * CLA = 0x80 * INS = 0xF2 -* P1 = 0x00 +* P1 = 0x00 for application status, 0x01 for key path status * P2 = 0x00 -* Response SW = 0x9000 on success -* Response Data = Application Status Template -* Preconditions: Secure Channel must be opened +* Response SW = 0x9000 on success, 0x6A86 on undefined P1 +* Response Data = Application Status Template or Key Path +* Preconditions: Secure Channel must be opened, if Key Path is required then the card must not be in the middle of a derivation session Response Data format: +if P1 = 0x00: - Tag 0xA3 = Application Status Template - Tag 0xC0 = PIN retry count (1 byte) - Tag 0xC1 = PUK retry count (1 byte) - Tag 0xC2 = 0 if key is not initialized, 1 otherwise - Tag 0xC3 = 1 if public key derivation is supported, 0 otherwise +if P1 = 0x01 +- a sequence of 32-bit numbers indicating the current key path. Empty if master key is selected. + ### VERIFY PIN * CLA = 0x80 @@ -168,7 +172,8 @@ specifications. This command always aborts open signing sessions, if any. The ge SIGN sessions. Because JavaCard does not offer native EC point multiplication before version 3.0.5, there is an alternative mode of operation where the public key is partially derived off-card and loaded back on card. In this mode of operation only 1 derivation step at the time can be completed and as such during assisted derivation the data can -contain a single 32-bit integer, instead of a sequence. +contain a single 32-bit integer, instead of a sequence. The maximum depth of derivation from the master key is 10. Any +attempt to get deeper results in 0x6A80 being returned. P1: * bit 0 = if 0 derive autonomously (only works if public key derivation is supported), if 1 do assisted derivation diff --git a/README.md b/README.md index d99e08f..ae61144 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,11 @@ In order to test with the simulator, you need to pass these additional parameter ``` com.fidesmo.gradle.javacard.home=/home/username/javacard-3_0_4 im.status.gradle.gpshell=/usr/local/bin/gpshell -im.status.gradle.gpshell.isd=A000000003000000 +im.status.gradle.gpshell.isd=A000000151000000 im.status.gradle.gpshell.mac_key=404142434445464748494a4b4c4d4e4f im.status.gradle.gpshell.enc_key=404142434445464748494a4b4c4d4e4f im.status.gradle.gpshell.kek_key=404142434445464748494a4b4c4d4e4f -im.status.gradle.gpshell.kvn=2 +im.status.gradle.gpshell.kvn=0 ``` ## Implementation notes diff --git a/src/main/java/im/status/wallet/WalletApplet.java b/src/main/java/im/status/wallet/WalletApplet.java index 9487ef0..0470edb 100644 --- a/src/main/java/im/status/wallet/WalletApplet.java +++ b/src/main/java/im/status/wallet/WalletApplet.java @@ -22,6 +22,9 @@ public class WalletApplet extends Applet { static final short CHAIN_CODE_SIZE = 32; static final short SEED_SIZE = CHAIN_CODE_SIZE * 2; + static final byte GET_STATUS_P1_APPLICATION = 0x00; + static final byte GET_STATUS_P1_KEY_PATH = 0x01; + static final byte LOAD_KEY_P1_EC = 0x01; static final byte LOAD_KEY_P1_EXT_EC = 0x02; static final byte LOAD_KEY_P1_SEED = 0x03; @@ -58,7 +61,8 @@ public class WalletApplet extends Applet { static final byte TLV_KEY_INITIALIZATION_STATUS = (byte) 0xC2; static final byte TLV_PUBLIC_KEY_DERIVATION = (byte) 0xC3; - 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[] 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; @@ -73,6 +77,9 @@ public class WalletApplet extends Applet { private ECPrivateKey privateKey; private byte[] chainCode; + private byte[] keyPath; + private short keyPathLen; + private Signature signature; private boolean signInProgress; private boolean expectPublicKey; @@ -101,6 +108,7 @@ public class WalletApplet extends Applet { masterPrivate = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, EC_KEY_SIZE, false); masterChainCode = new byte[32]; chainCode = new byte[32]; + keyPath = 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); @@ -176,6 +184,22 @@ public class WalletApplet extends Applet { short off = SecureChannel.SC_OUT_OFFSET; byte[] apduBuffer = apdu.getBuffer(); + short len; + + if (apduBuffer[ISO7816.OFFSET_P1] == GET_STATUS_P1_APPLICATION) { + len = getApplicationStatus(apduBuffer, off); + } else if (apduBuffer[ISO7816.OFFSET_P1] == GET_STATUS_P1_KEY_PATH) { + len = getKeyStatus(apduBuffer, off); + } else { + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + return; + } + + len = secureChannel.encryptAPDU(apduBuffer, len); + apdu.setOutgoingAndSend(ISO7816.OFFSET_CDATA, len); + } + + private short getApplicationStatus(byte[] apduBuffer, short off) { apduBuffer[off++] = TLV_APPLICATION_STATUS_TEMPLATE; apduBuffer[off++] = 9; apduBuffer[off++] = TLV_PIN_RETRY_COUNT; @@ -191,8 +215,16 @@ public class WalletApplet extends Applet { apduBuffer[off++] = 1; apduBuffer[off++] = SECP256k1.hasECPointMultiplication() ? (byte) 0x01 : (byte) 0x00; - short len = secureChannel.encryptAPDU(apduBuffer, (short) (off - SecureChannel.SC_OUT_OFFSET)); - apdu.setOutgoingAndSend(ISO7816.OFFSET_CDATA, len); + return (short) (off - SecureChannel.SC_OUT_OFFSET); + } + + private short getKeyStatus(byte[] apduBuffer, short off) { + if (expectPublicKey) { + ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); + } + + Util.arrayCopyNonAtomic(keyPath, (short) 0, apduBuffer, off, keyPathLen); + return keyPathLen; } private void verifyPIN(APDU apdu) { @@ -280,6 +312,7 @@ public class WalletApplet extends Applet { signInProgress = false; expectPublicKey = false; + keyPathLen = 0; } private void loadKeyPair(byte[] apduBuffer, boolean newExtended) { @@ -378,10 +411,11 @@ public class WalletApplet extends Applet { if (isPublicKey) { publicKey.setW(apduBuffer, ISO7816.OFFSET_CDATA, len); expectPublicKey = false; + keyPathLen += 4; return; } - if (((short) (len % 4) != 0) || (assistedDerivation && (len > 4))) { + if (((short) (len % 4) != 0) || (assistedDerivation && (len > 4)) || ((short)(len + (isReset ? 0 : keyPathLen)) > keyPath.length)) { ISOException.throwIt(ISO7816.SW_WRONG_DATA); } @@ -394,10 +428,13 @@ public class WalletApplet extends Applet { if (isReset) { resetKeys(apduBuffer, chainEnd); expectPublicKey = false; + keyPathLen = 0; } signInProgress = false; + Util.arrayCopyNonAtomic(apduBuffer, ISO7816.OFFSET_CDATA, keyPath, keyPathLen, len); + for (short i = ISO7816.OFFSET_CDATA; i < chainEnd; i += 4) { Crypto.bip32CKDPriv(apduBuffer, i, privateKey, publicKey, chainCode, (short) 0); @@ -412,6 +449,7 @@ public class WalletApplet extends Applet { } expectPublicKey = false; + keyPathLen += len; } private void outputPublicX(APDU apdu, byte[] apduBuffer) { diff --git a/src/test/java/im/status/wallet/WalletAppletCommandSet.java b/src/test/java/im/status/wallet/WalletAppletCommandSet.java index 9b07c14..423db96 100644 --- a/src/test/java/im/status/wallet/WalletAppletCommandSet.java +++ b/src/test/java/im/status/wallet/WalletAppletCommandSet.java @@ -37,13 +37,13 @@ public class WalletAppletCommandSet { return secureChannel.openSecureChannel(apduChannel); } - public ResponseAPDU getStatus() throws CardException { - CommandAPDU getStatus = new CommandAPDU(0x80, WalletApplet.INS_GET_STATUS, 0, 0, 256); + public ResponseAPDU getStatus(byte info) throws CardException { + CommandAPDU getStatus = new CommandAPDU(0x80, WalletApplet.INS_GET_STATUS, info, 0, 256); return apduChannel.transmit(getStatus); } public boolean getPublicKeyDerivationSupport() throws CardException { - ResponseAPDU resp = getStatus(); + ResponseAPDU resp = getStatus(WalletApplet.GET_STATUS_P1_APPLICATION); byte[] data = secureChannel.decryptAPDU(resp.getData()); return data[data.length - 1] == 1; } diff --git a/src/test/java/im/status/wallet/WalletAppletTest.java b/src/test/java/im/status/wallet/WalletAppletTest.java index 81b0ac1..3e5c76d 100644 --- a/src/test/java/im/status/wallet/WalletAppletTest.java +++ b/src/test/java/im/status/wallet/WalletAppletTest.java @@ -30,7 +30,10 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.security.*; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Security; +import java.security.Signature; import java.util.Arrays; import java.util.Random; @@ -116,30 +119,36 @@ public class WalletAppletTest { @DisplayName("GET STATUS command") void getStatusTest() throws CardException { // Security condition violation: SecureChannel not open - ResponseAPDU response = cmdSet.getStatus(); + ResponseAPDU response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION); assertEquals(0x6985, response.getSW()); cmdSet.openSecureChannel(); // 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. - response = cmdSet.getStatus(); + response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION); assertEquals(0x9000, response.getSW()); byte[] data = secureChannel.decryptAPDU(response.getData()); assertTrue(Hex.toHexString(data).matches("a309c00103c10105c2010[0-1]c3010[0-1]")); response = cmdSet.verifyPIN("123456"); assertEquals(0x63C2, response.getSW()); - response = cmdSet.getStatus(); + response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION); assertEquals(0x9000, response.getSW()); data = secureChannel.decryptAPDU(response.getData()); assertTrue(Hex.toHexString(data).matches("a309c00102c10105c2010[0-1]c3010[0-1]")); response = cmdSet.verifyPIN("000000"); assertEquals(0x9000, response.getSW()); - response = cmdSet.getStatus(); + response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION); assertEquals(0x9000, response.getSW()); data = secureChannel.decryptAPDU(response.getData()); assertTrue(Hex.toHexString(data).matches("a309c00103c10105c2010[0-1]c3010[0-1]")); + + // Check that key path is empty + response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_KEY_PATH); + assertEquals(0x9000, response.getSW()); + data = secureChannel.decryptAPDU(response.getData()); + assertEquals(0, data.length); } @Test @@ -467,15 +476,22 @@ public class WalletAppletTest { assertEquals(0x9000, response.getSW()); verifyKeyDerivation(keyPair, chainCode, new int[0]); - // Try to sign before load public key, then resume loading public key + // Try to sign and get key path before load public key, then resume loading public key response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, false, true, false); assertEquals(0x9000, response.getSW()); byte[] key = derivePublicKey(secureChannel.decryptAPDU(response.getData())); response = cmdSet.sign(sha256("test".getBytes()), WalletApplet.SIGN_P1_PRECOMPUTED_HASH, true, true); assertEquals(0x6985, response.getSW()); + response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_KEY_PATH); + assertEquals(0x6985, response.getSW()); response = cmdSet.deriveKey(key, false, true, true); assertEquals(0x9000, response.getSW()); verifyKeyDerivation(keyPair, chainCode, new int[] { 2 }); + + // Reset master key + response = cmdSet.deriveKey(new byte[0]); + assertEquals(0x9000, response.getSW()); + verifyKeyDerivation(keyPair, chainCode, new int[0]); } @Test @@ -748,6 +764,18 @@ public class WalletAppletTest { assertTrue(key.verify(hash, sig)); assertArrayEquals(key.getPubKeyPoint().getEncoded(false), publicKey); + + resp = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_KEY_PATH); + assertEquals(0x9000, resp.getSW()); + byte[] rawPath = secureChannel.decryptAPDU(resp.getData()); + + assertEquals(path.length * 4, rawPath.length); + + for (int i = 0; i < path.length; i++) { + int k = path[i]; + int k1 = (rawPath[i * 4] << 24) | (rawPath[(i * 4) + 1] << 16) | (rawPath[(i * 4) + 2] << 8) | rawPath[(i * 4) + 3]; + assertEquals(k, k1); + } } private byte[] derivePublicKey(byte[] data) {