mirror of
https://github.com/status-im/status-keycard.git
synced 2025-01-13 07:14:11 +00:00
document test utils
This commit is contained in:
parent
10a429bf6d
commit
868c476ced
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user