implement EXPORT KEY

This commit is contained in:
Michele Balistreri 2017-10-26 14:15:40 +03:00
parent d778080899
commit b68c6bd275
4 changed files with 168 additions and 10 deletions

View File

@ -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.

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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;