diff --git a/APPLICATION.MD b/APPLICATION.MD index b619ba4..63d215d 100644 --- a/APPLICATION.MD +++ b/APPLICATION.MD @@ -56,6 +56,7 @@ Response Data format: - Tag 0x80 = ECC public Key - Tag 0x02 = Application Version (2 bytes) - Tag 0x02 = Number of remaining pairing slots (1 byte) + - Tag 0x8E = Key UID (0 or 32 bytes) The SELECT command is documented in the ISO 7816-4 specifications and is used to select the application on the card, making it the active one. The data field is the AID of the application. The response is the Application Info template @@ -64,6 +65,8 @@ which must be used by the client to establish the Secure Channel. Additionally i application, formatted on two bytes. The first byte is the major version and the second is the minor version (e.g: version 1.1 is formatted as 0x0101). The number of remaining pairing slots is also included in the response. +The Key UID can be either empty (when no key is loaded on card) or the SHA-256 hash of the master public key. + ### OPEN SECURE CHANNEL The OPEN SECURE CHANNEL command is as specified in the [SECURE_CHANNEL.MD](SECURE_CHANNEL.MD). @@ -166,6 +169,7 @@ always returns 0x63C0, even if the PUK is inserted correctly. In this case the w * Data = the key data * Response SW = 0x9000 on success, 0x6A80 if the format is invalid, 0x6A86 if P1 is invalid, 0x6A81 if public key derivation is not supported and the public key is omitted +* Response Data = the key UID, defined as the SHA-256 of the public key * Preconditions: Secure Channel must be opened, user PIN must be verified P1: diff --git a/build.gradle b/build.gradle index c4abd21..a34d5b8 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ javacard { aid = '0x53:0x74:0x61:0x74:0x75:0x73:0x57:0x61:0x6c:0x6c:0x65:0x74:0x41:0x70:0x70' className = 'WalletApplet' } - version = '1.1' + version = '1.2' } } diff --git a/src/main/java/im/status/wallet/WalletApplet.java b/src/main/java/im/status/wallet/WalletApplet.java index 5fe780e..b5060dd 100644 --- a/src/main/java/im/status/wallet/WalletApplet.java +++ b/src/main/java/im/status/wallet/WalletApplet.java @@ -7,7 +7,7 @@ import javacard.security.*; * The applet's main class. All incoming commands a processed by this class. */ public class WalletApplet extends Applet { - static final short APPLICATION_VERSION = (short) 0x0101; + static final short APPLICATION_VERSION = (short) 0x0102; static final byte INS_GET_STATUS = (byte) 0xF2; static final byte INS_VERIFY_PIN = (byte) 0x20; @@ -31,6 +31,7 @@ public class WalletApplet extends Applet { static final short EC_KEY_SIZE = 256; static final short CHAIN_CODE_SIZE = 32; + static final short KEY_UID_LENGTH = 32; static final short BIP39_SEED_SIZE = CHAIN_CODE_SIZE * 2; static final byte GET_STATUS_P1_APPLICATION = 0x00; @@ -81,6 +82,7 @@ public class WalletApplet extends Applet { static final byte TLV_APPLICATION_INFO_TEMPLATE = (byte) 0xA4; static final byte TLV_UID = (byte) 0x8F; + static final byte TLV_KEY_UID = (byte) 0x8E; 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 EXPORT_KEY_HIGH_MASK = (byte) 0xc0; @@ -114,6 +116,8 @@ public class WalletApplet extends Applet { private boolean signInProgress; private boolean expectPublicKey; + private byte[] keyUID; + private Crypto crypto; private SECP256k1 secp256k1; @@ -161,6 +165,8 @@ public class WalletApplet extends Applet { keyPath = new byte[KEY_PATH_MAX_DEPTH * 4]; pinlessPath = new byte[KEY_PATH_MAX_DEPTH * 4]; + keyUID = new byte[KEY_UID_LENGTH]; + resetCurveParameters(); signature = Signature.getInstance(Signature.ALG_ECDSA_SHA_256, false); @@ -309,7 +315,17 @@ public class WalletApplet extends Applet { apduBuffer[(short)(UID_LENGTH + keyLength + 10)] = TLV_INT; apduBuffer[(short)(UID_LENGTH + keyLength + 11)] = 1; apduBuffer[(short)(UID_LENGTH + keyLength + 12)] = secureChannel.getRemainingPairingSlots(); - apduBuffer[1] = (byte)(keyLength + UID_LENGTH + 11); + apduBuffer[(short)(UID_LENGTH + keyLength + 13)] = TLV_KEY_UID; + + if (privateKey.isInitialized()) { + apduBuffer[(short)(UID_LENGTH + keyLength + 14)] = KEY_UID_LENGTH; + Util.arrayCopyNonAtomic(keyUID, (short) 0, apduBuffer, (short)(UID_LENGTH + keyLength + 15), KEY_UID_LENGTH); + keyLength += KEY_UID_LENGTH; + } else { + apduBuffer[(short)(UID_LENGTH + keyLength + 14)] = 0; + } + + apduBuffer[1] = (byte)(keyLength + UID_LENGTH + 13); apdu.setOutgoingAndSend((short) 0, (short)(apduBuffer[1] + 2)); } @@ -485,6 +501,11 @@ public class WalletApplet extends Applet { ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); break; } + + short pubLen = masterPublic.getW(apduBuffer, (short) 0); + crypto.sha256.doFinal(apduBuffer, (short) 0, pubLen, keyUID, (short) 0); + Util.arrayCopyNonAtomic(keyUID, (short) 0, apduBuffer, SecureChannel.SC_OUT_OFFSET, KEY_UID_LENGTH); + secureChannel.respond(apdu, KEY_UID_LENGTH, ISO7816.SW_NO_ERROR); } /** diff --git a/src/test/java/im/status/wallet/WalletAppletTest.java b/src/test/java/im/status/wallet/WalletAppletTest.java index d68eae2..539e659 100644 --- a/src/test/java/im/status/wallet/WalletAppletTest.java +++ b/src/test/java/im/status/wallet/WalletAppletTest.java @@ -34,6 +34,7 @@ import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.Security; import java.security.Signature; +import org.bouncycastle.jce.interfaces.ECPublicKey; import java.util.Arrays; import java.util.Random; @@ -117,6 +118,7 @@ public class WalletAppletTest { assertEquals(WalletApplet.APPLICATION_VERSION >> 8, data[24 + data[21]]); assertEquals(WalletApplet.APPLICATION_VERSION & 0xFF, data[25 + data[21]]); assertEquals(WalletApplet.TLV_INT, data[26 + data[21]]); + assertEquals(WalletApplet.TLV_KEY_UID, data[29 + data[21]]); } @Test @@ -495,18 +497,22 @@ public class WalletAppletTest { // Correct LOAD KEY response = cmdSet.loadKey(keyPair); assertEquals(0x9000, response.getSW()); + verifyKeyUID(response.getData(), ((ECPublicKey) keyPair.getPublic())); keyPair = g.generateKeyPair(); // Check extended key response = cmdSet.loadKey(keyPair, false, chainCode); assertEquals(0x9000, response.getSW()); + verifyKeyUID(response.getData(), ((ECPublicKey) keyPair.getPublic())); // Check omitted public key response = cmdSet.loadKey(keyPair, true, null); assertEquals(publicKeyDerivationSW, response.getSW()); + verifyKeyUID(response.getData(), ((ECPublicKey) keyPair.getPublic())); response = cmdSet.loadKey(keyPair, true, chainCode); assertEquals(publicKeyDerivationSW, response.getSW()); + verifyKeyUID(response.getData(), ((ECPublicKey) keyPair.getPublic())); // Check seed load response = cmdSet.loadKey(keyPair.getPrivate(), chainCode); @@ -571,6 +577,16 @@ public class WalletAppletTest { response = cmdSet.loadKey(keyPair); assertEquals(0x9000, response.getSW()); + response = cmdSet.select(); + assertEquals(0x9000, response.getSW()); + byte[] data = response.getData(); + assertEquals(32, data[30 + data[21]]); + verifyKeyUID(Arrays.copyOfRange(data, (31 + data[21]), (63 + data[21])), (ECPublicKey) keyPair.getPublic()); + + cmdSet.autoOpenSecureChannel(); + response = cmdSet.verifyPIN("000000"); + assertEquals(0x9000, response.getSW()); + assertTrue(cmdSet.getKeyInitializationStatus()); // Good case @@ -578,6 +594,11 @@ public class WalletAppletTest { assertEquals(0x9000, response.getSW()); assertFalse(cmdSet.getKeyInitializationStatus()); + + response = cmdSet.select(); + assertEquals(0x9000, response.getSW()); + data = response.getData(); + assertEquals(0, data[30 + data[21]]); } @Test @@ -1524,4 +1545,12 @@ public class WalletAppletTest { return new Sign.SignatureData(v, rB, sB); } + + private void verifyKeyUID(byte[] keyUID, ECPublicKey pubKey) { + verifyKeyUID(keyUID, pubKey.getQ().getEncoded(false)); + } + + private void verifyKeyUID(byte[] keyUID, byte[] pubKey) { + assertArrayEquals(sha256(pubKey), keyUID); + } }