diff --git a/APPLICATION.MD b/APPLICATION.MD index 8bea360..7257c84 100644 --- a/APPLICATION.MD +++ b/APPLICATION.MD @@ -55,6 +55,22 @@ 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 +* P2 = 0x00 +* Response SW = 0x9000 on success +* Response Data = Application Status Template +* Preconditions: Secure Channel must be opened + +Response Data format: +- 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 = 0 if public key derivation is not supported, 1 otherwise + ### VERIFY PIN * CLA = 0x80 @@ -143,14 +159,14 @@ signing sessions, if any. Unless a DERIVE KEY is sent, a subsequent SIGN command * Data = key derivation template * Response SW = 0x9000 on success, 0x6A80 if the format is invalid, 0x6A81 if public keys are omitted and their derivation is not supported. -* Preconditions: Secure Channel must be opened, user PIN must be verified +* Preconditions: Secure Channel must be opened, user PIN must be verified, an extended keyset must be loaded Data format: - Tag 0xA2 = key derivation template - - Tag 0x82 = a sequence of 32-bit integers (most significant byte first). Empty if the master key must be used. - - Tag 0x81 = parent public key (omitted if master or public key derivation is supported) - - Tag 0x80 = derived public key (omitted if master or public key derivation is supported) + - Tag 0xC0 = a sequence of 32-bit integers (most significant byte first). Empty if the master key must be used. + - Tag 0xC1 = derived public key (omitted if master or public key derivation is supported) + - Tag 0xC2 = parent public key (omitted if master or public key derivation is supported) This command is used before a signing session to generated a private key according to the [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) specifications. The generated key is used for all subsequent SIGN sessions. An empty 0x82 is used in order for SIGN to diff --git a/src/main/java/im/status/wallet/WalletApplet.java b/src/main/java/im/status/wallet/WalletApplet.java index bb817c1..d00247f 100644 --- a/src/main/java/im/status/wallet/WalletApplet.java +++ b/src/main/java/im/status/wallet/WalletApplet.java @@ -4,10 +4,13 @@ import javacard.framework.*; import javacard.security.*; public class WalletApplet extends Applet { + static final byte INS_GET_STATUS = (byte) 0xF2; static final byte INS_VERIFY_PIN = (byte) 0x20; static final byte INS_CHANGE_PIN = (byte) 0x21; static final byte INS_UNBLOCK_PIN = (byte) 0x22; static final byte INS_LOAD_KEY = (byte) 0xD0; + 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 PUK_LENGTH = 12; @@ -18,6 +21,8 @@ public class WalletApplet extends Applet { static final short EC_KEY_SIZE = 256; 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; static final byte SIGN_P1_DATA = 0x00; static final byte SIGN_P1_PRECOMPUTED_HASH = 0x01; @@ -26,9 +31,22 @@ public class WalletApplet extends Applet { static final byte SIGN_P2_LAST_BLOCK_MASK = (byte) 0x80; static final byte TLV_SIGNATURE_TEMPLATE = (byte) 0xA0; + static final byte TLV_KEY_TEMPLATE = (byte) 0xA1; static final byte TLV_PUB_KEY = (byte) 0x80; static final byte TLV_PRIV_KEY = (byte) 0x81; + static final byte TLV_CHAIN_CODE = (byte) 0x82; + + static final byte TLV_KEY_DERIVATION_TEMPLATE = (byte) 0xA2; + static final byte TLV_DERIVATION_SEQUENCE = (byte) 0xC0; + static final byte TLV_DERIVED_PUB_KEY = (byte) 0xC1; + static final byte TLV_PARENT_PUB_KEY = (byte) 0xC2; + + static final byte TLV_APPLICATION_STATUS_TEMPLATE = (byte) 0xA3; + static final byte TLV_PIN_RETRY_COUNT = (byte) 0xC0; + static final byte TLV_PUK_RETRY_COUNT = (byte) 0xC1; + static final byte TLV_KEY_INITIALIZATION_STATUS = (byte) 0xC2; + static final byte TLV_PUBLIC_KEY_DERIVATION_SUPPORTED = (byte) 0xC3; private OwnerPIN pin; private OwnerPIN puk; @@ -77,6 +95,9 @@ public class WalletApplet extends Applet { case SecureChannel.INS_OPEN_SECURE_CHANNEL: secureChannel.openSecureChannel(apdu); break; + case INS_GET_STATUS: + getStatus(apdu); + break; case INS_VERIFY_PIN: verifyPIN(apdu); break; @@ -89,6 +110,12 @@ public class WalletApplet extends Applet { case INS_LOAD_KEY: loadKey(apdu); break; + case INS_DERIVE_KEY: + deriveKey(apdu); + break; + case INS_GENERATE_MNEMONIC: + generateMnemonic(apdu); + break; case INS_SIGN: sign(apdu); break; @@ -108,6 +135,33 @@ public class WalletApplet extends Applet { apdu.setOutgoingAndSend(ISO7816.OFFSET_CDATA, keyLength); } + private void getStatus(APDU apdu) { + if (!secureChannel.isOpen()) { + ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); + } + + short off = SecureChannel.SC_OUT_OFFSET; + byte[] apduBuffer = apdu.getBuffer(); + + apduBuffer[off++] = TLV_APPLICATION_STATUS_TEMPLATE; + apduBuffer[off++] = 12; + apduBuffer[off++] = TLV_PIN_RETRY_COUNT; + apduBuffer[off++] = 1; + apduBuffer[off++] = pin.getTriesRemaining(); + apduBuffer[off++] = TLV_PUK_RETRY_COUNT; + apduBuffer[off++] = 1; + apduBuffer[off++] = puk.getTriesRemaining(); + apduBuffer[off++] = TLV_KEY_INITIALIZATION_STATUS; + apduBuffer[off++] = 1; + apduBuffer[off++] = privateKey.isInitialized() ? (byte) 0x01 : (byte) 0x00; + apduBuffer[off++] = TLV_PUBLIC_KEY_DERIVATION_SUPPORTED; + apduBuffer[off++] = 1; + apduBuffer[off++] = 1; //TODO: actually check if it is supported or totally remove if a fallback software implementation is a requirement + + short len = secureChannel.encryptAPDU(apduBuffer, (short) (off - SecureChannel.SC_OUT_OFFSET)); + apdu.setOutgoingAndSend(ISO7816.OFFSET_CDATA, len); + } + private void verifyPIN(APDU apdu) { apdu.setIncomingAndReceive(); @@ -199,6 +253,14 @@ public class WalletApplet extends Applet { signInProgress = false; } + private void deriveKey(APDU apdu) { + ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED); + } + + private void generateMnemonic(APDU apdu) { + ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED); + } + private void sign(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 711fc95..7d42bfd 100644 --- a/src/test/java/im/status/wallet/WalletAppletCommandSet.java +++ b/src/test/java/im/status/wallet/WalletAppletCommandSet.java @@ -36,6 +36,11 @@ public class WalletAppletCommandSet { return secureChannel.openSecureChannel(apduChannel); } + public ResponseAPDU getStatus() throws CardException { + CommandAPDU getStatus = new CommandAPDU(0x80, WalletApplet.INS_GET_STATUS, 0, 0); + return apduChannel.transmit(getStatus); + } + public ResponseAPDU verifyPIN(String pin) throws CardException { CommandAPDU verifyPIN = new CommandAPDU(0x80, WalletApplet.INS_VERIFY_PIN, 0, 0, secureChannel.encryptAPDU(pin.getBytes())); return apduChannel.transmit(verifyPIN); diff --git a/src/test/java/im/status/wallet/WalletAppletTest.java b/src/test/java/im/status/wallet/WalletAppletTest.java index de1441e..6cb10ab 100644 --- a/src/test/java/im/status/wallet/WalletAppletTest.java +++ b/src/test/java/im/status/wallet/WalletAppletTest.java @@ -12,7 +12,6 @@ import org.web3j.crypto.*; import org.web3j.protocol.Web3j; import org.web3j.protocol.core.DefaultBlockParameterName; import org.web3j.protocol.core.methods.request.RawTransaction; -import org.web3j.protocol.core.methods.response.EthGetTransactionReceipt; import org.web3j.protocol.core.methods.response.EthSendTransaction; import org.web3j.protocol.http.HttpService; import org.web3j.tx.Transfer; @@ -106,6 +105,36 @@ public class WalletAppletTest { assertEquals(SecureChannel.SC_SECRET_LENGTH, response.getData().length); } + @Test + @DisplayName("GET STATUS command") + void getStatusTest() throws CardException { + // Security condition violation: SecureChannel not open + ResponseAPDU response = cmdSet.getStatus(); + 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, the public key derivation cannot also be known here. + response = cmdSet.getStatus(); + assertEquals(0x9000, response.getSW()); + byte[] data = secureChannel.decryptAPDU(response.getData()); + assertTrue(Hex.toHexString(data).matches("a30cc00103c10105c2010[0-1]c3010[0-1]")); + + response = cmdSet.verifyPIN("123456"); + assertEquals(0x63C2, response.getSW()); + response = cmdSet.getStatus(); + assertEquals(0x9000, response.getSW()); + data = secureChannel.decryptAPDU(response.getData()); + assertTrue(Hex.toHexString(data).matches("a30cc00102c10105c2010[0-1]c3010[0-1]")); + + response = cmdSet.verifyPIN("000000"); + assertEquals(0x9000, response.getSW()); + response = cmdSet.getStatus(); + assertEquals(0x9000, response.getSW()); + data = secureChannel.decryptAPDU(response.getData()); + assertTrue(Hex.toHexString(data).matches("a30cc00103c10105c2010[0-1]c3010[0-1]")); + } + @Test @DisplayName("VERIFY PIN command") void verifyPinTest() throws CardException { @@ -442,8 +471,6 @@ public class WalletAppletTest { } assertFalse(ethSendTransaction.hasError()); - - EthGetTransactionReceipt receipt = web3j.ethGetTransactionReceipt(ethSendTransaction.getTransactionHash()).send(); } private KeyPairGenerator keypairGenerator() throws Exception { @@ -481,7 +508,7 @@ public class WalletAppletTest { cmdSet.openSecureChannel(); } - public Sign.SignatureData signMessage(byte[] message) throws Exception { + private Sign.SignatureData signMessage(byte[] message) throws Exception { byte[] messageHash = Hash.sha3(message); ResponseAPDU response = cmdSet.sign(messageHash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);