document test utils

This commit is contained in:
Michele Balistreri 2017-11-04 12:54:31 +03:00
parent 10a429bf6d
commit 868c476ced
3 changed files with 232 additions and 1 deletions

View File

@ -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;

View File

@ -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);

View File

@ -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);