remove assisted key derivation

This commit is contained in:
Michele Balistreri 2018-11-07 15:17:17 +03:00
parent 22b41ab781
commit e78397c198
5 changed files with 76 additions and 478 deletions

View File

@ -1,5 +1,14 @@
# Status Wallet Application
## Version
Version numbers are in the form major.minor. An major revision increment indicates the presence of breaking changes as
compared to the previous released version. This is version 2.0 of the specs (unreleased).
### Changes since 1.2
* **BREAKING** Removed assisted key derivation
* Added internal key generation
## Overview
This application allows signing of transactions using ECDSA with a keyset stored on card. The keys are defined on the
@ -130,8 +139,7 @@ command to work.
* P2 = 0x00
* Response SW = 0x9000 on success, 0x6A86 on undefined P1
* Response Data = Application Status Template or Key Path
* Preconditions: Secure Channel must be opened, if Key Path is required then the card must not be in the middle of a
derivation session
* Preconditions: Secure Channel must be opened
Response Data format:
if P1 = 0x00:
@ -139,7 +147,6 @@ if P1 = 0x00:
- Tag 0x02 = PIN retry count (1 byte)
- Tag 0x02 = PUK retry count (1 byte)
- Tag 0x01 = 0xff if key is initialized, 0 otherwise
- Tag 0x01 = 0xff if public key derivation is supported, 0 otherwise
if P1 = 0x01
- a sequence of 32-bit numbers indicating the current key path. Empty if master key is selected.
@ -203,8 +210,7 @@ always returns 0x63C0, even if the PUK is inserted correctly. In this case the w
* P1 = key type
* P2 = 0x00
* 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 SW = 0x9000 on success, 0x6A80 if the format is invalid, 0x6A86 if P1 is invalid
* 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
@ -217,7 +223,7 @@ Data:
If P1 is 0x01 or 0x02
- Tag 0xA1 = keypair template
- Tag 0x80 = ECC public key component (can be omitted if public key derivation is supported)
- Tag 0x80 = ECC public key component (can be omitted)
- Tag 0x81 = ECC private key component
- Tag 0x82 = chain code (if P1=0x02)
@ -233,54 +239,33 @@ signing sessions, if any. Unless a DERIVE KEY is sent, a subsequent SIGN command
* CLA = 0x80
* INS = 0xD1
* P1 = derivation options
* P2 = assisted derivation phase
* P2 = 0x00
* Data = a sequence of 32-bit integers (most significant byte first). Empty if the master key must be used. On assisted
derivation contains a public key when P2 = 0x02.
* Response SW = 0x9000 on success, 0x6A80 if the format is invalid, 0x6A81 if public key derivation is not supported and
bit 0 of P1 is set, 0x6A86 if P2 = 0x01 and bit 0 of P1 is not set, 0x6984 if one of the components in the path
* Response SW = 0x9000 on success, 0x6A80 if the format is invalid, 0x6984 if one of the components in the path
generates an invalid key, 0x6B00 if derivation from parent keys is selected but no valid parent key is cached.
* Response Data = On assisted derivation and P2 = 0x01 the key derivation template. Empty otherwise.
* Preconditions: Secure Channel must be opened, user PIN must be verified (if no PIN-less key is defined), an extended
keyset must be loaded
This command is used before a signing session to generate a private key according to the [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
specifications. This command always aborts open signing sessions, if any. The generated key is used for all subsequent
SIGN sessions. Because JavaCard does not offer native EC point multiplication before version 3.0.5, there is an
alternative mode of operation where the public key is partially derived off-card and loaded back on card. In this mode
of operation only 1 derivation step at the time can be completed and as such during assisted derivation the data can
contain a single 32-bit integer, instead of a sequence. The maximum depth of derivation from the master key is 10. Any
attempt to get deeper results in 0x6A80 being returned. The BIP32 specifications define a few checks which must be
performed on the derived keys. If these fail, the 0x6984 is returned and the invalid key is discarded. A client should
perform a GET STATUS command to get the actual current key path and resume derivation using a different path.
SIGN sessions. The maximum depth of derivation from the master key is 10. Any attempt to get deeper results in 0x6A80
being returned. The BIP32 specifications define a few checks which must be performed on the derived keys. If these fail,
the 0x6984 is returned and the invalid key is discarded. A client should perform a GET STATUS command to get the actual
current key path and resume derivation using a different path.
The ability to start derivation from the parent keys allows to more efficiently switch between children of the same key.
Note however that only the immediate parent of the current key is cached so you cannot use this to go back in the
hierarchy. If no valid parent key is available the status code 0x6B00 will be returned.
When loading the public key using assisted derivation the only valid P1/P2 combination is P1=0x81 and P2=0x01.
P1:
* bit 0 = if 0 derive autonomously (only works if public key derivation is supported), if 1 do assisted derivation
* bit 1-5 = reserved
* bit 0-5 = reserved
* bit 7-6:
- 00 derive from master keys
- 01 derive from parent keys
- 10 derive from current keys
- 11 reserved
P2:
* 0x00 = data is a sequence of 32-bit integers
* 0x01 = data is a public key
Response Data format:
The signature is calculated over the SHA-256 hash of the message "STATUS KEY DERIVATION"
- Tag 0xA2 = key derivation template
- Tag 0x83 = ECC Public Key X component
- Tag 0x30 = ECDSA Signature
- Tag 0x02 = R value
- Tag 0x02 = S value
### GENERATE MNEMONIC
* CLA = 0x80

View File

@ -1,8 +1,5 @@
package im.status.wallet;
import javacard.framework.ISO7816;
import javacard.framework.ISOException;
import javacard.security.CryptoException;
import javacard.security.ECKey;
import javacard.security.ECPrivateKey;
import javacard.security.KeyAgreement;
@ -60,12 +57,7 @@ public class SECP256k1 {
*/
SECP256k1(Crypto crypto) {
this.crypto = crypto;
try {
ecPointMultiplier = KeyAgreement.getInstance(ALG_EC_SVDP_DH_PLAIN_XY, false);
} catch(CryptoException e) {
ecPointMultiplier = null;
}
ecPointMultiplier = KeyAgreement.getInstance(ALG_EC_SVDP_DH_PLAIN_XY, false);
}
/**
@ -95,21 +87,6 @@ public class SECP256k1 {
return multiplyPoint(privateKey, SECP256K1_G, (short) 0, (short) SECP256K1_G.length, pubOut, pubOff);
}
/**
* Derives the X of the public key from the given private key and outputs it in the xOut buffer. This is the plain
* output of the EC-DH algorithm calculated using the G point of the curve in place of the public key of the second
* party.
*
* @param privateKey the private key
* @param xOut the output buffer for the X of the public key
* @param xOff the offset in xOut
* @return the length of X
*/
short derivePublicX(ECPrivateKey privateKey, byte[] xOut, short xOff) {
crypto.ecdh.init(privateKey);
return crypto.ecdh.generateSecret(SECP256K1_G, (short) 0, (short) SECP256K1_G.length, xOut, xOff);
}
/**
* Multiplies a scalar in the form of a private key by the given point. Internally uses a special version of EC-DH
* supported since JavaCard 3.0.5 which outputs both X and Y in their uncompressed form.
@ -123,27 +100,7 @@ public class SECP256k1 {
* @return the length of the data written in the out buffer
*/
short multiplyPoint(ECPrivateKey privateKey, byte[] point, short pointOff, short pointLen, byte[] out, short outOff) {
assertECPointMultiplicationSupport();
ecPointMultiplier.init(privateKey);
return ecPointMultiplier.generateSecret(point, pointOff, pointLen, out, outOff);
}
/**
* Returns whether the card supports EC point multiplication or not.
*
* @return whether the card supports EC point multiplication or not
*/
boolean hasECPointMultiplication() {
return ecPointMultiplier != null;
}
/**
* Asserts that EC point multiplication is supported. If not, the 0x6A81 status word is returned by throwing an
* ISOException.
*/
void assertECPointMultiplicationSupport() {
if(!hasECPointMultiplication()) {
ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED);
}
}
}

View File

@ -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) 0x0102;
static final short APPLICATION_VERSION = (short) 0x0200;
static final byte INS_INIT = (byte) 0xFE;
static final byte INS_GET_STATUS = (byte) 0xF2;
@ -53,15 +53,10 @@ public class WalletApplet extends Applet {
static final byte SIGN_P2_FIRST_BLOCK_MASK = 0x01;
static final byte SIGN_P2_LAST_BLOCK_MASK = (byte) 0x80;
static final byte DERIVE_P1_ASSISTED_MASK = 0x01;
static final byte DERIVE_P1_SOURCE_MASK = (byte) 0xC0;
static final byte DERIVE_P1_SOURCE_MASTER = (byte) 0x00;
static final byte DERIVE_P1_SOURCE_PARENT = (byte) 0x40;
static final byte DERIVE_P1_SOURCE_CURRENT = (byte) 0x80;
static final byte DERIVE_P2_KEY_PATH = 0x00;
static final byte DERIVE_P2_PUBLIC_KEY = 0x01;
static final byte GENERATE_MNEMONIC_P1_CS_MIN = 4;
static final byte GENERATE_MNEMONIC_P1_CS_MAX = 8;
static final byte GENERATE_MNEMONIC_TMP_OFF = SecureChannel.SC_OUT_OFFSET + ((((GENERATE_MNEMONIC_P1_CS_MAX * 32) + GENERATE_MNEMONIC_P1_CS_MAX) / 11) * 2);
@ -79,9 +74,6 @@ public class WalletApplet extends Applet {
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_PUB_X = (byte) 0x83;
static final byte TLV_APPLICATION_STATUS_TEMPLATE = (byte) 0xA3;
static final byte TLV_INT = (byte) 0x02;
static final byte TLV_BOOL = (byte) 0x01;
@ -90,7 +82,6 @@ public class WalletApplet extends Applet {
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;
private OwnerPIN pin;
@ -120,7 +111,6 @@ public class WalletApplet extends Applet {
private Signature signature;
private boolean signInProgress;
private boolean expectPublicKey;
private byte[] keyUID;
@ -419,9 +409,6 @@ public class WalletApplet extends Applet {
apduBuffer[off++] = TLV_BOOL;
apduBuffer[off++] = 1;
apduBuffer[off++] = privateKey.isInitialized() ? (byte) 0xFF : (byte) 0x00;
apduBuffer[off++] = TLV_BOOL;
apduBuffer[off++] = 1;
apduBuffer[off++] = secp256k1.hasECPointMultiplication() ? (byte) 0xFF : (byte) 0x00;
return (short) (off - SecureChannel.SC_OUT_OFFSET);
}
@ -437,10 +424,6 @@ public class WalletApplet extends Applet {
* @return the length in bytes of the data to output
*/
private short getKeyStatus(byte[] apduBuffer, short off) {
if (expectPublicKey) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
Util.arrayCopyNonAtomic(keyPath, (short) 0, apduBuffer, off, keyPathLen);
return keyPathLen;
}
@ -616,7 +599,6 @@ public class WalletApplet extends Applet {
*/
private void resetKeyStatus(boolean toParent) {
signInProgress = false;
expectPublicKey = false;
parentValid = false;
keyPathLen = toParent ? (short) (keyPathLen - 4) : 0;
}
@ -635,7 +617,6 @@ public class WalletApplet extends Applet {
short chainOffset = (short)(privOffset + apduBuffer[(short)(privOffset + 1)] + 2);
if (apduBuffer[pubOffset] != TLV_PUB_KEY) {
secp256k1.assertECPointMultiplicationSupport();
chainOffset = privOffset;
privOffset = pubOffset;
pubOffset = -1;
@ -691,8 +672,6 @@ public class WalletApplet extends Applet {
* @param apduBuffer the APDU buffer
*/
private void loadSeed(byte[] apduBuffer) {
secp256k1.assertECPointMultiplicationSupport();
if (apduBuffer[ISO7816.OFFSET_LC] != BIP39_SEED_SIZE) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
@ -747,37 +726,17 @@ public class WalletApplet extends Applet {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
boolean assistedDerivation = (apduBuffer[ISO7816.OFFSET_P1] & DERIVE_P1_ASSISTED_MASK) == DERIVE_P1_ASSISTED_MASK;
boolean isPublicKey = apduBuffer[ISO7816.OFFSET_P2] == DERIVE_P2_PUBLIC_KEY;
boolean isReset = (apduBuffer[ISO7816.OFFSET_P1] & DERIVE_P1_SOURCE_MASK) == DERIVE_P1_SOURCE_MASTER;
boolean fromParent = (apduBuffer[ISO7816.OFFSET_P1] & DERIVE_P1_SOURCE_MASK) == DERIVE_P1_SOURCE_PARENT;
boolean isReset = apduBuffer[ISO7816.OFFSET_P1] == DERIVE_P1_SOURCE_MASTER;
boolean fromParent = apduBuffer[ISO7816.OFFSET_P1] == DERIVE_P1_SOURCE_PARENT;
if (fromParent && !parentValid) {
ISOException.throwIt(ISO7816.SW_WRONG_P1P2);
}
if ((isPublicKey != (expectPublicKey && !isReset && !fromParent)) || (isPublicKey && !assistedDerivation)) {
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
}
if (isPublicKey) {
JCSystem.beginTransaction();
publicKey.setW(apduBuffer, ISO7816.OFFSET_CDATA, len);
expectPublicKey = false;
keyPathLen += 4;
parentValid = true;
JCSystem.commitTransaction();
return;
}
if (((short) (len % 4) != 0) || (assistedDerivation && (len > 4)) || ((short)(len + (isReset ? 0 : keyPathLen)) > keyPath.length)) {
if (((short) (len % 4) != 0) || ((short)(len + (isReset ? 0 : keyPathLen)) > keyPath.length)) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
if ((len != 0) && !assistedDerivation) {
secp256k1.assertECPointMultiplicationSupport();
}
short chainEnd = (short) (ISO7816.OFFSET_CDATA + len);
if (isReset || fromParent) {
@ -797,41 +756,15 @@ public class WalletApplet extends Applet {
Util.arrayCopy(apduBuffer, i, keyPath, keyPathLen, (short) 4);
if (assistedDerivation) {
expectPublicKey = true;
outputPublicX(apdu, apduBuffer);
} else {
short pubLen = secp256k1.derivePublicKey(privateKey, apduBuffer, chainEnd);
publicKey.setW(apduBuffer, chainEnd, pubLen);
keyPathLen += 4;
parentValid = true;
}
short pubLen = secp256k1.derivePublicKey(privateKey, apduBuffer, chainEnd);
publicKey.setW(apduBuffer, chainEnd, pubLen);
keyPathLen += 4;
parentValid = true;
JCSystem.commitTransaction();
}
}
/**
* Outputs the X of the public key for the current private. Called internally by the deriveKey method and used during
* assisted key derivation.
*
* @param apdu the JCRE-owned APDU object.
* @param apduBuffer the APDU buffer.
*/
private void outputPublicX(APDU apdu, byte[] apduBuffer) {
short xLen = secp256k1.derivePublicX(privateKey, apduBuffer, (short) (SecureChannel.SC_OUT_OFFSET + 4));
signature.init(privateKey, Signature.MODE_SIGN);
short sigLen = signature.signPreComputedHash(ASSISTED_DERIVATION_HASH, (short) 0, (short) ASSISTED_DERIVATION_HASH.length, apduBuffer, (short) (SecureChannel.SC_OUT_OFFSET + xLen + 4));
apduBuffer[SecureChannel.SC_OUT_OFFSET] = TLV_KEY_DERIVATION_TEMPLATE;
apduBuffer[(short) (SecureChannel.SC_OUT_OFFSET + 1)] = (byte) (xLen + sigLen + 2);
apduBuffer[(short) (SecureChannel.SC_OUT_OFFSET + 2)] = TLV_PUB_X;
apduBuffer[(short) (SecureChannel.SC_OUT_OFFSET + 3)] = (byte) xLen;
secureChannel.respond(apdu, (short) (xLen + sigLen + 4), ISO7816.SW_NO_ERROR);
}
/**
* Resets the current key and key path to the parent or master key. A transaction is used to make sure this all
* happens at once. This method is called internally by the deriveKey method.
@ -970,7 +903,6 @@ public class WalletApplet extends Applet {
parentValid = false;
isExtended = false;
signInProgress = false;
expectPublicKey = false;
privateKey.clearKey();
publicKey.clearKey();
masterPrivate.clearKey();
@ -1024,7 +956,7 @@ public class WalletApplet extends Applet {
byte[] apduBuffer = apdu.getBuffer();
short len = secureChannel.preprocessAPDU(apduBuffer);
if (!((pin.isValidated() || isPinless()) && privateKey.isInitialized() && !expectPublicKey)) {
if (!((pin.isValidated() || isPinless()) && privateKey.isInitialized())) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}

View File

@ -127,19 +127,6 @@ public class WalletAppletCommandSet {
return secureChannel.transmit(apduChannel, 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 = resp.getData();
return data[data.length - 1] != 0x00;
}
/**
* Sends a GET STATUS APDU to retrieve the APPLICATION STATUS template and reads the byte indicating key initialization
* status
@ -150,7 +137,7 @@ public class WalletAppletCommandSet {
public boolean getKeyInitializationStatus() throws CardException {
ResponseAPDU resp = getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
byte[] data = resp.getData();
return data[data.length - 4] != 0x00;
return data[data.length - 1] != 0x00;
}
/**
@ -426,34 +413,27 @@ public class WalletAppletCommandSet {
}
/**
* 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.
* Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The P1 is forced to 0, meaning that the derivation
* starts from the master key.
*
* @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, WalletApplet.DERIVE_P1_SOURCE_MASTER, false, false);
return deriveKey(data, WalletApplet.DERIVE_P1_SOURCE_MASTER);
}
/**
* 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.
* Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The source parameter is used as P1
*
* @param data the raw key path or a public key
* @param source the source to start derivation
* @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, int source, boolean assisted, boolean isPublicKey) throws CardException {
byte p1 = assisted ? WalletApplet.DERIVE_P1_ASSISTED_MASK : 0;
p1 |= source;
byte p2 = isPublicKey ? WalletApplet.DERIVE_P2_PUBLIC_KEY : WalletApplet.DERIVE_P2_KEY_PATH;
CommandAPDU deriveKey = secureChannel.protectedCommand(0x80, WalletApplet.INS_DERIVE_KEY, p1, p2, data);
public ResponseAPDU deriveKey(byte[] data, int source) throws CardException {
CommandAPDU deriveKey = secureChannel.protectedCommand(0x80, WalletApplet.INS_DERIVE_KEY, source, 0, data);
return secureChannel.transmit(apduChannel, deriveKey);
}

View File

@ -328,21 +328,21 @@ public class WalletAppletTest {
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
assertEquals(0x9000, response.getSW());
byte[] data = response.getData();
assertTrue(Hex.toHexString(data).matches("a30c0201030201050101[0f][0f]0101[0f][0f]"));
assertTrue(Hex.toHexString(data).matches("a30c0201030201050101[0f][0f]"));
response = cmdSet.verifyPIN("123456");
assertEquals(0x63C2, response.getSW());
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
assertEquals(0x9000, response.getSW());
data = response.getData();
assertTrue(Hex.toHexString(data).matches("a30c0201020201050101[0f][0f]0101[0f][0f]"));
assertTrue(Hex.toHexString(data).matches("a30c0201020201050101[0f][0f]"));
response = cmdSet.verifyPIN("000000");
assertEquals(0x9000, response.getSW());
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
assertEquals(0x9000, response.getSW());
data = response.getData();
assertTrue(Hex.toHexString(data).matches("a30c0201030201050101[0f][0f]0101[0f][0f]"));
assertTrue(Hex.toHexString(data).matches("a30c0201030201050101[0f][0f]"));
// Check that key path is empty
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_KEY_PATH);
@ -550,8 +550,6 @@ public class WalletAppletTest {
cmdSet.autoOpenSecureChannel();
int publicKeyDerivationSW = cmdSet.getPublicKeyDerivationSupport() ? 0x9000 : 0x6a81;
// Security condition violation: PIN not verified
response = cmdSet.loadKey(keyPair);
assertEquals(0x6985, response.getSW());
@ -592,15 +590,15 @@ public class WalletAppletTest {
// Check omitted public key
response = cmdSet.loadKey(keyPair, true, null);
assertEquals(publicKeyDerivationSW, response.getSW());
assertEquals(0x9000, response.getSW());
verifyKeyUID(response.getData(), ((ECPublicKey) keyPair.getPublic()));
response = cmdSet.loadKey(keyPair, true, chainCode);
assertEquals(publicKeyDerivationSW, response.getSW());
assertEquals(0x9000, response.getSW());
verifyKeyUID(response.getData(), ((ECPublicKey) keyPair.getPublic()));
// Check seed load
response = cmdSet.loadKey(keyPair.getPrivate(), chainCode);
assertEquals(publicKeyDerivationSW, response.getSW());
assertEquals(0x9000, response.getSW());
}
@Test
@ -721,7 +719,6 @@ public class WalletAppletTest {
assertEquals(0x6985, response.getSW());
cmdSet.autoOpenSecureChannel();
boolean autonomousDerivation = cmdSet.getPublicKeyDerivationSupport();
// Security condition violation: PIN is not verified
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x00});
@ -744,100 +741,44 @@ public class WalletAppletTest {
response = cmdSet.loadKey(keyPair, false, chainCode);
assertEquals(0x9000, response.getSW());
// Wrong P1/P2
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
assertEquals(0x6A86, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_CURRENT, false, true);
assertEquals(0x6A86, response.getSW());
// Wrong data format
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00});
assertEquals(0x6A80, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x00, 0x00});
assertEquals(0x6A80, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, WalletApplet.DERIVE_P1_SOURCE_MASTER, true, false);
assertEquals(0x6A80, response.getSW());
if (autonomousDerivation) {
// Correct
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x01});
assertEquals(0x9000, response.getSW());
verifyKeyDerivation(keyPair, chainCode, new int[]{1});
// 3 levels with hardened key
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x01, (byte) 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02});
assertEquals(0x9000, response.getSW());
verifyKeyDerivation(keyPair, chainCode, new int[]{1, 0x80000000, 2});
// From parent
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x03}, WalletApplet.DERIVE_P1_SOURCE_PARENT, false, false);
assertEquals(0x9000, response.getSW());
verifyKeyDerivation(keyPair, chainCode, new int[]{1, 0x80000000, 3});
// Reset master key
response = cmdSet.deriveKey(new byte[0]);
assertEquals(0x9000, response.getSW());
verifyKeyDerivation(keyPair, chainCode, new int[0]);
// Try parent when none available
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x03}, WalletApplet.DERIVE_P1_SOURCE_PARENT, false, false);
assertEquals(0x6B00, response.getSW());
// 3 levels with hardened key using separate commands
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_MASTER, false, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(new byte[]{(byte) 0x80, 0x00, 0x00, 0x00}, WalletApplet.DERIVE_P1_SOURCE_CURRENT, false, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x02}, WalletApplet.DERIVE_P1_SOURCE_CURRENT, false, false);
assertEquals(0x9000, response.getSW());
verifyKeyDerivation(keyPair, chainCode, new int[]{1, 0x80000000, 2});
} else {
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x01});
assertEquals(0x6a81, response.getSW());
}
// Assisted derivation
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_MASTER, true, false);
// Correct
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x01});
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
assertEquals(0x9000, response.getSW());
verifyKeyDerivation(keyPair, chainCode, new int[] { 1 });
verifyKeyDerivation(keyPair, chainCode, new int[]{1});
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, false);
// 3 levels with hardened key
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x01, (byte) 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02});
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
assertEquals(0x9000, response.getSW());
verifyKeyDerivation(keyPair, chainCode, new int[] { 1, 2 });
verifyKeyDerivation(keyPair, chainCode, new int[]{1, 0x80000000, 2});
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x03}, WalletApplet.DERIVE_P1_SOURCE_PARENT, true, false);
// From parent
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x03}, WalletApplet.DERIVE_P1_SOURCE_PARENT);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
assertEquals(0x9000, response.getSW());
verifyKeyDerivation(keyPair, chainCode, new int[] { 1, 3 });
// Try to derive two keys at once
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, false);
assertEquals(0x6a86, response.getSW());
verifyKeyDerivation(keyPair, chainCode, new int[]{1, 0x80000000, 3});
// Reset master key
response = cmdSet.deriveKey(new byte[0]);
assertEquals(0x9000, response.getSW());
verifyKeyDerivation(keyPair, chainCode, new int[0]);
// Try to sign and get key path before load public key, then resume loading public key
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, false);
// Try parent when none available
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x03}, WalletApplet.DERIVE_P1_SOURCE_PARENT);
assertEquals(0x6B00, response.getSW());
// 3 levels with hardened key using separate commands
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_MASTER);
assertEquals(0x9000, response.getSW());
byte[] key = derivePublicKey(response.getData());
response = cmdSet.sign(sha256("test".getBytes()), WalletApplet.SIGN_P1_PRECOMPUTED_HASH, true, true);
assertEquals(0x6985, response.getSW());
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_KEY_PATH);
assertEquals(0x6985, response.getSW());
response = cmdSet.deriveKey(key, WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
response = cmdSet.deriveKey(new byte[]{(byte) 0x80, 0x00, 0x00, 0x00}, WalletApplet.DERIVE_P1_SOURCE_CURRENT);
assertEquals(0x9000, response.getSW());
verifyKeyDerivation(keyPair, chainCode, new int[] { 2 });
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x02}, WalletApplet.DERIVE_P1_SOURCE_CURRENT);
assertEquals(0x9000, response.getSW());
verifyKeyDerivation(keyPair, chainCode, new int[]{1, 0x80000000, 2});
// Reset master key
response = cmdSet.deriveKey(new byte[0]);
@ -930,19 +871,11 @@ public class WalletAppletTest {
resetAndSelectAndOpenSC();
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
assertEquals(0x6985, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, WalletApplet.DERIVE_P1_SOURCE_MASTER, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_MASTER);
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
assertEquals(0x6985, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, WalletApplet.DERIVE_P1_SOURCE_CURRENT);
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
assertEquals(0x9000, response.getSW());
@ -956,13 +889,7 @@ public class WalletAppletTest {
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
assertEquals(0x6985, response.getSW());
assertEquals(0x6985, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, WalletApplet.DERIVE_P1_SOURCE_MASTER, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_MASTER);
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
assertEquals(0x9000, response.getSW());
@ -975,7 +902,7 @@ public class WalletAppletTest {
resetAndSelectAndOpenSC();
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
assertEquals(0x6985, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, WalletApplet.DERIVE_P1_SOURCE_MASTER, true, false);
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, WalletApplet.DERIVE_P1_SOURCE_MASTER);
assertEquals(0x6985, response.getSW());
}
@ -1006,27 +933,19 @@ public class WalletAppletTest {
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_HIGH, false);
assertEquals(0x6985, response.getSW());
response = cmdSet.deriveKey(new byte[] { (byte) 0x80, 0x00, 0x00, 0x2c}, WalletApplet.DERIVE_P1_SOURCE_MASTER, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
response = cmdSet.deriveKey(new byte[] { (byte) 0x80, 0x00, 0x00, 0x2c}, WalletApplet.DERIVE_P1_SOURCE_MASTER);
assertEquals(0x9000, response.getSW());
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_HIGH, false);
assertEquals(0x6985, response.getSW());
response = cmdSet.deriveKey(new byte[] { (byte) 0x80, 0x00, 0x00, 0x3c}, WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
response = cmdSet.deriveKey(new byte[] { (byte) 0x80, 0x00, 0x00, 0x3c}, WalletApplet.DERIVE_P1_SOURCE_CURRENT);
assertEquals(0x9000, response.getSW());
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_HIGH, false);
assertEquals(0x6985, response.getSW());
response = cmdSet.deriveKey(new byte[] { (byte) 0x80, 0x00, 0x00, 0x00}, WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
response = cmdSet.deriveKey(new byte[] { (byte) 0x80, 0x00, 0x00, 0x00}, WalletApplet.DERIVE_P1_SOURCE_CURRENT);
assertEquals(0x9000, response.getSW());
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_HIGH, false);
assertEquals(0x6985, response.getSW());
response = cmdSet.deriveKey(new byte[] { (byte) 0x00, 0x00, 0x00, 0x00}, WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
response = cmdSet.deriveKey(new byte[] { (byte) 0x00, 0x00, 0x00, 0x00}, WalletApplet.DERIVE_P1_SOURCE_CURRENT);
assertEquals(0x9000, response.getSW());
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_HIGH, false);
assertEquals(0x6985, response.getSW());
@ -1042,9 +961,7 @@ public class WalletAppletTest {
byte[] keyTemplate = response.getData();
verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 0x8000002c, 0x8000003c, 0x80000000, 0x00000000 }, true);
response = cmdSet.deriveKey(new byte[] {(byte) 0xC0, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
response = cmdSet.deriveKey(new byte[] {(byte) 0xC0, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_CURRENT);
assertEquals(0x9000, response.getSW());
// Wrong P1
@ -1064,7 +981,7 @@ public class WalletAppletTest {
verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 0x8000002c, 0x8000003c, 0x80000000, 0x00000000, 0xC0000001 }, true);
// Reset
response = cmdSet.deriveKey(new byte[] {}, WalletApplet.DERIVE_P1_SOURCE_MASTER, false, false);
response = cmdSet.deriveKey(new byte[] {}, WalletApplet.DERIVE_P1_SOURCE_MASTER);
assertEquals(0x9000, response.getSW());
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_HIGH, false);
assertEquals(0x6985, response.getSW());
@ -1216,151 +1133,9 @@ public class WalletAppletTest {
}
@Test
@DisplayName("Performance Test with assisted key derivation")
@DisplayName("Performance Test")
@Tag("manual")
void performanceAssistedDerivationTest() throws Exception {
long time, overhead = 0, deriveMaster = 0, deriveMasterHardened = 0, deriveCurrentHardened = 0, deriveCurrent = 0, deriveParent = 0, deriveParentHardened = 0, pubKey = 0;
final long SAMPLE_COUNT = 10;
for (int i = 0; i < SAMPLE_COUNT; i++) {
time = System.currentTimeMillis();
ResponseAPDU response = apduChannel.transmit(new CommandAPDU(0x00, 0xAA, 0, 0, 0));
overhead += System.currentTimeMillis() - time;
assertEquals(0x6D00, response.getSW());
}
overhead /= SAMPLE_COUNT;
System.out.println("Measuring key derivation performance. All times are expressed in milliseconds");
System.out.println("***********************************************" );
System.out.println("Java SmartCard I/O overhead: " + overhead);
System.out.println("***********************************************" );
// Prepare the card
cmdSet.autoOpenSecureChannel();
ResponseAPDU response = cmdSet.verifyPIN("000000");
assertEquals(0x9000, response.getSW());
KeyPairGenerator g = keypairGenerator();
KeyPair keyPair = g.generateKeyPair();
byte[] chainCode = new byte[32];
new Random().nextBytes(chainCode);
response = cmdSet.loadKey(keyPair, false, chainCode);
assertEquals(0x9000, response.getSW());
for (int i = 0; i < SAMPLE_COUNT; i++) {
time = System.currentTimeMillis();
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_MASTER, true, false);
deriveMaster += System.currentTimeMillis() - time;
assertEquals(0x9000, response.getSW());
byte[] pub = derivePublicKey(response.getData());
time = System.currentTimeMillis();
response = cmdSet.deriveKey(pub, WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
pubKey += System.currentTimeMillis() - time;
assertEquals(0x9000, response.getSW());
}
deriveMaster /= SAMPLE_COUNT;
pubKey /= SAMPLE_COUNT;
System.out.println("Public key upload: " + pubKey + ". Without overhead: " + (pubKey - overhead));
System.out.println("***********************************************" );
System.out.println("Derivation time (1 level, non-hardened, from master): " + deriveMaster + ". Without overhead: " + (deriveMaster - overhead));
System.out.println("Derivation + pubkey: " + (deriveMaster + pubKey) + ". Without overhead: " + (deriveMaster + pubKey - (overhead * 2)));
System.out.println("***********************************************" );
for (int i = 0; i < SAMPLE_COUNT; i++) {
time = System.currentTimeMillis();
response = cmdSet.deriveKey(new byte[] {(byte) 0x80, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_MASTER, true, false);
deriveMasterHardened += System.currentTimeMillis() - time;
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
assertEquals(0x9000, response.getSW());
}
deriveMasterHardened /= SAMPLE_COUNT;
System.out.println("Derivation time (1 level, hardened, from master): " + deriveMasterHardened + ". Without overhead: " + (deriveMasterHardened - overhead));
System.out.println("Derivation + pubkey: " + (deriveMasterHardened + pubKey) + ". Without overhead: " + (deriveMasterHardened + pubKey - (overhead * 2)));
System.out.println("***********************************************" );
for (int i = 0; i < SAMPLE_COUNT; i++) {
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_MASTER, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
assertEquals(0x9000, response.getSW());
time = System.currentTimeMillis();
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, false);
deriveCurrent += System.currentTimeMillis() - time;
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
assertEquals(0x9000, response.getSW());
}
deriveCurrent /= SAMPLE_COUNT;
System.out.println("Derivation time (1 level, non-hardened, from current): " + deriveCurrent + ". Without overhead: " + (deriveCurrent - overhead));
System.out.println("Derivation + pubkey: " + (deriveCurrent + pubKey) + ". Without overhead: " + (deriveCurrent + pubKey - (overhead * 2)));
System.out.println("***********************************************" );
for (int i = 0; i < SAMPLE_COUNT; i++) {
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_MASTER, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
assertEquals(0x9000, response.getSW());
time = System.currentTimeMillis();
response = cmdSet.deriveKey(new byte[] {(byte) 0x80, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, false);
deriveCurrentHardened += System.currentTimeMillis() - time;
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
assertEquals(0x9000, response.getSW());
}
deriveCurrentHardened /= SAMPLE_COUNT;
System.out.println("Derivation time (1 level, hardened, from current): " + deriveCurrentHardened + ". Without overhead: " + (deriveCurrentHardened - overhead));
System.out.println("Derivation + pubkey: " + (deriveCurrentHardened + pubKey) + ". Without overhead: " + (deriveCurrentHardened + pubKey - (overhead * 2)));
System.out.println("***********************************************" );
for (int i = 0; i < SAMPLE_COUNT; i++) {
time = System.currentTimeMillis();
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, (byte) i}, WalletApplet.DERIVE_P1_SOURCE_PARENT, true, false);
deriveParent += System.currentTimeMillis() - time;
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
assertEquals(0x9000, response.getSW());
}
deriveParent /= SAMPLE_COUNT;
System.out.println("Derivation time (1 level, non-hardened, from parent): " + deriveParent + ". Without overhead: " + (deriveParent - overhead));
System.out.println("Derivation + pubkey: " + (deriveParent + pubKey) + ". Without overhead: " + (deriveParent + pubKey - (overhead * 2)));
System.out.println("***********************************************" );
for (int i = 0; i < SAMPLE_COUNT; i++) {
time = System.currentTimeMillis();
response = cmdSet.deriveKey(new byte[] {(byte) 0x80, 0x00, 0x00, (byte) i}, WalletApplet.DERIVE_P1_SOURCE_PARENT, true, false);
deriveParentHardened += System.currentTimeMillis() - time;
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(response.getData()), WalletApplet.DERIVE_P1_SOURCE_CURRENT, true, true);
assertEquals(0x9000, response.getSW());
}
deriveParentHardened /= SAMPLE_COUNT;
System.out.println("Derivation time (1 level, hardened, from parent): " + deriveParentHardened + ". Without overhead: " + (deriveParentHardened - overhead));
System.out.println("Derivation + pubkey: " + (deriveParentHardened + pubKey) + ". Without overhead: " + (deriveParentHardened + pubKey - (overhead * 2)));
System.out.println("***********************************************" );
System.out.println("Estimated time to derive m/44'/60'/0'/0/0: " + (deriveMasterHardened + (deriveCurrentHardened * 2) + (deriveCurrent * 2) + (pubKey * 5)));
System.out.println("Estimated time to switch m/44'/60'/0'/0/0': " + (deriveParentHardened + pubKey));
System.out.println("Estimated time to switch back to m/44'/60'/0'/0/0: " + (deriveParent + pubKey));
}
@Test
@DisplayName("Performance Test with unassisted key derivation")
@Tag("manual")
void performanceUnassistedDerivationTest() throws Exception {
void performanceTest() throws Exception {
long time, deriveAccount = 0, deriveParent = 0, deriveParentHardened = 0;
final long SAMPLE_COUNT = 10;
@ -1381,7 +1156,7 @@ public class WalletAppletTest {
for (int i = 0; i < SAMPLE_COUNT; i++) {
time = System.currentTimeMillis();
response = cmdSet.deriveKey(new byte[] { (byte) 0x80, 0x00, 0x00, 0x2C, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00}, WalletApplet.DERIVE_P1_SOURCE_MASTER, false, false);
response = cmdSet.deriveKey(new byte[] { (byte) 0x80, 0x00, 0x00, 0x2C, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00}, WalletApplet.DERIVE_P1_SOURCE_MASTER);
deriveAccount += System.currentTimeMillis() - time;
assertEquals(0x9000, response.getSW());
}
@ -1390,7 +1165,7 @@ public class WalletAppletTest {
for (int i = 0; i < SAMPLE_COUNT; i++) {
time = System.currentTimeMillis();
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, (byte) i}, WalletApplet.DERIVE_P1_SOURCE_PARENT, false, false);
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, (byte) i}, WalletApplet.DERIVE_P1_SOURCE_PARENT);
deriveParent += System.currentTimeMillis() - time;
assertEquals(0x9000, response.getSW());
}
@ -1399,7 +1174,7 @@ public class WalletAppletTest {
for (int i = 0; i < SAMPLE_COUNT; i++) {
time = System.currentTimeMillis();
response = cmdSet.deriveKey(new byte[] {(byte) 0x80, 0x00, 0x00, (byte) i}, WalletApplet.DERIVE_P1_SOURCE_PARENT, false, false);
response = cmdSet.deriveKey(new byte[] {(byte) 0x80, 0x00, 0x00, (byte) i}, WalletApplet.DERIVE_P1_SOURCE_PARENT);
deriveParentHardened += System.currentTimeMillis() - time;
assertEquals(0x9000, response.getSW());
}
@ -1555,37 +1330,6 @@ public class WalletAppletTest {
return s.compareTo(limit) >= 1;
}
/**
* 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);
byte[] hash = sha256("STATUS KEY DERIVATION".getBytes());
pubKey[0] = 0x02;
ECKey candidate = ECKey.fromPublicOnly(pubKey);
if (!candidate.verify(hash, signature)) {
pubKey[0] = 0x03;
candidate = ECKey.fromPublicOnly(pubKey);
assertTrue(candidate.verify(hash, signature));
}
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: