diff --git a/src/test/java/im/status/wallet/SecureChannelSession.java b/src/test/java/im/status/wallet/SecureChannelSession.java index 3e879b1..d28976a 100644 --- a/src/test/java/im/status/wallet/SecureChannelSession.java +++ b/src/test/java/im/status/wallet/SecureChannelSession.java @@ -15,6 +15,9 @@ import javax.smartcardio.CommandAPDU; import javax.smartcardio.ResponseAPDU; import java.security.*; +/** + * Handles a SecureChannel session with the card. + */ public class SecureChannelSession { public static final int PAYLOAD_MAX_SIZE = 223; @@ -24,7 +27,13 @@ public class SecureChannelSession { private SecretKeySpec sessionKey; private SecureRandom random; - + /** + * Constructs a SecureChannel session on the client. The client should generate a fresh key pair for each session. + * The public key of the card is used as input for the EC-DH algorithm. The output is hashed with SHA1 and is stored + * as the secret. + * + * @param keyData the public key returned by the applet as response to the SELECT command + */ public SecureChannelSession(byte[] keyData) { try { random = new SecureRandom(); @@ -49,6 +58,17 @@ public class SecureChannelSession { } } + /** + * Establishes a Secure Channel with the card. The command parameters are the public key generated in the first step. + * The card returns a secret value which must be appended to the secret previously generated through the EC-DH + * algorithm. This entire value must be hashed using SHA-256. The hash will be used as the key for an AES CBC cipher + * using ISO9797-1 Method 2 padding. From this point all further APDU must be sent encrypted and all responses from + * the card must be decrypted using this secure channel. + * + * @param apduChannel the apdu channel + * @return the card response + * @throws CardException communication error + */ public ResponseAPDU openSecureChannel(CardChannel apduChannel) throws CardException { CommandAPDU openSecureChannel = new CommandAPDU(0x80, SecureChannel.INS_OPEN_SECURE_CHANNEL, 0, 0, publicKey); ResponseAPDU response = apduChannel.transmit(openSecureChannel); @@ -66,6 +86,14 @@ public class SecureChannelSession { return response; } + /** + * Encrypts the plaintext data using the session key. The maximum plaintext size is 223 bytes. The returned ciphertext + * already includes the IV and padding and can be sent as-is in the APDU payload. If the input is an empty byte array + * the returned data will still contain the IV and padding. + * + * @param data the plaintext data + * @return the encrypted data + */ public byte[] encryptAPDU(byte[] data) { assert data.length <= PAYLOAD_MAX_SIZE; @@ -92,6 +120,13 @@ public class SecureChannelSession { } } + /** + * Decrypts the response from the card using the session key. The returned data is already stripped from IV and padding + * and can be potentially empty. + * + * @param data the ciphetext + * @return the plaintext + */ public byte[] decryptAPDU(byte[] data) { if (sessionKey == null) { return data; diff --git a/src/test/java/im/status/wallet/WalletAppletCommandSet.java b/src/test/java/im/status/wallet/WalletAppletCommandSet.java index e222246..2a71eb4 100644 --- a/src/test/java/im/status/wallet/WalletAppletCommandSet.java +++ b/src/test/java/im/status/wallet/WalletAppletCommandSet.java @@ -13,6 +13,11 @@ import javax.smartcardio.ResponseAPDU; import java.security.KeyPair; import java.security.PrivateKey; +/** + * This class is used to send APDU to the applet. Each method corresponds to an APDU as defined in the APPLICATION.md + * file. Some APDUs map to multiple methods for the sake of convenience since their payload or response require some + * pre/post processing. + */ public class WalletAppletCommandSet { public static final String APPLET_AID = "53746174757357616C6C6574417070"; public static final byte[] APPLET_AID_BYTES = Hex.decode(APPLET_AID); @@ -28,41 +33,102 @@ public class WalletAppletCommandSet { this.secureChannel = secureChannel; } + /** + * Selects the applet. The applet is assumed to have been installed with its default AID. The returned data is a + * public key which must be used to initialize the secure channel. + * + * @return the raw card response + * @throws CardException communication error + */ public ResponseAPDU select() throws CardException { CommandAPDU selectApplet = new CommandAPDU(ISO7816.CLA_ISO7816, ISO7816.INS_SELECT, 4, 0, APPLET_AID_BYTES); return apduChannel.transmit(selectApplet); } + /** + * Opens the secure channel. Calls the corresponding method of the SecureChannel class. + * + * @return the raw card response + * @throws CardException communication error + */ public ResponseAPDU openSecureChannel() throws CardException { return secureChannel.openSecureChannel(apduChannel); } + /** + * Sends a GET STATUS APDU. The info byte is the P1 parameter of the command, valid constants are defined in the applet + * class itself. + * + * @param info the P1 of the APDU + * @return the raw card response + * @throws CardException communication error + */ public ResponseAPDU getStatus(byte info) throws CardException { CommandAPDU getStatus = new CommandAPDU(0x80, WalletApplet.INS_GET_STATUS, info, 0, 256); return apduChannel.transmit(getStatus); } + /** + * Sends a GET STATUS APDU to retrieve the APPLICATION STATUS template and reads the byte indicating public key + * derivation support. + * + * @return whether public key derivation is supported or not + * @throws CardException communication error + */ public boolean getPublicKeyDerivationSupport() throws CardException { ResponseAPDU resp = getStatus(WalletApplet.GET_STATUS_P1_APPLICATION); byte[] data = secureChannel.decryptAPDU(resp.getData()); return data[data.length - 1] == 1; } + /** + * Sends a VERIFY PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU + * data. + * + * @param pin the pin + * @return the raw card response + * @throws CardException communication error + */ 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); } + /** + * Sends a CHANGE PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU + * data. + * + * @param pin the new PIN + * @return the raw card response + * @throws CardException communication error + */ public ResponseAPDU changePIN(String pin) throws CardException { CommandAPDU changePIN = new CommandAPDU(0x80, WalletApplet.INS_CHANGE_PIN, 0, 0, secureChannel.encryptAPDU(pin.getBytes())); return apduChannel.transmit(changePIN); } + /** + * Sends an UNBLOCK PIN APDU. The PUK and PIN are concatenated and the raw bytes are encrypted using the secure + * channel and used as APDU data. + * + * @return the raw card response + * @throws CardException communication error + */ public ResponseAPDU unblockPIN(String puk, String newPin) throws CardException { CommandAPDU unblockPIN = new CommandAPDU(0x80, WalletApplet.INS_UNBLOCK_PIN, 0, 0, secureChannel.encryptAPDU((puk + newPin).getBytes())); return apduChannel.transmit(unblockPIN); } + /** + * Sends a LOAD KEY APDU. The given private key and chain code are formatted as a raw binary seed and the P1 of + * the command is set to WalletApplet.LOAD_KEY_P1_SEED (0x03). This works on cards which support public key derivation. + * The loaded keyset is extended and support further key derivation. + * + * @param aPrivate a private key + * @param chainCode the chain code + * @return the raw card response + * @throws CardException communication error + */ public ResponseAPDU loadKey(PrivateKey aPrivate, byte[] chainCode) throws CardException { byte[] privateKey = ((ECPrivateKey) aPrivate).getD().toByteArray(); @@ -81,10 +147,29 @@ public class WalletAppletCommandSet { return loadKey(data, WalletApplet.LOAD_KEY_P1_SEED); } + /** + * Sends a LOAD KEY APDU. The key is sent in TLV format, includes the public key and no chain code, meaning that + * the card will not be able to do further key derivation. + * + * @param ecKeyPair a key pair + * @return the raw card response + * @throws CardException communication error + */ public ResponseAPDU loadKey(KeyPair ecKeyPair) throws CardException { return loadKey(ecKeyPair, false, null); } + /** + * Sends a LOAD KEY APDU. The key is sent in TLV format. The public key is included or not depending on the value + * of the omitPublicKey parameter. The chain code is included if the chainCode is not null. P1 is set automatically + * to either WalletApplet.LOAD_KEY_P1_EC or WalletApplet.LOAD_KEY_P1_EXT_EC depending on the presence of the chainCode. + * + * @param keyPair a key pair + * @param omitPublicKey whether the public key is sent or not + * @param chainCode the chain code + * @return the raw card response + * @throws CardException communication error + */ public ResponseAPDU loadKey(KeyPair keyPair, boolean omitPublicKey, byte[] chainCode) throws CardException { byte[] publicKey = omitPublicKey ? null : ((ECPublicKey) keyPair.getPublic()).getQ().getEncoded(false); byte[] privateKey = ((ECPrivateKey) keyPair.getPrivate()).getD().toByteArray(); @@ -92,6 +177,16 @@ public class WalletAppletCommandSet { return loadKey(publicKey, privateKey, chainCode); } + /** + * Sends a LOAD KEY APDU. The key is sent in TLV format, includes the public key and no chain code, meaning that + * the card will not be able to do further key derivation. This is needed when the argument is an EC keypair from + * the web3j package instead of the regular Java ones. Used by the test which actually submits the transaction to + * the network. + * + * @param ecKeyPair a key pair + * @return the raw card response + * @throws CardException communication error + */ public ResponseAPDU loadKey(ECKeyPair ecKeyPair) throws CardException { byte[] publicKey = ecKeyPair.getPublicKey().toByteArray(); byte[] privateKey = ecKeyPair.getPrivateKey().toByteArray(); @@ -111,6 +206,17 @@ public class WalletAppletCommandSet { return loadKey(ansiPublic, privateKey, null); } + /** + * Sends a LOAD KEY APDU. The key is sent in TLV format. The public key is included if not null. The chain code is + * included if not null. P1 is set automatically to either WalletApplet.LOAD_KEY_P1_EC or + * WalletApplet.LOAD_KEY_P1_EXT_EC depending on the presence of the chainCode. + * + * @param publicKey a raw public key + * @param privateKey a raw private key + * @param chainCode the chain code + * @return the raw card response + * @throws CardException communication error + */ public ResponseAPDU loadKey(byte[] publicKey, byte[] privateKey, byte[] chainCode) throws CardException { int privLen = privateKey.length; int privOff = 0; @@ -167,26 +273,73 @@ public class WalletAppletCommandSet { return loadKey(data, p1); } + /** + * Sends a LOAD KEY APDU. The data is encrypted and sent as-is. The keyType parameter is used as P1. + * + * @param data key data + * @param keyType the P1 parameter + * @return the raw card response + * @throws CardException communication error + */ public ResponseAPDU loadKey(byte[] data, byte keyType) throws CardException { CommandAPDU loadKey = new CommandAPDU(0x80, WalletApplet.INS_LOAD_KEY, keyType, 0, secureChannel.encryptAPDU(data)); return apduChannel.transmit(loadKey); } + /** + * Sends a GENERATE MNEMONIC APDU. The cs parameter is the length of the checksum and is used as P1. + * + * @param cs the P1 parameter + * @return the raw card response + * @throws CardException communication error + */ public ResponseAPDU generateMnemonic(int cs) throws CardException { CommandAPDU generateMnemonic = new CommandAPDU(0x80, WalletApplet.INS_GENERATE_MNEMONIC, cs, 0, 256); return apduChannel.transmit(generateMnemonic); } + /** + * Sends a SIGN APDU. The dataType is P1 as defined in the applet. The isFirst and isLast arguments are used to form + * the P2 parameter. The data is the data to sign, or part of it. Only when sending the last block a signature is + * generated and thus returned. When signing a precomputed hash it must be done in a single block, so isFirst and + * isLast will always be true at the same time. + * + * @param data the data to sign + * @param dataType the P1 parameter + * @param isFirst whether this is the first block of the command or not + * @param isLast whether this is the last block of the command or not + * @return the raw card response + * @throws CardException communication error + */ public ResponseAPDU sign(byte[] data, byte dataType, boolean isFirst, boolean isLast) throws CardException { byte p2 = (byte) ((isFirst ? 0x01 : 0x00) | (isLast ? 0x80 : 0x00)); CommandAPDU sign = new CommandAPDU(0x80, WalletApplet.INS_SIGN, dataType, p2, secureChannel.encryptAPDU(data)); return apduChannel.transmit(sign); } + /** + * Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The P1 and P2 parameters are forced to 0, meaning + * that the derivation starts from the master key and is non-assisted. + * + * @param data the raw key path + * @return the raw card response + * @throws CardException communication error + */ public ResponseAPDU deriveKey(byte[] data) throws CardException { return deriveKey(data, true, false, false); } + /** + * Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The reset and assisted parameters are combined to + * form P1. The isPublicKey parameter is used for P2. + * + * @param data the raw key path or a public key + * @param reset whether the derivation must start from the master key or not + * @param assisted whether we are doing assisted derivation or not + * @param isPublicKey whether we are sending a public key or a key path (only make sense during assisted derivation) + * @return the raw card response + * @throws CardException communication error + */ public ResponseAPDU deriveKey(byte[] data, boolean reset, boolean assisted, boolean isPublicKey) throws CardException { byte p1 = assisted ? WalletApplet.DERIVE_P1_ASSISTED_MASK : 0; p1 |= reset ? 0 : WalletApplet.DERIVE_P1_APPEND_MASK; @@ -196,11 +349,25 @@ public class WalletAppletCommandSet { return apduChannel.transmit(deriveKey); } + /** + * Sends a SET PINLESS PATH APDU. The data is encrypted and sent as-is. + * + * @param data the raw key path + * @return the raw card response + * @throws CardException communication error + */ 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); } + /** + * Sends an EXPORT KEY APDU. The keyPathIndex is used as P1. Valid values are defined in the applet itself + * + * @param keyPathIndex the P1 parameter + * @return the raw card response + * @throws CardException communication error + */ public ResponseAPDU exportKey(byte keyPathIndex) throws CardException { CommandAPDU exportKey = new CommandAPDU(0x80, WalletApplet.INS_EXPORT_KEY, keyPathIndex, 0x00, 256); return apduChannel.transmit(exportKey); diff --git a/src/test/java/im/status/wallet/WalletAppletTest.java b/src/test/java/im/status/wallet/WalletAppletTest.java index c702019..186c008 100644 --- a/src/test/java/im/status/wallet/WalletAppletTest.java +++ b/src/test/java/im/status/wallet/WalletAppletTest.java @@ -948,6 +948,21 @@ public class WalletAppletTest { return key; } + /** + * This method takes the response from the first stage of an assisted key derivation command and derives the complete + * public key from the received X and signature. Outside of test code, proper TLV parsing would be a better idea, here + * we just assume that the data is where we expect it to be. + * + * The algorithm used to derive the public key is dead simple. We take the X and we preprend the 0x02 byte so it + * becomes a compressed public key with even parity. We then try to verify the signature using this key. If it verifies + * then we have found the key, otherwise we set the first byte to 0x03 to turn the key to odd parity. Again we try + * to verify the signature using this key, it must work this time. + * + * We then uncompress the point we found and return it. This will be sent in the next DERIVE KEY command. + * + * @param data the unencrypted response from the card + * @return the uncompressed public key + */ private byte[] derivePublicKey(byte[] data) { byte[] pubKey = Arrays.copyOfRange(data, 3, 4 + data[3]); byte[] signature = Arrays.copyOfRange(data, 4 + data[3], data.length); @@ -964,6 +979,20 @@ public class WalletAppletTest { return candidate.decompress().getPubKey(); } + /** + * Signs a signature using the card. Returns a SignatureData object which contains v, r and s. The algorithm to do + * this is as follow: + * + * 1) The Keccak-256 hash of transaction is generated off-card + * 2) A SIGN command is sent to the card to sign the precomputed hash + * 3) The returned data is the public key and the signature + * 4) The signature and public key can be used to generate the v value. The v value allows to recover the public key + * from the signature. Here we use the web3j implementation through reflection + * 5) v, r and s are the final signature to append to the transaction + * + * @param message the raw transaction + * @return the signature data + */ private Sign.SignatureData signMessage(byte[] message) throws Exception { byte[] messageHash = Hash.sha3(message);