mirror of
https://github.com/status-im/status-keycard.git
synced 2025-01-13 15:24:53 +00:00
commit
98ffcda1b8
@ -6,10 +6,12 @@ Version numbers are in the form major.minor. An major revision increment indicat
|
|||||||
compared to the previous released version. This is version 2.0 of the specs (unreleased).
|
compared to the previous released version. This is version 2.0 of the specs (unreleased).
|
||||||
|
|
||||||
### Changes since 1.2
|
### Changes since 1.2
|
||||||
|
* **BREAKING** Completely redefined the EXPORT KEY command
|
||||||
* **BREAKING** Removed assisted key derivation
|
* **BREAKING** Removed assisted key derivation
|
||||||
* **BREAKING** Removed plain data signing, now only 32-byte long hashes can be signed
|
* **BREAKING** Removed plain data signing, now only 32-byte long hashes can be signed
|
||||||
* Added internal key generation (GENERATE KEY)
|
* Added internal key generation (GENERATE KEY)
|
||||||
* Added the ability to customize the NDEF response (SET NDEF)
|
* Added the ability to customize the NDEF response (SET NDEF)
|
||||||
|
* Added DUPLICATE KEY command
|
||||||
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
@ -260,8 +262,7 @@ signing sessions, if any. Unless a DERIVE KEY is sent, a subsequent SIGN command
|
|||||||
* INS = 0xD1
|
* INS = 0xD1
|
||||||
* P1 = derivation options
|
* P1 = derivation options
|
||||||
* P2 = 0x00
|
* P2 = 0x00
|
||||||
* Data = a sequence of 32-bit integers (most significant byte first). Empty if the master key must be used. On assisted
|
* Data = a sequence of 32-bit integers (most significant byte first). Empty if the master key must be used.
|
||||||
derivation contains a public key when P2 = 0x02.
|
|
||||||
* Response SW = 0x9000 on success, 0x6A80 if the format is invalid, 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.
|
generates an invalid key, 0x6B00 if derivation from parent keys is selected but no valid parent key is cached.
|
||||||
* Preconditions: Secure Channel must be opened, user PIN must be verified (if no PIN-less key is defined), an extended
|
* Preconditions: Secure Channel must be opened, user PIN must be verified (if no PIN-less key is defined), an extended
|
||||||
@ -410,16 +411,18 @@ will work even if no PIN authentication has been performed. An empty sequence me
|
|||||||
|
|
||||||
* CLA = 0x80
|
* CLA = 0x80
|
||||||
* INS = 0xC2
|
* INS = 0xC2
|
||||||
* P1 = key path index
|
* P1 = derivation options
|
||||||
* P2 = export options
|
* P2 = export options
|
||||||
* Response SW = 0x9000 on success, 0x6A86 if P1 or P2 are wrong
|
* Response SW = 0x9000 on success, 0x6A86 if P1 or P2 are wrong
|
||||||
|
* Data = a sequence of 32-bit integers (empty if P1=0x00)
|
||||||
* Response Data = key pair template
|
* Response Data = key pair template
|
||||||
* Preconditions: Secure Channel must be opened, user PIN must be verified, the current key path must match the one of
|
* Response SW = 0x9000 on success, 0x6985 if the private key cannot be exported, 0x6A80 if the path is malformed
|
||||||
the key selected through P1
|
* Preconditions: Secure Channel must be opened, user PIN must be verified
|
||||||
|
|
||||||
P1:
|
P1:
|
||||||
0x00 = Any key
|
0x00 = Current key
|
||||||
0x01 = Non-wallet key
|
0x01 = Derive
|
||||||
|
0x02 = Derive and make current
|
||||||
|
|
||||||
P2:
|
P2:
|
||||||
0x00 = private and public key
|
0x00 = private and public key
|
||||||
@ -427,13 +430,16 @@ P2:
|
|||||||
|
|
||||||
Response Data format:
|
Response Data format:
|
||||||
- Tag 0xA1 = keypair template
|
- Tag 0xA1 = keypair template
|
||||||
- Tag 0x80 = ECC public key component
|
- Tag 0x80 = ECC public key component (could be omitted)
|
||||||
- Tag 0x81 = ECC private key component (if P2=0x00)
|
- Tag 0x81 = ECC private key component (if P2=0x00)
|
||||||
|
|
||||||
This command exports the current public and private key if and only if the current key path matches the conditions
|
This command exports the requested public and private key. The public key can be always exported (P2=0x01), but the
|
||||||
dictated by the given P1 parameter. This currently allows exporting any key whose last path component has a key index
|
private key (P2=0x00) can be exported if and only if the requested key path is in the
|
||||||
with the two most significant bits set (that is, matches the 0xc0000000 mask). No key in this range should be used for
|
[EIP-1581](https://eips.ethereum.org/EIPS/eip-1581) subtree.
|
||||||
wallet accounts, even as intermediate path component. These keys are meant for client-specific use such as the Whisper key.
|
|
||||||
|
|
||||||
The special index 0x00 indicates any path, which means the current key will always be exported regardless of its actual
|
The P1 parameter indicates how to the derive the desired key. P1 = 0x00 indicates that the current key must be exported,
|
||||||
path. This works however only in combination with P2=0x01, so only the public key will be exported.
|
and no derivation will be performed. P1 = 0x01 derives the path given in the data field without changing the current
|
||||||
|
path of the card. P1 = 0x02 derives the path but also changes the current path of the card.
|
||||||
|
|
||||||
|
If the private key is being exported, the card could omit exporting the public key for performance reason. The public
|
||||||
|
key can then be calculate off-card if needed.
|
@ -42,7 +42,7 @@ dependencies {
|
|||||||
testCompile('org.web3j:core:2.3.1')
|
testCompile('org.web3j:core:2.3.1')
|
||||||
testCompile('org.bitcoinj:bitcoinj-core:0.14.5')
|
testCompile('org.bitcoinj:bitcoinj-core:0.14.5')
|
||||||
testCompile("org.bouncycastle:bcprov-jdk15on:1.58")
|
testCompile("org.bouncycastle:bcprov-jdk15on:1.58")
|
||||||
testCompile("com.github.status-im:hardwallet-lite-sdk:f64cefd")
|
testCompile("com.github.status-im:hardwallet-lite-sdk:75d9f54")
|
||||||
testCompile("org.junit.jupiter:junit-jupiter-api:5.1.1")
|
testCompile("org.junit.jupiter:junit-jupiter-api:5.1.1")
|
||||||
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.1.1")
|
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.1.1")
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,9 @@ import javacardx.crypto.Cipher;
|
|||||||
public class Crypto {
|
public class Crypto {
|
||||||
final static public short AES_BLOCK_SIZE = 16;
|
final static public short AES_BLOCK_SIZE = 16;
|
||||||
|
|
||||||
final static private short KEY_SECRET_SIZE = 32;
|
final static short KEY_SECRET_SIZE = 32;
|
||||||
final static private short KEY_DERIVATION_INPUT_SIZE = 37;
|
final static short KEY_PUB_SIZE = 65;
|
||||||
|
final static short KEY_DERIVATION_SCRATCH_SIZE = 37;
|
||||||
final static private short HMAC_OUT_SIZE = 64;
|
final static private short HMAC_OUT_SIZE = 64;
|
||||||
|
|
||||||
final static private byte[] MAX_S = { (byte) 0x7F, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x5D, (byte) 0x57, (byte) 0x6E, (byte) 0x73, (byte) 0x57, (byte) 0xA4, (byte) 0x50, (byte) 0x1D, (byte) 0xDF, (byte) 0xE9, (byte) 0x2F, (byte) 0x46, (byte) 0x68, (byte) 0x1B, (byte) 0x20, (byte) 0xA0 };
|
final static private byte[] MAX_S = { (byte) 0x7F, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x5D, (byte) 0x57, (byte) 0x6E, (byte) 0x73, (byte) 0x57, (byte) 0xA4, (byte) 0x50, (byte) 0x1D, (byte) 0xDF, (byte) 0xE9, (byte) 0x2F, (byte) 0x46, (byte) 0x68, (byte) 0x1B, (byte) 0x20, (byte) 0xA0 };
|
||||||
@ -22,7 +23,6 @@ public class Crypto {
|
|||||||
final static private byte HMAC_IPAD = (byte) 0x36;
|
final static private byte HMAC_IPAD = (byte) 0x36;
|
||||||
final static private byte HMAC_OPAD = (byte) 0x5c;
|
final static private byte HMAC_OPAD = (byte) 0x5c;
|
||||||
final static private short HMAC_BLOCK_SIZE = (short) 128;
|
final static private short HMAC_BLOCK_SIZE = (short) 128;
|
||||||
final static private short HMAC_BLOCK_OFFSET = (short) KEY_DERIVATION_INPUT_SIZE + HMAC_OUT_SIZE;
|
|
||||||
|
|
||||||
final static private byte[] KEY_BITCOIN_SEED = {'B', 'i', 't', 'c', 'o', 'i', 'n', ' ', 's', 'e', 'e', 'd'};
|
final static private byte[] KEY_BITCOIN_SEED = {'B', 'i', 't', 'c', 'o', 'i', 'n', ' ', 's', 'e', 'e', 'd'};
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ public class Crypto {
|
|||||||
|
|
||||||
private AESKey tmpAES256;
|
private AESKey tmpAES256;
|
||||||
|
|
||||||
private byte[] tmp;
|
private byte[] hmacBlock;
|
||||||
|
|
||||||
Crypto() {
|
Crypto() {
|
||||||
random = RandomData.getInstance(RandomData.ALG_SECURE_RANDOM);
|
random = RandomData.getInstance(RandomData.ALG_SECURE_RANDOM);
|
||||||
@ -49,18 +49,14 @@ public class Crypto {
|
|||||||
|
|
||||||
tmpAES256 = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES_TRANSIENT_DESELECT, KeyBuilder.LENGTH_AES_256, false);
|
tmpAES256 = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES_TRANSIENT_DESELECT, KeyBuilder.LENGTH_AES_256, false);
|
||||||
|
|
||||||
short blockSize;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
blockSize = 0;
|
|
||||||
hmacSHA512 = Signature.getInstance(Signature.ALG_HMAC_SHA_512, false);
|
hmacSHA512 = Signature.getInstance(Signature.ALG_HMAC_SHA_512, false);
|
||||||
hmacKey = (HMACKey) KeyBuilder.buildKey(KeyBuilder.TYPE_HMAC_TRANSIENT_DESELECT, KEY_SECRET_SIZE, false);
|
hmacKey = (HMACKey) KeyBuilder.buildKey(KeyBuilder.TYPE_HMAC_TRANSIENT_DESELECT, KEY_SECRET_SIZE, false);
|
||||||
} catch (CryptoException e) {
|
} catch (CryptoException e) {
|
||||||
hmacSHA512 = null;
|
hmacSHA512 = null;
|
||||||
blockSize = HMAC_BLOCK_SIZE;
|
hmacBlock = JCSystem.makeTransientByteArray(HMAC_BLOCK_SIZE, JCSystem.CLEAR_ON_RESET);
|
||||||
}
|
}
|
||||||
|
|
||||||
tmp = JCSystem.makeTransientByteArray((short) (HMAC_BLOCK_OFFSET + blockSize), JCSystem.CLEAR_ON_RESET);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public short oneShotAES(byte mode, byte[] src, short sOff, short sLen, byte[] dst, short dOff, byte[] key, short keyOff) {
|
public short oneShotAES(byte mode, byte[] src, short sOff, short sLen, byte[] dst, short dOff, byte[] key, short keyOff) {
|
||||||
@ -69,6 +65,10 @@ public class Crypto {
|
|||||||
return aesCbcIso9797m2.doFinal(src, (short) (sOff + AES_BLOCK_SIZE), sLen, dst, dOff);
|
return aesCbcIso9797m2.doFinal(src, (short) (sOff + AES_BLOCK_SIZE), sLen, dst, dOff);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean bip32IsHardened(byte[] i, short iOff) {
|
||||||
|
return (i[iOff] & (byte) 0x80) == (byte) 0x80;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derives a private key according to the algorithm defined in BIP32. The BIP32 specifications define some checks
|
* Derives a private key according to the algorithm defined in BIP32. The BIP32 specifications define some checks
|
||||||
* to be performed on the derived keys. In the very unlikely event that these checks fail this key is not considered
|
* to be performed on the derived keys. In the very unlikely event that these checks fail this key is not considered
|
||||||
@ -76,43 +76,33 @@ public class Crypto {
|
|||||||
*
|
*
|
||||||
* @param i the buffer containing the key path element (a 32-bit big endian integer)
|
* @param i the buffer containing the key path element (a 32-bit big endian integer)
|
||||||
* @param iOff the offset in the buffer
|
* @param iOff the offset in the buffer
|
||||||
* @param privateKey the parent private key
|
|
||||||
* @param publicKey the parent public key
|
|
||||||
* @param chain the chain code
|
|
||||||
* @param chainOff the offset in the chain code buffer
|
|
||||||
* @return true if successful, false otherwise
|
* @return true if successful, false otherwise
|
||||||
*/
|
*/
|
||||||
boolean bip32CKDPriv(byte[] i, short iOff, ECPrivateKey privateKey, ECPublicKey publicKey, byte[] chain, short chainOff) {
|
boolean bip32CKDPriv(byte[] i, short iOff, byte[] scratch, short scratchOff, byte[] data, short dataOff, byte[] output, short outOff) {
|
||||||
short off = 0;
|
short off = scratchOff;
|
||||||
|
|
||||||
if ((i[iOff] & (byte) 0x80) == (byte) 0x80) {
|
if (bip32IsHardened(i, iOff)) {
|
||||||
tmp[off++] = 0;
|
scratch[off++] = 0;
|
||||||
off += privateKey.getS(tmp, off);
|
off = Util.arrayCopyNonAtomic(data, dataOff, scratch, off, KEY_SECRET_SIZE);
|
||||||
} else {
|
} else {
|
||||||
off = (short) (publicKey.getW(tmp, (short) 0) - 1);
|
scratch[off++] = ((data[(short) (dataOff + KEY_SECRET_SIZE + KEY_SECRET_SIZE + KEY_PUB_SIZE - 1)] & 1) != 0 ? (byte) 0x03 : (byte) 0x02);
|
||||||
tmp[0] = ((tmp[off] & 1) != 0 ? (byte) 0x03 : (byte) 0x02);
|
off = Util.arrayCopyNonAtomic(data, (short) (dataOff + KEY_SECRET_SIZE + KEY_SECRET_SIZE + 1), scratch, off, KEY_SECRET_SIZE);
|
||||||
off = (short) ((short) (off / 2) + 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
off = Util.arrayCopyNonAtomic(i, iOff, tmp, off, (short) 4);
|
off = Util.arrayCopyNonAtomic(i, iOff, scratch, off, (short) 4);
|
||||||
|
|
||||||
hmacSHA512(chain, chainOff, KEY_SECRET_SIZE, tmp, (short) 0, off, tmp, off);
|
hmacSHA512(data, (short)(dataOff + KEY_SECRET_SIZE), KEY_SECRET_SIZE, scratch, scratchOff, (short)(off - scratchOff), output, outOff);
|
||||||
|
|
||||||
if (ucmp256(tmp, off, SECP256k1.SECP256K1_R, (short) 0) >= 0) {
|
if (ucmp256(output, outOff, SECP256k1.SECP256K1_R, (short) 0) >= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
privateKey.getS(tmp, (short) 0);
|
addm256(output, outOff, data, dataOff, SECP256k1.SECP256K1_R, (short) 0, output, outOff);
|
||||||
|
|
||||||
addm256(tmp, off, tmp, (short) 0, SECP256k1.SECP256K1_R, (short) 0, tmp, off);
|
if (isZero256(output, outOff)) {
|
||||||
|
|
||||||
if (isZero256(tmp, off)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
privateKey.setS(tmp, off, (short) KEY_SECRET_SIZE);
|
|
||||||
Util.arrayCopy(tmp, (short)(off + KEY_SECRET_SIZE), chain, chainOff, (short) KEY_SECRET_SIZE);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,13 +167,13 @@ public class Crypto {
|
|||||||
hmacSHA512.sign(in, inOff, inLen, out, outOff);
|
hmacSHA512.sign(in, inOff, inLen, out, outOff);
|
||||||
} else {
|
} else {
|
||||||
for (byte i = 0; i < 2; i++) {
|
for (byte i = 0; i < 2; i++) {
|
||||||
Util.arrayFillNonAtomic(tmp, HMAC_BLOCK_OFFSET, HMAC_BLOCK_SIZE, (i == 0 ? HMAC_IPAD : HMAC_OPAD));
|
Util.arrayFillNonAtomic(hmacBlock, (short) 0, HMAC_BLOCK_SIZE, (i == 0 ? HMAC_IPAD : HMAC_OPAD));
|
||||||
|
|
||||||
for (short j = 0; j < keyLen; j++) {
|
for (short j = 0; j < keyLen; j++) {
|
||||||
tmp[(short)(HMAC_BLOCK_OFFSET + j)] ^= key[(short)(keyOff + j)];
|
hmacBlock[j] ^= key[(short)(keyOff + j)];
|
||||||
}
|
}
|
||||||
|
|
||||||
sha512.update(tmp, HMAC_BLOCK_OFFSET, HMAC_BLOCK_SIZE);
|
sha512.update(hmacBlock, (short) 0, HMAC_BLOCK_SIZE);
|
||||||
|
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
sha512.doFinal(in, inOff, inLen, out, outOff);
|
sha512.doFinal(in, inOff, inLen, out, outOff);
|
||||||
|
@ -3,6 +3,7 @@ package im.status.wallet;
|
|||||||
import javacard.security.ECKey;
|
import javacard.security.ECKey;
|
||||||
import javacard.security.ECPrivateKey;
|
import javacard.security.ECPrivateKey;
|
||||||
import javacard.security.KeyAgreement;
|
import javacard.security.KeyAgreement;
|
||||||
|
import javacard.security.KeyBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility methods to work with the SECP256k1 curve. This class is not meant to be instantiated, but its init method
|
* Utility methods to work with the SECP256k1 curve. This class is not meant to be instantiated, but its init method
|
||||||
@ -47,17 +48,23 @@ public class SECP256k1 {
|
|||||||
|
|
||||||
static final byte SECP256K1_K = (byte)0x01;
|
static final byte SECP256K1_K = (byte)0x01;
|
||||||
|
|
||||||
|
static final short SECP256K1_KEY_SIZE = 256;
|
||||||
|
|
||||||
private static final byte ALG_EC_SVDP_DH_PLAIN_XY = 6; // constant from JavaCard 3.0.5
|
private static final byte ALG_EC_SVDP_DH_PLAIN_XY = 6; // constant from JavaCard 3.0.5
|
||||||
|
|
||||||
|
|
||||||
private KeyAgreement ecPointMultiplier;
|
private KeyAgreement ecPointMultiplier;
|
||||||
private Crypto crypto;
|
private Crypto crypto;
|
||||||
|
private ECPrivateKey tmpECPrivateKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allocates objects needed by this class. Must be invoked during the applet installation exactly 1 time.
|
* Allocates objects needed by this class. Must be invoked during the applet installation exactly 1 time.
|
||||||
*/
|
*/
|
||||||
SECP256k1(Crypto crypto) {
|
SECP256k1(Crypto crypto) {
|
||||||
this.crypto = crypto;
|
this.crypto = crypto;
|
||||||
ecPointMultiplier = KeyAgreement.getInstance(ALG_EC_SVDP_DH_PLAIN_XY, false);
|
this.ecPointMultiplier = KeyAgreement.getInstance(ALG_EC_SVDP_DH_PLAIN_XY, false);
|
||||||
|
this.tmpECPrivateKey = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, SECP256K1_KEY_SIZE, false);
|
||||||
|
setCurveParameters(tmpECPrivateKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -87,6 +94,21 @@ public class SECP256k1 {
|
|||||||
return multiplyPoint(privateKey, SECP256K1_G, (short) 0, (short) SECP256K1_G.length, pubOut, pubOff);
|
return multiplyPoint(privateKey, SECP256K1_G, (short) 0, (short) SECP256K1_G.length, pubOut, pubOff);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives the public key from the given private key and outputs it in the pubOut buffer. This is done by multiplying
|
||||||
|
* the private key by the G point of the curve.
|
||||||
|
*
|
||||||
|
* @param privateKey the private key
|
||||||
|
* @param pubOut the output buffer for the public key
|
||||||
|
* @param pubOff the offset in pubOut
|
||||||
|
* @return the length of the public key
|
||||||
|
*/
|
||||||
|
short derivePublicKey(byte[] privateKey, short privOff, byte[] pubOut, short pubOff) {
|
||||||
|
tmpECPrivateKey.setS(privateKey, privOff, (short)(SECP256K1_KEY_SIZE/8));
|
||||||
|
return derivePublicKey(tmpECPrivateKey, pubOut, pubOff);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Multiplies a scalar in the form of a private key by the given point. Internally uses a special version of EC-DH
|
* 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.
|
* supported since JavaCard 3.0.5 which outputs both X and Y in their uncompressed form.
|
||||||
|
@ -34,7 +34,6 @@ public class WalletApplet extends Applet {
|
|||||||
static final byte PAIRING_MAX_CLIENT_COUNT = 5;
|
static final byte PAIRING_MAX_CLIENT_COUNT = 5;
|
||||||
static final byte UID_LENGTH = 16;
|
static final byte UID_LENGTH = 16;
|
||||||
|
|
||||||
static final short EC_KEY_SIZE = 256;
|
|
||||||
static final short CHAIN_CODE_SIZE = 32;
|
static final short CHAIN_CODE_SIZE = 32;
|
||||||
static final short KEY_UID_LENGTH = 32;
|
static final short KEY_UID_LENGTH = 32;
|
||||||
static final short BIP39_SEED_SIZE = CHAIN_CODE_SIZE * 2;
|
static final short BIP39_SEED_SIZE = CHAIN_CODE_SIZE * 2;
|
||||||
@ -63,8 +62,9 @@ public class WalletApplet extends Applet {
|
|||||||
static final byte DUPLICATE_KEY_P1_EXPORT = 0x02;
|
static final byte DUPLICATE_KEY_P1_EXPORT = 0x02;
|
||||||
static final byte DUPLICATE_KEY_P1_IMPORT = 0x03;
|
static final byte DUPLICATE_KEY_P1_IMPORT = 0x03;
|
||||||
|
|
||||||
static final byte EXPORT_KEY_P1_ANY = 0x00;
|
static final byte EXPORT_KEY_P1_CURRENT = 0x00;
|
||||||
static final byte EXPORT_KEY_P1_HIGH = 0x01;
|
static final byte EXPORT_KEY_P1_DERIVE = 0x01;
|
||||||
|
static final byte EXPORT_KEY_P1_DERIVE_AND_MAKE_CURRENT = 0x02;
|
||||||
|
|
||||||
static final byte EXPORT_KEY_P2_PRIVATE_AND_PUBLIC = 0x00;
|
static final byte EXPORT_KEY_P2_PRIVATE_AND_PUBLIC = 0x00;
|
||||||
static final byte EXPORT_KEY_P2_PUBLIC_ONLY = 0x01;
|
static final byte EXPORT_KEY_P2_PUBLIC_ONLY = 0x01;
|
||||||
@ -84,7 +84,7 @@ public class WalletApplet extends Applet {
|
|||||||
static final byte TLV_UID = (byte) 0x8F;
|
static final byte TLV_UID = (byte) 0x8F;
|
||||||
static final byte TLV_KEY_UID = (byte) 0x8E;
|
static final byte TLV_KEY_UID = (byte) 0x8E;
|
||||||
|
|
||||||
private static final byte EXPORT_KEY_HIGH_MASK = (byte) 0xc0;
|
static final byte[] EIP_1581_PREFIX = { (byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2D};
|
||||||
|
|
||||||
private OwnerPIN pin;
|
private OwnerPIN pin;
|
||||||
private OwnerPIN puk;
|
private OwnerPIN puk;
|
||||||
@ -99,7 +99,6 @@ public class WalletApplet extends Applet {
|
|||||||
private ECPublicKey parentPublicKey;
|
private ECPublicKey parentPublicKey;
|
||||||
private ECPrivateKey parentPrivateKey;
|
private ECPrivateKey parentPrivateKey;
|
||||||
private byte[] parentChainCode;
|
private byte[] parentChainCode;
|
||||||
private boolean parentValid;
|
|
||||||
|
|
||||||
private ECPublicKey publicKey;
|
private ECPublicKey publicKey;
|
||||||
private ECPrivateKey privateKey;
|
private ECPrivateKey privateKey;
|
||||||
@ -121,6 +120,8 @@ public class WalletApplet extends Applet {
|
|||||||
private byte[] duplicationEncKey;
|
private byte[] duplicationEncKey;
|
||||||
private short expectedEntropy;
|
private short expectedEntropy;
|
||||||
|
|
||||||
|
private byte[] derivationOutput;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoked during applet installation. Creates an instance of this class. The installation parameters are passed in
|
* Invoked during applet installation. Creates an instance of this class. The installation parameters are passed in
|
||||||
* the given buffer.
|
* the given buffer.
|
||||||
@ -151,14 +152,14 @@ public class WalletApplet extends Applet {
|
|||||||
uid = new byte[UID_LENGTH];
|
uid = new byte[UID_LENGTH];
|
||||||
crypto.random.generateData(uid, (short) 0, UID_LENGTH);
|
crypto.random.generateData(uid, (short) 0, UID_LENGTH);
|
||||||
|
|
||||||
masterPublic = (ECPublicKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PUBLIC, EC_KEY_SIZE, false);
|
masterPublic = (ECPublicKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PUBLIC, SECP256k1.SECP256K1_KEY_SIZE, false);
|
||||||
masterPrivate = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, EC_KEY_SIZE, false);
|
masterPrivate = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, SECP256k1.SECP256K1_KEY_SIZE, false);
|
||||||
|
|
||||||
parentPublicKey = (ECPublicKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PUBLIC, EC_KEY_SIZE, false);
|
parentPublicKey = (ECPublicKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PUBLIC, SECP256k1.SECP256K1_KEY_SIZE, false);
|
||||||
parentPrivateKey = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, EC_KEY_SIZE, false);
|
parentPrivateKey = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, SECP256k1.SECP256K1_KEY_SIZE, false);
|
||||||
|
|
||||||
publicKey = (ECPublicKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PUBLIC, EC_KEY_SIZE, false);
|
publicKey = (ECPublicKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PUBLIC, SECP256k1.SECP256K1_KEY_SIZE, false);
|
||||||
privateKey = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, EC_KEY_SIZE, false);
|
privateKey = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, SECP256k1.SECP256K1_KEY_SIZE, false);
|
||||||
|
|
||||||
masterChainCode = new byte[CHAIN_CODE_SIZE];
|
masterChainCode = new byte[CHAIN_CODE_SIZE];
|
||||||
parentChainCode = new byte[CHAIN_CODE_SIZE];
|
parentChainCode = new byte[CHAIN_CODE_SIZE];
|
||||||
@ -176,6 +177,8 @@ public class WalletApplet extends Applet {
|
|||||||
duplicationEncKey = new byte[(short)(KeyBuilder.LENGTH_AES_256/8)];
|
duplicationEncKey = new byte[(short)(KeyBuilder.LENGTH_AES_256/8)];
|
||||||
expectedEntropy = -1;
|
expectedEntropy = -1;
|
||||||
|
|
||||||
|
derivationOutput = JCSystem.makeTransientByteArray((short) (Crypto.KEY_SECRET_SIZE + CHAIN_CODE_SIZE), JCSystem.CLEAR_ON_RESET);
|
||||||
|
|
||||||
register(bArray, (short) (bOffset + 1), bArray[bOffset]);
|
register(bArray, (short) (bOffset + 1), bArray[bOffset]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,11 +266,11 @@ public class WalletApplet extends Applet {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch(ISOException sw) {
|
} catch(ISOException sw) {
|
||||||
if (shouldRespond(apdu) && (sw.getReason() != ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED)) {
|
handleException(apdu, sw.getReason());
|
||||||
secureChannel.respond(apdu, (short) 0, sw.getReason());
|
} catch (CryptoException ce) {
|
||||||
} else {
|
handleException(apdu, (short)(ISO7816.SW_UNKNOWN | ce.getReason()));
|
||||||
throw sw;
|
} catch (Exception e) {
|
||||||
}
|
handleException(apdu, ISO7816.SW_UNKNOWN);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldRespond(apdu)) {
|
if (shouldRespond(apdu)) {
|
||||||
@ -275,6 +278,14 @@ public class WalletApplet extends Applet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleException(APDU apdu, short sw) {
|
||||||
|
if (shouldRespond(apdu) && (sw != ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED)) {
|
||||||
|
secureChannel.respond(apdu, (short) 0, sw);
|
||||||
|
} else {
|
||||||
|
ISOException.throwIt(sw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes the init command, this is invoked only if the applet has not yet been personalized with secrets.
|
* Processes the init command, this is invoked only if the applet has not yet been personalized with secrets.
|
||||||
*
|
*
|
||||||
@ -627,9 +638,10 @@ public class WalletApplet extends Applet {
|
|||||||
* Resets the status of the keys. This method must be called immediately before committing the transaction where key
|
* Resets the status of the keys. This method must be called immediately before committing the transaction where key
|
||||||
* manipulation has happened to be sure that the state is always consistent.
|
* manipulation has happened to be sure that the state is always consistent.
|
||||||
*/
|
*/
|
||||||
private void resetKeyStatus(boolean toParent) {
|
private void resetKeyStatus() {
|
||||||
parentValid = false;
|
parentPrivateKey.clearKey();
|
||||||
keyPathLen = toParent ? (short) (keyPathLen - 4) : 0;
|
secp256k1.setCurveParameters(parentPrivateKey);
|
||||||
|
keyPathLen = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -686,7 +698,7 @@ public class WalletApplet extends Applet {
|
|||||||
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
||||||
}
|
}
|
||||||
|
|
||||||
resetKeyStatus(false);
|
resetKeyStatus();
|
||||||
JCSystem.commitTransaction();
|
JCSystem.commitTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -718,7 +730,7 @@ public class WalletApplet extends Applet {
|
|||||||
masterPublic.setW(apduBuffer, (short) 0, pubLen);
|
masterPublic.setW(apduBuffer, (short) 0, pubLen);
|
||||||
publicKey.setW(apduBuffer, (short) 0, pubLen);
|
publicKey.setW(apduBuffer, (short) 0, pubLen);
|
||||||
|
|
||||||
resetKeyStatus(false);
|
resetKeyStatus();
|
||||||
JCSystem.commitTransaction();
|
JCSystem.commitTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -749,85 +761,143 @@ public class WalletApplet extends Applet {
|
|||||||
byte[] apduBuffer = apdu.getBuffer();
|
byte[] apduBuffer = apdu.getBuffer();
|
||||||
short len = secureChannel.preprocessAPDU(apduBuffer);
|
short len = secureChannel.preprocessAPDU(apduBuffer);
|
||||||
|
|
||||||
if (!((pin.isValidated() || (pinlessPathLen > 0)) && isExtended)) {
|
if (!((pin.isValidated() || (pinlessPathLen > 0)))) {
|
||||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isReset = apduBuffer[ISO7816.OFFSET_P1] == DERIVE_P1_SOURCE_MASTER;
|
doDerive(apduBuffer, len, apduBuffer[ISO7816.OFFSET_P1], true);
|
||||||
boolean fromParent = apduBuffer[ISO7816.OFFSET_P1] == DERIVE_P1_SOURCE_PARENT;
|
}
|
||||||
|
|
||||||
if (fromParent && !parentValid) {
|
/**
|
||||||
ISOException.throwIt(ISO7816.SW_WRONG_P1P2);
|
* Internal derivation function, called by DERIVE KEY and EXPORT KEY
|
||||||
|
* @param apduBuffer the APDU buffer
|
||||||
|
* @param len the APDU len
|
||||||
|
* @param source derivation source
|
||||||
|
* @param makeCurrent whether the results should be saved or not
|
||||||
|
*/
|
||||||
|
private void doDerive(byte[] apduBuffer, short len, byte source, boolean makeCurrent) {
|
||||||
|
if (!isExtended) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (((short) (len % 4) != 0) || ((short)(len + (isReset ? 0 : keyPathLen)) > keyPath.length)) {
|
short newPathLen;
|
||||||
|
short pathLenOff;
|
||||||
|
ECPublicKey sourcePub;
|
||||||
|
ECPrivateKey sourcePriv;
|
||||||
|
byte[] sourceChain;
|
||||||
|
|
||||||
|
switch (source) {
|
||||||
|
case DERIVE_P1_SOURCE_MASTER:
|
||||||
|
if (len == 0) {
|
||||||
|
resetToMaster(apduBuffer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newPathLen = len;
|
||||||
|
sourcePriv = masterPrivate;
|
||||||
|
sourcePub = masterPublic;
|
||||||
|
sourceChain = masterChainCode;
|
||||||
|
pathLenOff = 0;
|
||||||
|
break;
|
||||||
|
case DERIVE_P1_SOURCE_PARENT:
|
||||||
|
if (!parentPrivateKey.isInitialized()) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_WRONG_P1P2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len == 0) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
newPathLen = (short) (keyPathLen + len - 4);
|
||||||
|
sourcePriv = parentPrivateKey;
|
||||||
|
sourcePub = parentPublicKey;
|
||||||
|
sourceChain = parentChainCode;
|
||||||
|
pathLenOff = (short) (keyPathLen - 4);
|
||||||
|
break;
|
||||||
|
case DERIVE_P1_SOURCE_CURRENT:
|
||||||
|
if (len == 0) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
newPathLen = (short) (keyPathLen + len);
|
||||||
|
sourcePriv = privateKey;
|
||||||
|
sourcePub = publicKey;
|
||||||
|
sourceChain = chainCode;
|
||||||
|
pathLenOff = keyPathLen;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (((short) (len % 4) != 0) || (newPathLen > keyPath.length)) {
|
||||||
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
||||||
}
|
}
|
||||||
|
|
||||||
short chainEnd = (short) (ISO7816.OFFSET_CDATA + len);
|
short scratchOff = (short) (ISO7816.OFFSET_CDATA + len);
|
||||||
|
short dataOff = (short) (scratchOff + Crypto.KEY_DERIVATION_SCRATCH_SIZE);
|
||||||
|
|
||||||
if (isReset || fromParent) {
|
short pubKeyOff = (short) (dataOff + sourcePriv.getS(apduBuffer, dataOff));
|
||||||
resetKeys(fromParent, apduBuffer, chainEnd);
|
pubKeyOff = Util.arrayCopyNonAtomic(sourceChain, (short) 0, apduBuffer, pubKeyOff, CHAIN_CODE_SIZE);
|
||||||
|
|
||||||
|
if (!crypto.bip32IsHardened(apduBuffer, ISO7816.OFFSET_CDATA)) {
|
||||||
|
sourcePub.getW(apduBuffer, pubKeyOff);
|
||||||
|
} else {
|
||||||
|
apduBuffer[pubKeyOff] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (short i = ISO7816.OFFSET_CDATA; i < chainEnd; i += 4) {
|
for (short i = ISO7816.OFFSET_CDATA; i < scratchOff; i += 4) {
|
||||||
JCSystem.beginTransaction();
|
if (i > ISO7816.OFFSET_CDATA) {
|
||||||
|
Util.arrayCopyNonAtomic(derivationOutput, (short) 0, apduBuffer, dataOff, (short) (Crypto.KEY_SECRET_SIZE + CHAIN_CODE_SIZE));
|
||||||
|
|
||||||
copyKeys(privateKey, publicKey, chainCode, parentPrivateKey, parentPublicKey, parentChainCode, apduBuffer, chainEnd);
|
if (!crypto.bip32IsHardened(apduBuffer, i)) {
|
||||||
|
secp256k1.derivePublicKey(apduBuffer, dataOff, apduBuffer, pubKeyOff);
|
||||||
if (!crypto.bip32CKDPriv(apduBuffer, i, privateKey, publicKey, chainCode, (short) 0)) {
|
} else {
|
||||||
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
|
apduBuffer[pubKeyOff] = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Util.arrayCopy(apduBuffer, i, keyPath, keyPathLen, (short) 4);
|
if (!crypto.bip32CKDPriv(apduBuffer, i, apduBuffer, scratchOff, apduBuffer, dataOff, derivationOutput, (short) 0)) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
short pubLen = secp256k1.derivePublicKey(privateKey, apduBuffer, chainEnd);
|
if (makeCurrent) {
|
||||||
publicKey.setW(apduBuffer, chainEnd, pubLen);
|
JCSystem.beginTransaction();
|
||||||
keyPathLen += 4;
|
|
||||||
parentValid = true;
|
|
||||||
|
|
||||||
|
parentPrivateKey.setS(apduBuffer, dataOff, Crypto.KEY_SECRET_SIZE);
|
||||||
|
Util.arrayCopy(apduBuffer, (short)(dataOff + Crypto.KEY_SECRET_SIZE), parentChainCode, (short) 0, CHAIN_CODE_SIZE);
|
||||||
|
|
||||||
|
if (apduBuffer[pubKeyOff] == 0x04) {
|
||||||
|
parentPublicKey.setW(apduBuffer, pubKeyOff, Crypto.KEY_PUB_SIZE);
|
||||||
|
} else {
|
||||||
|
secp256k1.derivePublicKey(parentPrivateKey, apduBuffer, scratchOff);
|
||||||
|
parentPublicKey.setW(apduBuffer, scratchOff, Crypto.KEY_PUB_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey.setS(derivationOutput, (short) 0, Crypto.KEY_SECRET_SIZE);
|
||||||
|
Util.arrayCopy(derivationOutput, Crypto.KEY_SECRET_SIZE, chainCode, (short) 0, CHAIN_CODE_SIZE);
|
||||||
|
secp256k1.derivePublicKey(privateKey, apduBuffer, scratchOff);
|
||||||
|
publicKey.setW(apduBuffer, scratchOff, Crypto.KEY_PUB_SIZE);
|
||||||
|
|
||||||
|
Util.arrayCopy(apduBuffer, ISO7816.OFFSET_CDATA, keyPath, pathLenOff, len);
|
||||||
|
keyPathLen = newPathLen;
|
||||||
JCSystem.commitTransaction();
|
JCSystem.commitTransaction();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the current key and key path to the parent or master key. A transaction is used to make sure this all
|
* Resets to master key
|
||||||
* happens at once. This method is called internally by the deriveKey method.
|
|
||||||
*
|
*
|
||||||
* @param toParent resets to the parent key
|
* @param apduBuffer the APDU buffer
|
||||||
* @param buffer a buffer which can be overwritten (currently the APDU buffer)
|
|
||||||
* @param offset the offset at which the buffer is free
|
|
||||||
*/
|
*/
|
||||||
private void resetKeys(boolean toParent, byte[] buffer, short offset) {
|
private void resetToMaster(byte[] apduBuffer) {
|
||||||
ECPrivateKey srcPrivKey = toParent ? parentPrivateKey : masterPrivate;
|
resetKeyStatus();
|
||||||
ECPublicKey srcPubKey = toParent ? parentPublicKey : masterPublic;
|
masterPrivate.getS(apduBuffer, ISO7816.OFFSET_CDATA);
|
||||||
byte[] srcChainCode = toParent ? parentChainCode : masterChainCode;
|
privateKey.setS(apduBuffer, ISO7816.OFFSET_CDATA, Crypto.KEY_SECRET_SIZE);
|
||||||
|
masterPublic.getW(apduBuffer, ISO7816.OFFSET_CDATA);
|
||||||
JCSystem.beginTransaction();
|
publicKey.setW(apduBuffer, ISO7816.OFFSET_CDATA, Crypto.KEY_PUB_SIZE);
|
||||||
copyKeys(srcPrivKey, srcPubKey, srcChainCode, privateKey, publicKey, chainCode, buffer, offset);
|
Util.arrayCopyNonAtomic(masterChainCode, (short) 0, chainCode, (short) 0, CHAIN_CODE_SIZE);
|
||||||
resetKeyStatus(toParent);
|
|
||||||
JCSystem.commitTransaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copys a key set to another one. Requires a transient buffer which can be overwritten.
|
|
||||||
*
|
|
||||||
* @param srcPrivate source private key
|
|
||||||
* @param srcPublic source public key
|
|
||||||
* @param srcChain source chain code
|
|
||||||
* @param dstPrivate destination private key
|
|
||||||
* @param dstPublic destination public key
|
|
||||||
* @param dstChain destination chain code
|
|
||||||
* @param buffer tmp buffer
|
|
||||||
* @param offset tmp buffer offset
|
|
||||||
*/
|
|
||||||
private void copyKeys(ECPrivateKey srcPrivate, ECPublicKey srcPublic, byte[] srcChain, ECPrivateKey dstPrivate, ECPublicKey dstPublic, byte[] dstChain, byte[] buffer, short offset) {
|
|
||||||
short pubOff = (short) (offset + srcPrivate.getS(buffer, offset));
|
|
||||||
short pubLen = srcPublic.getW(buffer, pubOff);
|
|
||||||
|
|
||||||
Util.arrayCopy(srcChain, (short) 0, dstChain, (short) 0, CHAIN_CODE_SIZE);
|
|
||||||
dstPrivate.setS(buffer, offset, CHAIN_CODE_SIZE);
|
|
||||||
dstPublic.setW(buffer, pubOff, pubLen);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -925,7 +995,6 @@ public class WalletApplet extends Applet {
|
|||||||
|
|
||||||
keyPathLen = 0;
|
keyPathLen = 0;
|
||||||
pinlessPathLen = 0;
|
pinlessPathLen = 0;
|
||||||
parentValid = false;
|
|
||||||
isExtended = false;
|
isExtended = false;
|
||||||
privateKey.clearKey();
|
privateKey.clearKey();
|
||||||
publicKey.clearKey();
|
publicKey.clearKey();
|
||||||
@ -1138,16 +1207,13 @@ public class WalletApplet extends Applet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes the EXPORT KEY command. Requires an open secure channel and the PIN to be verified. The P1 parameter is
|
* Processes the EXPORT KEY command. Requires an open secure channel and the PIN to be verified.
|
||||||
* an index to which key must be exported from the list of exportable ones. At the moment only the Whisper key with
|
|
||||||
* key path m/1/1 is exportable. The key is exported only if the current key path matches the key path of the key to
|
|
||||||
* be exported. The public key of the current path can always be exported with P1=0x00 and P2=0x01.
|
|
||||||
*
|
*
|
||||||
* @param apdu the JCRE-owned APDU object.
|
* @param apdu the JCRE-owned APDU object.
|
||||||
*/
|
*/
|
||||||
private void exportKey(APDU apdu) {
|
private void exportKey(APDU apdu) {
|
||||||
byte[] apduBuffer = apdu.getBuffer();
|
byte[] apduBuffer = apdu.getBuffer();
|
||||||
secureChannel.preprocessAPDU(apduBuffer);
|
short dataLen = secureChannel.preprocessAPDU(apduBuffer);
|
||||||
|
|
||||||
if (!pin.isValidated() || !privateKey.isInitialized()) {
|
if (!pin.isValidated() || !privateKey.isInitialized()) {
|
||||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||||
@ -1167,36 +1233,71 @@ public class WalletApplet extends Applet {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
byte[] exportPath;
|
||||||
|
short exportPathOff;
|
||||||
|
short exportPathLen;
|
||||||
|
boolean derive = false;
|
||||||
|
boolean makeCurrent = false;
|
||||||
|
|
||||||
switch (apduBuffer[ISO7816.OFFSET_P1]) {
|
switch (apduBuffer[ISO7816.OFFSET_P1]) {
|
||||||
case EXPORT_KEY_P1_ANY:
|
case EXPORT_KEY_P1_CURRENT:
|
||||||
if (!publicOnly) {
|
exportPath = keyPath;
|
||||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
exportPathOff = (short) 0;
|
||||||
}
|
exportPathLen = keyPathLen;
|
||||||
break;
|
break;
|
||||||
case EXPORT_KEY_P1_HIGH:
|
case EXPORT_KEY_P1_DERIVE_AND_MAKE_CURRENT:
|
||||||
if (keyPathLen < 4 || ((((byte)(keyPath[(byte)(keyPathLen - 4)] & EXPORT_KEY_HIGH_MASK)) != EXPORT_KEY_HIGH_MASK))){
|
makeCurrent = true;
|
||||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
case EXPORT_KEY_P1_DERIVE:
|
||||||
}
|
derive = true;
|
||||||
|
exportPath = apduBuffer;
|
||||||
|
exportPathOff = ISO7816.OFFSET_CDATA;
|
||||||
|
exportPathLen = dataLen;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
|
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!publicOnly && ((exportPathLen < (short)(((short) EIP_1581_PREFIX.length) + 8)) || (Util.arrayCompare(EIP_1581_PREFIX, (short) 0, exportPath, exportPathOff, (short) EIP_1581_PREFIX.length) != 0))) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (derive) {
|
||||||
|
doDerive(apduBuffer, dataLen, DERIVE_P1_SOURCE_MASTER, makeCurrent);
|
||||||
|
}
|
||||||
|
|
||||||
short off = SecureChannel.SC_OUT_OFFSET;
|
short off = SecureChannel.SC_OUT_OFFSET;
|
||||||
|
|
||||||
apduBuffer[off++] = TLV_KEY_TEMPLATE;
|
apduBuffer[off++] = TLV_KEY_TEMPLATE;
|
||||||
off++;
|
off++;
|
||||||
apduBuffer[off++] = TLV_PUB_KEY;
|
|
||||||
off++;
|
short len;
|
||||||
short len = publicKey.getW(apduBuffer, off);
|
|
||||||
apduBuffer[(short)(off - 1)] = (byte) len;
|
if (!derive || makeCurrent) {
|
||||||
off += len;
|
apduBuffer[off++] = TLV_PUB_KEY;
|
||||||
|
off++;
|
||||||
|
len = publicKey.getW(apduBuffer, off);
|
||||||
|
apduBuffer[(short) (off - 1)] = (byte) len;
|
||||||
|
off += len;
|
||||||
|
} else if (publicOnly) {
|
||||||
|
apduBuffer[off++] = TLV_PUB_KEY;
|
||||||
|
off++;
|
||||||
|
len = secp256k1.derivePublicKey(derivationOutput, (short) 0, apduBuffer, off);
|
||||||
|
apduBuffer[(short) (off - 1)] = (byte) len;
|
||||||
|
off += len;
|
||||||
|
}
|
||||||
|
|
||||||
if (!publicOnly) {
|
if (!publicOnly) {
|
||||||
apduBuffer[off++] = TLV_PRIV_KEY;
|
apduBuffer[off++] = TLV_PRIV_KEY;
|
||||||
off++;
|
off++;
|
||||||
len = privateKey.getS(apduBuffer, off);
|
|
||||||
|
if (!derive || makeCurrent) {
|
||||||
|
len = privateKey.getS(apduBuffer, off);
|
||||||
|
} else {
|
||||||
|
Util.arrayCopyNonAtomic(derivationOutput, (short) 0, apduBuffer, off, Crypto.KEY_SECRET_SIZE);
|
||||||
|
len = Crypto.KEY_SECRET_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
apduBuffer[(short) (off - 1)] = (byte) len;
|
apduBuffer[(short) (off - 1)] = (byte) len;
|
||||||
off += len;
|
off += len;
|
||||||
}
|
}
|
||||||
|
@ -737,7 +737,7 @@ public class WalletAppletTest {
|
|||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
byte[] keyUID = response.getData();
|
byte[] keyUID = response.getData();
|
||||||
|
|
||||||
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_ANY, true);
|
response = cmdSet.exportCurrentKey(true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
byte[] pubKey = response.getData();
|
byte[] pubKey = response.getData();
|
||||||
|
|
||||||
@ -945,13 +945,13 @@ public class WalletAppletTest {
|
|||||||
new Random().nextBytes(chainCode);
|
new Random().nextBytes(chainCode);
|
||||||
|
|
||||||
// Security condition violation: SecureChannel not open
|
// Security condition violation: SecureChannel not open
|
||||||
ResponseAPDU response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_HIGH, false);
|
ResponseAPDU response = cmdSet.exportCurrentKey(true);
|
||||||
assertEquals(0x6985, response.getSW());
|
assertEquals(0x6985, response.getSW());
|
||||||
|
|
||||||
cmdSet.autoOpenSecureChannel();
|
cmdSet.autoOpenSecureChannel();
|
||||||
|
|
||||||
// Security condition violation: PIN not verified
|
// Security condition violation: PIN not verified
|
||||||
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_HIGH, false);
|
response = cmdSet.exportCurrentKey(true);
|
||||||
assertEquals(0x6985, response.getSW());
|
assertEquals(0x6985, response.getSW());
|
||||||
|
|
||||||
response = cmdSet.verifyPIN("000000");
|
response = cmdSet.verifyPIN("000000");
|
||||||
@ -960,61 +960,50 @@ public class WalletAppletTest {
|
|||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
|
|
||||||
// Security condition violation: current key is not exportable
|
// Security condition violation: current key is not exportable
|
||||||
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_HIGH, false);
|
response = cmdSet.exportCurrentKey(false);
|
||||||
assertEquals(0x6985, response.getSW());
|
assertEquals(0x6985, response.getSW());
|
||||||
|
|
||||||
response = cmdSet.deriveKey(new byte[] { (byte) 0x80, 0x00, 0x00, 0x2c}, WalletApplet.DERIVE_P1_SOURCE_MASTER);
|
response = cmdSet.deriveKey(new byte[] {(byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2c, (byte) 0x00, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00}, WalletApplet.DERIVE_P1_SOURCE_MASTER);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_HIGH, false);
|
response = cmdSet.exportCurrentKey(false);
|
||||||
assertEquals(0x6985, response.getSW());
|
|
||||||
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);
|
|
||||||
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);
|
|
||||||
assertEquals(0x9000, response.getSW());
|
|
||||||
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_HIGH, false);
|
|
||||||
assertEquals(0x6985, response.getSW());
|
assertEquals(0x6985, response.getSW());
|
||||||
|
|
||||||
|
response = cmdSet.deriveKey(new byte[] {(byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2D, (byte) 0x00, 0x00, 0x00, 0x00}, WalletApplet.DERIVE_P1_SOURCE_MASTER);
|
||||||
// Export current public key (wrong P2)
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_ANY, false);
|
response = cmdSet.exportCurrentKey(false);
|
||||||
assertEquals(0x6985, response.getSW());
|
assertEquals(0x6985, response.getSW());
|
||||||
|
|
||||||
|
|
||||||
// Export current public key
|
// Export current public key
|
||||||
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_ANY, true);
|
response = cmdSet.exportCurrentKey(true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
byte[] keyTemplate = response.getData();
|
byte[] keyTemplate = response.getData();
|
||||||
verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 0x8000002c, 0x8000003c, 0x80000000, 0x00000000 }, true);
|
verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 0x8000002b, 0x8000003c, 0x8000062d, 0x00000000 }, true, false);
|
||||||
|
|
||||||
response = cmdSet.deriveKey(new byte[] {(byte) 0xC0, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_CURRENT);
|
// Derive & Make current
|
||||||
assertEquals(0x9000, response.getSW());
|
response = cmdSet.exportKey(new byte[] {(byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2D, (byte) 0x00, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00}, true,false);
|
||||||
|
|
||||||
// Wrong P1
|
|
||||||
response = cmdSet.exportKey((byte) 3, false);
|
|
||||||
assertEquals(0x6a86, response.getSW());
|
|
||||||
|
|
||||||
// Correct
|
|
||||||
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_HIGH, false);
|
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
keyTemplate = response.getData();
|
keyTemplate = response.getData();
|
||||||
verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 0x8000002c, 0x8000003c, 0x80000000, 0x00000000, 0xC0000001 }, false);
|
verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 0x8000002b, 0x8000003c, 0x8000062d, 0x00000000, 0x00000000 }, false, false);
|
||||||
|
|
||||||
// Correct public only
|
// Derive without making current
|
||||||
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_HIGH, true);
|
response = cmdSet.exportKey(new byte[] {(byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2D, (byte) 0x00, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x01}, false,false);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
keyTemplate = response.getData();
|
keyTemplate = response.getData();
|
||||||
verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 0x8000002c, 0x8000003c, 0x80000000, 0x00000000, 0xC0000001 }, true);
|
verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 0x8000002b, 0x8000003c, 0x8000062d, 0x00000000, 0x00000001 }, false, true);
|
||||||
|
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_KEY_PATH);
|
||||||
|
assertEquals(0x9000, response.getSW());
|
||||||
|
assertArrayEquals(new byte[] {(byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2D, (byte) 0x00, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00}, response.getData());
|
||||||
|
|
||||||
|
// Export current
|
||||||
|
response = cmdSet.exportCurrentKey(false);
|
||||||
|
assertEquals(0x9000, response.getSW());
|
||||||
|
keyTemplate = response.getData();
|
||||||
|
verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 0x8000002b, 0x8000003c, 0x8000062d, 0x00000000, 0x00000000 }, false, false);
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
response = cmdSet.deriveKey(new byte[] {}, WalletApplet.DERIVE_P1_SOURCE_MASTER);
|
response = cmdSet.deriveKey(new byte[] {}, WalletApplet.DERIVE_P1_SOURCE_MASTER);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_HIGH, false);
|
|
||||||
assertEquals(0x6985, response.getSW());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -1315,19 +1304,24 @@ public class WalletAppletTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void verifyExportedKey(byte[] keyTemplate, KeyPair keyPair, byte[] chainCode, int[] path, boolean publicOnly) {
|
private void verifyExportedKey(byte[] keyTemplate, KeyPair keyPair, byte[] chainCode, int[] path, boolean publicOnly, boolean noPubKey) {
|
||||||
ECKey key = deriveKey(keyPair, chainCode, path).decompress();
|
ECKey key = deriveKey(keyPair, chainCode, path).decompress();
|
||||||
assertEquals(WalletApplet.TLV_KEY_TEMPLATE, keyTemplate[0]);
|
assertEquals(WalletApplet.TLV_KEY_TEMPLATE, keyTemplate[0]);
|
||||||
assertEquals(WalletApplet.TLV_PUB_KEY, keyTemplate[2]);
|
int pubKeyLen = 0;
|
||||||
byte[] pubKey = Arrays.copyOfRange(keyTemplate, 4, 4 + keyTemplate[3]);
|
|
||||||
assertArrayEquals(key.getPubKey(), pubKey);
|
if (!noPubKey) {
|
||||||
|
assertEquals(WalletApplet.TLV_PUB_KEY, keyTemplate[2]);
|
||||||
|
byte[] pubKey = Arrays.copyOfRange(keyTemplate, 4, 4 + keyTemplate[3]);
|
||||||
|
assertArrayEquals(key.getPubKey(), pubKey);
|
||||||
|
pubKeyLen = 2 + pubKey.length;
|
||||||
|
}
|
||||||
|
|
||||||
if (publicOnly) {
|
if (publicOnly) {
|
||||||
assertEquals(pubKey.length + 2, keyTemplate[1]);
|
assertEquals(pubKeyLen, keyTemplate[1]);
|
||||||
assertEquals(pubKey.length + 4, keyTemplate.length);
|
assertEquals(pubKeyLen + 2, keyTemplate.length);
|
||||||
} else {
|
} else {
|
||||||
assertEquals(WalletApplet.TLV_PRIV_KEY, keyTemplate[4 + pubKey.length]);
|
assertEquals(WalletApplet.TLV_PRIV_KEY, keyTemplate[2 + pubKeyLen]);
|
||||||
byte[] privateKey = Arrays.copyOfRange(keyTemplate, 6 + pubKey.length, 6 + pubKey.length + keyTemplate[5 + pubKey.length]);
|
byte[] privateKey = Arrays.copyOfRange(keyTemplate, 4 + pubKeyLen, 4 + pubKeyLen + keyTemplate[3 + pubKeyLen]);
|
||||||
|
|
||||||
byte[] tPrivKey = key.getPrivKey().toByteArray();
|
byte[] tPrivKey = key.getPrivKey().toByteArray();
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user