implement EXPORT KEY
This commit is contained in:
parent
d778080899
commit
b68c6bd275
|
@ -270,3 +270,25 @@ On applet selection any pending signing session is aborted.
|
|||
|
||||
Sets the given sequence of 32-bit integers as a PIN-less path. When the current derived key matches this path, SIGN
|
||||
will work even if no PIN authentication has been performed. An empty sequence means that no PIN-less path is defined.
|
||||
|
||||
### EXPORT KEY
|
||||
|
||||
* CLA = 0x80
|
||||
* INS = 0xC2
|
||||
* P1 = 0x01 (Whisper key)
|
||||
* P2 = 0x00
|
||||
* Response SW = 0x9000 on success, 0x6A86 if P1 is wrong
|
||||
* 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
|
||||
the key selected through P1
|
||||
|
||||
Response Data format:
|
||||
- Tag 0xA1 = keypair template
|
||||
- Tag 0x80 = ECC public key component
|
||||
- Tag 0x81 = ECC private key component
|
||||
|
||||
This command exports the current public and private key if and only if the current key path matches the one of the key
|
||||
selected by P1. P1 is only an index, the actual key path is stored immutably in the applet itself. At the moment only
|
||||
the Whisper key (P1=0x01) can be exported and its key path is m/1/1. Other key paths could be added in the future, but
|
||||
the last should remain as short as possible because of the security implications of revealing private keys to a possibly
|
||||
compromised device. The current chain code is never exported to make it impossible to further derive keys off-card.
|
|
@ -13,6 +13,7 @@ public class WalletApplet extends Applet {
|
|||
static final byte INS_GENERATE_MNEMONIC = (byte) 0xD2;
|
||||
static final byte INS_SIGN = (byte) 0xC0;
|
||||
static final byte INS_SET_PINLESS_PATH = (byte) 0xC1;
|
||||
static final byte INS_EXPORT_KEY = (byte) 0xC2;
|
||||
|
||||
static final byte PUK_LENGTH = 12;
|
||||
static final byte PUK_MAX_RETRIES = 5;
|
||||
|
@ -47,6 +48,8 @@ public class WalletApplet extends Applet {
|
|||
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);
|
||||
|
||||
static final byte EXPORT_KEY_P1_WHISPER = 0x01;
|
||||
|
||||
static final byte TLV_SIGNATURE_TEMPLATE = (byte) 0xA0;
|
||||
|
||||
static final byte TLV_KEY_TEMPLATE = (byte) 0xA1;
|
||||
|
@ -64,6 +67,7 @@ public class WalletApplet extends Applet {
|
|||
static final byte TLV_PUBLIC_KEY_DERIVATION = (byte) 0xC3;
|
||||
|
||||
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[] WHISPER_KEY_PATH = { 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01};
|
||||
|
||||
private OwnerPIN pin;
|
||||
private OwnerPIN puk;
|
||||
|
@ -168,6 +172,9 @@ public class WalletApplet extends Applet {
|
|||
case INS_SET_PINLESS_PATH:
|
||||
setPinlessPath(apdu);
|
||||
break;
|
||||
case INS_EXPORT_KEY:
|
||||
exportKey(apdu);
|
||||
break;
|
||||
default:
|
||||
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
|
||||
break;
|
||||
|
@ -589,6 +596,49 @@ public class WalletApplet extends Applet {
|
|||
}
|
||||
}
|
||||
|
||||
private void exportKey(APDU apdu) {
|
||||
apdu.setIncomingAndReceive();
|
||||
|
||||
if (!(secureChannel.isOpen() && pin.isValidated())) {
|
||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||
}
|
||||
|
||||
byte[] apduBuffer = apdu.getBuffer();
|
||||
byte[] toExport;
|
||||
|
||||
switch (apduBuffer[ISO7816.OFFSET_P1]) {
|
||||
case EXPORT_KEY_P1_WHISPER:
|
||||
toExport = WHISPER_KEY_PATH;
|
||||
break;
|
||||
default:
|
||||
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!((keyPathLen == toExport.length) && (Util.arrayCompare(keyPath, (short) 0, toExport, (short) 0, keyPathLen) == 0))) {
|
||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||
}
|
||||
|
||||
short off = SecureChannel.SC_OUT_OFFSET;
|
||||
|
||||
apduBuffer[off++] = TLV_KEY_TEMPLATE;
|
||||
off++;
|
||||
apduBuffer[off++] = TLV_PUB_KEY;
|
||||
off++;
|
||||
short len = publicKey.getW(apduBuffer, off);
|
||||
apduBuffer[(short)(off - 1)] = (byte) len;
|
||||
off += len;
|
||||
apduBuffer[off++] = TLV_PRIV_KEY;
|
||||
off++;
|
||||
len = privateKey.getS(apduBuffer, off);
|
||||
apduBuffer[(short)(off - 1)] = (byte) len;
|
||||
len += (off - SecureChannel.SC_OUT_OFFSET);
|
||||
apduBuffer[(SecureChannel.SC_OUT_OFFSET + 1)] = (byte) (len - 2);
|
||||
|
||||
len = secureChannel.encryptAPDU(apduBuffer, (short) len);
|
||||
apdu.setOutgoingAndSend(ISO7816.OFFSET_CDATA, len);
|
||||
}
|
||||
|
||||
private void setPinlessPath(APDU apdu) {
|
||||
apdu.setIncomingAndReceive();
|
||||
|
||||
|
|
|
@ -200,4 +200,9 @@ public class WalletAppletCommandSet {
|
|||
CommandAPDU setPinlessPath = new CommandAPDU(0x80, WalletApplet.INS_SET_PINLESS_PATH, 0x00, 0x00, secureChannel.encryptAPDU(data));
|
||||
return apduChannel.transmit(setPinlessPath);
|
||||
}
|
||||
|
||||
public ResponseAPDU exportKey(byte keyPathIndex) throws CardException {
|
||||
CommandAPDU exportKey = new CommandAPDU(0x80, WalletApplet.INS_EXPORT_KEY, keyPathIndex, 0x00, 256);
|
||||
return apduChannel.transmit(exportKey);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -531,7 +531,7 @@ public class WalletAppletTest {
|
|||
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
|
||||
assertEquals(0x9000, response.getSW());
|
||||
byte[] sig = secureChannel.decryptAPDU(response.getData());
|
||||
byte[] keyData = extractPublicKey(sig);
|
||||
byte[] keyData = extractPublicKeyFromSignature(sig);
|
||||
sig = extractSignature(sig);
|
||||
assertEquals((SecureChannel.SC_KEY_LENGTH * 2 / 8) + 1, keyData.length);
|
||||
signature.update(data);
|
||||
|
@ -627,6 +627,63 @@ public class WalletAppletTest {
|
|||
assertEquals(0x6985, response.getSW());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("EXPORT KEY command")
|
||||
void exportKey() throws Exception {
|
||||
KeyPairGenerator g = keypairGenerator();
|
||||
KeyPair keyPair = g.generateKeyPair();
|
||||
byte[] chainCode = new byte[32];
|
||||
new Random().nextBytes(chainCode);
|
||||
|
||||
// Security condition violation: SecureChannel not open
|
||||
ResponseAPDU response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER);
|
||||
assertEquals(0x6985, response.getSW());
|
||||
|
||||
cmdSet.openSecureChannel();
|
||||
|
||||
// Security condition violation: PIN not verified
|
||||
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER);
|
||||
assertEquals(0x6985, response.getSW());
|
||||
|
||||
response = cmdSet.verifyPIN("000000");
|
||||
assertEquals(0x9000, response.getSW());
|
||||
response = cmdSet.loadKey(keyPair, false, chainCode);
|
||||
assertEquals(0x9000, response.getSW());
|
||||
|
||||
// Security condition violation: current key is not Whisper key
|
||||
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER);
|
||||
assertEquals(0x6985, response.getSW());
|
||||
|
||||
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, true, true, false);
|
||||
assertEquals(0x9000, response.getSW());
|
||||
response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true);
|
||||
assertEquals(0x9000, response.getSW());
|
||||
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER);
|
||||
assertEquals(0x6985, response.getSW());
|
||||
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, false, true, false);
|
||||
assertEquals(0x9000, response.getSW());
|
||||
response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true);
|
||||
assertEquals(0x9000, response.getSW());
|
||||
|
||||
// Wrong P1
|
||||
response = cmdSet.exportKey((byte) 0);
|
||||
assertEquals(0x6a86, response.getSW());
|
||||
response = cmdSet.exportKey((byte) 2);
|
||||
assertEquals(0x6a86, response.getSW());
|
||||
|
||||
// Correct
|
||||
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER);
|
||||
assertEquals(0x9000, response.getSW());
|
||||
byte[] keyTemplate = secureChannel.decryptAPDU(response.getData());
|
||||
verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 1, 1 });
|
||||
|
||||
// Reset
|
||||
response = cmdSet.deriveKey(new byte[] {}, true, false, false);
|
||||
assertEquals(0x9000, response.getSW());
|
||||
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER);
|
||||
assertEquals(0x6985, response.getSW());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SIGN data (unused for the current scenario)")
|
||||
@Tag("manual")
|
||||
|
@ -785,7 +842,7 @@ public class WalletAppletTest {
|
|||
return Arrays.copyOfRange(sig, off, off + sig[off + 1] + 2);
|
||||
}
|
||||
|
||||
private byte[] extractPublicKey(byte[] sig) {
|
||||
private byte[] extractPublicKeyFromSignature(byte[] sig) {
|
||||
assertEquals(WalletApplet.TLV_SIGNATURE_TEMPLATE, sig[0]);
|
||||
assertEquals((byte) 0x81, sig[1]);
|
||||
assertEquals(WalletApplet.TLV_PUB_KEY, sig[3]);
|
||||
|
@ -838,17 +895,13 @@ public class WalletAppletTest {
|
|||
}
|
||||
|
||||
private void verifyKeyDerivation(KeyPair keyPair, byte[] chainCode, int[] path) throws Exception {
|
||||
DeterministicKey key = HDKeyDerivation.createMasterPrivKeyFromBytes(((org.bouncycastle.jce.interfaces.ECPrivateKey) keyPair.getPrivate()).getD().toByteArray(), chainCode);
|
||||
|
||||
for (int i : path) {
|
||||
key = HDKeyDerivation.deriveChildKey(key, new ChildNumber(i));
|
||||
}
|
||||
DeterministicKey key = deriveKey(keyPair, chainCode, path);
|
||||
|
||||
byte[] hash = Hash.sha3(new byte[8]);
|
||||
ResponseAPDU resp = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH, true, true);
|
||||
assertEquals(0x9000, resp.getSW());
|
||||
byte[] sig = secureChannel.decryptAPDU(resp.getData());
|
||||
byte[] publicKey = extractPublicKey(sig);
|
||||
byte[] publicKey = extractPublicKeyFromSignature(sig);
|
||||
sig = extractSignature(sig);
|
||||
|
||||
assertTrue(key.verify(hash, sig));
|
||||
|
@ -867,6 +920,34 @@ public class WalletAppletTest {
|
|||
}
|
||||
}
|
||||
|
||||
private void verifyExportedKey(byte[] keyTemplate, KeyPair keyPair, byte[] chainCode, int[] path) {
|
||||
ECKey key = deriveKey(keyPair, chainCode, path).decompress();
|
||||
assertEquals(WalletApplet.TLV_KEY_TEMPLATE, keyTemplate[0]);
|
||||
assertEquals(WalletApplet.TLV_PUB_KEY, keyTemplate[2]);
|
||||
byte[] pubKey = Arrays.copyOfRange(keyTemplate, 4, 4 + keyTemplate[3]);
|
||||
assertEquals(WalletApplet.TLV_PRIV_KEY, keyTemplate[4 + pubKey.length]);
|
||||
byte[] privateKey = Arrays.copyOfRange(keyTemplate, 6 + pubKey.length, 6 + pubKey.length + keyTemplate[5 + pubKey.length]);
|
||||
|
||||
byte[] tPrivKey = key.getPrivKey().toByteArray();
|
||||
|
||||
if (tPrivKey[0] == 0x00) {
|
||||
tPrivKey = Arrays.copyOfRange(tPrivKey, 1, tPrivKey.length);
|
||||
}
|
||||
|
||||
assertArrayEquals(key.getPubKey(), pubKey);
|
||||
assertArrayEquals(tPrivKey, privateKey);
|
||||
}
|
||||
|
||||
private DeterministicKey deriveKey(KeyPair keyPair, byte[] chainCode, int[] path) {
|
||||
DeterministicKey key = HDKeyDerivation.createMasterPrivKeyFromBytes(((org.bouncycastle.jce.interfaces.ECPrivateKey) keyPair.getPrivate()).getD().toByteArray(), chainCode);
|
||||
|
||||
for (int i : path) {
|
||||
key = HDKeyDerivation.deriveChildKey(key, new ChildNumber(i));
|
||||
}
|
||||
|
||||
return 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);
|
||||
|
@ -909,7 +990,7 @@ public class WalletAppletTest {
|
|||
Method recoverFromSignature = Sign.class.getDeclaredMethod("recoverFromSignature", int.class, ecdsaSignature, byte[].class);
|
||||
recoverFromSignature.setAccessible(true);
|
||||
|
||||
byte[] pubData = extractPublicKey(respData);
|
||||
byte[] pubData = extractPublicKeyFromSignature(respData);
|
||||
BigInteger publicKey = new BigInteger(Arrays.copyOfRange(pubData, 1, pubData.length));
|
||||
|
||||
int recId = -1;
|
||||
|
|
Loading…
Reference in New Issue