mirror of
https://github.com/status-im/status-keycard.git
synced 2025-01-13 07:14:11 +00:00
implement retrieval of current key path
This commit is contained in:
parent
c062e53a6e
commit
5fc82298b8
@ -59,19 +59,23 @@ The OPEN SECURE CHANNEL command is as specified in the [SECURE_CHANNEL.MD](SECUR
|
||||
### GET STATUS
|
||||
* CLA = 0x80
|
||||
* INS = 0xF2
|
||||
* P1 = 0x00
|
||||
* P1 = 0x00 for application status, 0x01 for key path status
|
||||
* P2 = 0x00
|
||||
* Response SW = 0x9000 on success
|
||||
* Response Data = Application Status Template
|
||||
* Preconditions: Secure Channel must be opened
|
||||
* 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
|
||||
|
||||
Response Data format:
|
||||
if P1 = 0x00:
|
||||
- Tag 0xA3 = Application Status Template
|
||||
- Tag 0xC0 = PIN retry count (1 byte)
|
||||
- Tag 0xC1 = PUK retry count (1 byte)
|
||||
- Tag 0xC2 = 0 if key is not initialized, 1 otherwise
|
||||
- Tag 0xC3 = 1 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.
|
||||
|
||||
### VERIFY PIN
|
||||
|
||||
* CLA = 0x80
|
||||
@ -168,7 +172,8 @@ specifications. This command always aborts open signing sessions, if any. The ge
|
||||
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.
|
||||
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.
|
||||
|
||||
P1:
|
||||
* bit 0 = if 0 derive autonomously (only works if public key derivation is supported), if 1 do assisted derivation
|
||||
|
@ -35,11 +35,11 @@ In order to test with the simulator, you need to pass these additional parameter
|
||||
```
|
||||
com.fidesmo.gradle.javacard.home=/home/username/javacard-3_0_4
|
||||
im.status.gradle.gpshell=/usr/local/bin/gpshell
|
||||
im.status.gradle.gpshell.isd=A000000003000000
|
||||
im.status.gradle.gpshell.isd=A000000151000000
|
||||
im.status.gradle.gpshell.mac_key=404142434445464748494a4b4c4d4e4f
|
||||
im.status.gradle.gpshell.enc_key=404142434445464748494a4b4c4d4e4f
|
||||
im.status.gradle.gpshell.kek_key=404142434445464748494a4b4c4d4e4f
|
||||
im.status.gradle.gpshell.kvn=2
|
||||
im.status.gradle.gpshell.kvn=0
|
||||
```
|
||||
|
||||
## Implementation notes
|
||||
|
@ -22,6 +22,9 @@ public class WalletApplet extends Applet {
|
||||
static final short CHAIN_CODE_SIZE = 32;
|
||||
static final short SEED_SIZE = CHAIN_CODE_SIZE * 2;
|
||||
|
||||
static final byte GET_STATUS_P1_APPLICATION = 0x00;
|
||||
static final byte GET_STATUS_P1_KEY_PATH = 0x01;
|
||||
|
||||
static final byte LOAD_KEY_P1_EC = 0x01;
|
||||
static final byte LOAD_KEY_P1_EXT_EC = 0x02;
|
||||
static final byte LOAD_KEY_P1_SEED = 0x03;
|
||||
@ -58,7 +61,8 @@ public class WalletApplet extends Applet {
|
||||
static final byte TLV_KEY_INITIALIZATION_STATUS = (byte) 0xC2;
|
||||
static final byte TLV_PUBLIC_KEY_DERIVATION = (byte) 0xC3;
|
||||
|
||||
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[] 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 short KEY_PATH_MAX_DEPTH = 10;
|
||||
|
||||
private OwnerPIN pin;
|
||||
private OwnerPIN puk;
|
||||
@ -73,6 +77,9 @@ public class WalletApplet extends Applet {
|
||||
private ECPrivateKey privateKey;
|
||||
private byte[] chainCode;
|
||||
|
||||
private byte[] keyPath;
|
||||
private short keyPathLen;
|
||||
|
||||
private Signature signature;
|
||||
private boolean signInProgress;
|
||||
private boolean expectPublicKey;
|
||||
@ -101,6 +108,7 @@ public class WalletApplet extends Applet {
|
||||
masterPrivate = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, EC_KEY_SIZE, false);
|
||||
masterChainCode = new byte[32];
|
||||
chainCode = new byte[32];
|
||||
keyPath = new byte[KEY_PATH_MAX_DEPTH * 4];
|
||||
|
||||
publicKey = (ECPublicKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PUBLIC, EC_KEY_SIZE, false);
|
||||
privateKey = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, EC_KEY_SIZE, false);
|
||||
@ -176,6 +184,22 @@ public class WalletApplet extends Applet {
|
||||
short off = SecureChannel.SC_OUT_OFFSET;
|
||||
byte[] apduBuffer = apdu.getBuffer();
|
||||
|
||||
short len;
|
||||
|
||||
if (apduBuffer[ISO7816.OFFSET_P1] == GET_STATUS_P1_APPLICATION) {
|
||||
len = getApplicationStatus(apduBuffer, off);
|
||||
} else if (apduBuffer[ISO7816.OFFSET_P1] == GET_STATUS_P1_KEY_PATH) {
|
||||
len = getKeyStatus(apduBuffer, off);
|
||||
} else {
|
||||
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
|
||||
return;
|
||||
}
|
||||
|
||||
len = secureChannel.encryptAPDU(apduBuffer, len);
|
||||
apdu.setOutgoingAndSend(ISO7816.OFFSET_CDATA, len);
|
||||
}
|
||||
|
||||
private short getApplicationStatus(byte[] apduBuffer, short off) {
|
||||
apduBuffer[off++] = TLV_APPLICATION_STATUS_TEMPLATE;
|
||||
apduBuffer[off++] = 9;
|
||||
apduBuffer[off++] = TLV_PIN_RETRY_COUNT;
|
||||
@ -191,8 +215,16 @@ public class WalletApplet extends Applet {
|
||||
apduBuffer[off++] = 1;
|
||||
apduBuffer[off++] = SECP256k1.hasECPointMultiplication() ? (byte) 0x01 : (byte) 0x00;
|
||||
|
||||
short len = secureChannel.encryptAPDU(apduBuffer, (short) (off - SecureChannel.SC_OUT_OFFSET));
|
||||
apdu.setOutgoingAndSend(ISO7816.OFFSET_CDATA, len);
|
||||
return (short) (off - SecureChannel.SC_OUT_OFFSET);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private void verifyPIN(APDU apdu) {
|
||||
@ -280,6 +312,7 @@ public class WalletApplet extends Applet {
|
||||
|
||||
signInProgress = false;
|
||||
expectPublicKey = false;
|
||||
keyPathLen = 0;
|
||||
}
|
||||
|
||||
private void loadKeyPair(byte[] apduBuffer, boolean newExtended) {
|
||||
@ -378,10 +411,11 @@ public class WalletApplet extends Applet {
|
||||
if (isPublicKey) {
|
||||
publicKey.setW(apduBuffer, ISO7816.OFFSET_CDATA, len);
|
||||
expectPublicKey = false;
|
||||
keyPathLen += 4;
|
||||
return;
|
||||
}
|
||||
|
||||
if (((short) (len % 4) != 0) || (assistedDerivation && (len > 4))) {
|
||||
if (((short) (len % 4) != 0) || (assistedDerivation && (len > 4)) || ((short)(len + (isReset ? 0 : keyPathLen)) > keyPath.length)) {
|
||||
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
||||
}
|
||||
|
||||
@ -394,10 +428,13 @@ public class WalletApplet extends Applet {
|
||||
if (isReset) {
|
||||
resetKeys(apduBuffer, chainEnd);
|
||||
expectPublicKey = false;
|
||||
keyPathLen = 0;
|
||||
}
|
||||
|
||||
signInProgress = false;
|
||||
|
||||
Util.arrayCopyNonAtomic(apduBuffer, ISO7816.OFFSET_CDATA, keyPath, keyPathLen, len);
|
||||
|
||||
for (short i = ISO7816.OFFSET_CDATA; i < chainEnd; i += 4) {
|
||||
Crypto.bip32CKDPriv(apduBuffer, i, privateKey, publicKey, chainCode, (short) 0);
|
||||
|
||||
@ -412,6 +449,7 @@ public class WalletApplet extends Applet {
|
||||
}
|
||||
|
||||
expectPublicKey = false;
|
||||
keyPathLen += len;
|
||||
}
|
||||
|
||||
private void outputPublicX(APDU apdu, byte[] apduBuffer) {
|
||||
|
@ -37,13 +37,13 @@ public class WalletAppletCommandSet {
|
||||
return secureChannel.openSecureChannel(apduChannel);
|
||||
}
|
||||
|
||||
public ResponseAPDU getStatus() throws CardException {
|
||||
CommandAPDU getStatus = new CommandAPDU(0x80, WalletApplet.INS_GET_STATUS, 0, 0, 256);
|
||||
public ResponseAPDU getStatus(byte info) throws CardException {
|
||||
CommandAPDU getStatus = new CommandAPDU(0x80, WalletApplet.INS_GET_STATUS, info, 0, 256);
|
||||
return apduChannel.transmit(getStatus);
|
||||
}
|
||||
|
||||
public boolean getPublicKeyDerivationSupport() throws CardException {
|
||||
ResponseAPDU resp = getStatus();
|
||||
ResponseAPDU resp = getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
|
||||
byte[] data = secureChannel.decryptAPDU(resp.getData());
|
||||
return data[data.length - 1] == 1;
|
||||
}
|
||||
|
@ -30,7 +30,10 @@ import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.*;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.Security;
|
||||
import java.security.Signature;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
@ -116,30 +119,36 @@ public class WalletAppletTest {
|
||||
@DisplayName("GET STATUS command")
|
||||
void getStatusTest() throws CardException {
|
||||
// Security condition violation: SecureChannel not open
|
||||
ResponseAPDU response = cmdSet.getStatus();
|
||||
ResponseAPDU response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
|
||||
assertEquals(0x6985, response.getSW());
|
||||
cmdSet.openSecureChannel();
|
||||
|
||||
// Good case. Since the order of test execution is undefined, the test cannot know if the keys are initialized or not.
|
||||
// Additionally, support for public key derivation is hw dependent.
|
||||
response = cmdSet.getStatus();
|
||||
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
|
||||
assertEquals(0x9000, response.getSW());
|
||||
byte[] data = secureChannel.decryptAPDU(response.getData());
|
||||
assertTrue(Hex.toHexString(data).matches("a309c00103c10105c2010[0-1]c3010[0-1]"));
|
||||
|
||||
response = cmdSet.verifyPIN("123456");
|
||||
assertEquals(0x63C2, response.getSW());
|
||||
response = cmdSet.getStatus();
|
||||
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
|
||||
assertEquals(0x9000, response.getSW());
|
||||
data = secureChannel.decryptAPDU(response.getData());
|
||||
assertTrue(Hex.toHexString(data).matches("a309c00102c10105c2010[0-1]c3010[0-1]"));
|
||||
|
||||
response = cmdSet.verifyPIN("000000");
|
||||
assertEquals(0x9000, response.getSW());
|
||||
response = cmdSet.getStatus();
|
||||
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
|
||||
assertEquals(0x9000, response.getSW());
|
||||
data = secureChannel.decryptAPDU(response.getData());
|
||||
assertTrue(Hex.toHexString(data).matches("a309c00103c10105c2010[0-1]c3010[0-1]"));
|
||||
|
||||
// Check that key path is empty
|
||||
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_KEY_PATH);
|
||||
assertEquals(0x9000, response.getSW());
|
||||
data = secureChannel.decryptAPDU(response.getData());
|
||||
assertEquals(0, data.length);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -467,15 +476,22 @@ public class WalletAppletTest {
|
||||
assertEquals(0x9000, response.getSW());
|
||||
verifyKeyDerivation(keyPair, chainCode, new int[0]);
|
||||
|
||||
// Try to sign before load public key, then resume loading public key
|
||||
// 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}, false, true, false);
|
||||
assertEquals(0x9000, response.getSW());
|
||||
byte[] key = derivePublicKey(secureChannel.decryptAPDU(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, false, true, true);
|
||||
assertEquals(0x9000, response.getSW());
|
||||
verifyKeyDerivation(keyPair, chainCode, new int[] { 2 });
|
||||
|
||||
// Reset master key
|
||||
response = cmdSet.deriveKey(new byte[0]);
|
||||
assertEquals(0x9000, response.getSW());
|
||||
verifyKeyDerivation(keyPair, chainCode, new int[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -748,6 +764,18 @@ public class WalletAppletTest {
|
||||
|
||||
assertTrue(key.verify(hash, sig));
|
||||
assertArrayEquals(key.getPubKeyPoint().getEncoded(false), publicKey);
|
||||
|
||||
resp = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_KEY_PATH);
|
||||
assertEquals(0x9000, resp.getSW());
|
||||
byte[] rawPath = secureChannel.decryptAPDU(resp.getData());
|
||||
|
||||
assertEquals(path.length * 4, rawPath.length);
|
||||
|
||||
for (int i = 0; i < path.length; i++) {
|
||||
int k = path[i];
|
||||
int k1 = (rawPath[i * 4] << 24) | (rawPath[(i * 4) + 1] << 16) | (rawPath[(i * 4) + 2] << 8) | rawPath[(i * 4) + 3];
|
||||
assertEquals(k, k1);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] derivePublicKey(byte[] data) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user