remove assisted key derivation
This commit is contained in:
parent
22b41ab781
commit
e78397c198
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue