implement DERIVE KEY test

This commit is contained in:
Michele Balistreri 2017-10-18 14:30:56 +03:00
parent de91e33f74
commit 32fbcfdcd5
5 changed files with 101 additions and 3 deletions

View File

@ -157,7 +157,7 @@ signing sessions, if any. Unless a DERIVE KEY is sent, a subsequent SIGN command
* Response SW = 0x9000 on success, 0x6A80 if the format is invalid * Response SW = 0x9000 on success, 0x6A80 if the format is invalid
* Preconditions: Secure Channel must be opened, user PIN must be verified, an extended keyset must be loaded * Preconditions: Secure Channel must be opened, user PIN must be verified, an extended keyset must be loaded
This command is used before a signing session to generated a private key according to the [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) 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. The generated key is used for all subsequent SIGN sessions. specifications. The generated key is used for all subsequent SIGN sessions.
### GENERATE MNEMONIC ### GENERATE MNEMONIC

View File

@ -34,6 +34,7 @@ repositories {
dependencies { dependencies {
testCompile(files("../jcardsim/jcardsim-3.0.5-SNAPSHOT.jar")) testCompile(files("../jcardsim/jcardsim-3.0.5-SNAPSHOT.jar"))
testCompile('org.web3j:core:2.3.1') testCompile('org.web3j:core:2.3.1')
testCompile('org.bitcoinj:bitcoinj-core:0.14.5')
testCompile("org.bouncycastle:bcprov-jdk15on:1.58") testCompile("org.bouncycastle:bcprov-jdk15on:1.58")
testCompile("org.junit.jupiter:junit-jupiter-api:5.0.0") testCompile("org.junit.jupiter:junit-jupiter-api:5.0.0")
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.0.0") testRuntime("org.junit.jupiter:junit-jupiter-engine:5.0.0")

View File

@ -343,7 +343,38 @@ public class WalletApplet extends Applet {
} }
private void deriveKey(APDU apdu) { private void deriveKey(APDU apdu) {
ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED); apdu.setIncomingAndReceive();
if (!(secureChannel.isOpen() && pin.isValidated() && isExtended)) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
byte[] apduBuffer = apdu.getBuffer();
short len = secureChannel.decryptAPDU(apduBuffer);
if ((short) (len % 4) != 0) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
resetKeys(apduBuffer, len);
for (short i = 0; i < len; i += 4) {
Crypto.bip32CKDPriv(apduBuffer, i, privateKey, publicKey, chainCode, (short) 0);
short pubLen = SECP256k1.derivePublicKey(privateKey, apduBuffer, (short) 0);
publicKey.setW(apduBuffer, (short) 0, pubLen);
}
}
private void resetKeys(byte[] buffer, short offset) {
short pubOff = (short) (offset + masterPrivate.getS(buffer, offset));
short pubLen = masterPublic.getW(buffer, pubOff);
JCSystem.beginTransaction();
Util.arrayCopy(masterChainCode, (short) 0, chainCode, (short) 0, CHAIN_CODE_SIZE);
privateKey.setS(buffer, offset, CHAIN_CODE_SIZE);
publicKey.setW(buffer, pubOff, pubLen);
JCSystem.commitTransaction();
} }
private void generateMnemonic(APDU apdu) { private void generateMnemonic(APDU apdu) {

View File

@ -176,4 +176,9 @@ public class WalletAppletCommandSet {
CommandAPDU sign = new CommandAPDU(0x80, WalletApplet.INS_SIGN, dataType, p2, secureChannel.encryptAPDU(data)); CommandAPDU sign = new CommandAPDU(0x80, WalletApplet.INS_SIGN, dataType, p2, secureChannel.encryptAPDU(data));
return apduChannel.transmit(sign); return apduChannel.transmit(sign);
} }
public ResponseAPDU deriveKey(byte[] data) throws CardException {
CommandAPDU deriveKey = new CommandAPDU(0x80, WalletApplet.INS_DERIVE_KEY, 0x00, 0x00, secureChannel.encryptAPDU(data));
return apduChannel.transmit(deriveKey);
}
} }

View File

@ -4,6 +4,9 @@ import com.licel.jcardsim.smartcardio.CardSimulator;
import com.licel.jcardsim.smartcardio.CardTerminalSimulator; import com.licel.jcardsim.smartcardio.CardTerminalSimulator;
import com.licel.jcardsim.utils.AIDUtil; import com.licel.jcardsim.utils.AIDUtil;
import javacard.framework.AID; import javacard.framework.AID;
import org.bitcoinj.crypto.ChildNumber;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.HDKeyDerivation;
import org.bouncycastle.jce.ECNamedCurveTable; import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECParameterSpec; import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.util.encoders.Hex; import org.bouncycastle.util.encoders.Hex;
@ -329,7 +332,7 @@ public class WalletAppletTest {
@DisplayName("GENERATE MNEMONIC command") @DisplayName("GENERATE MNEMONIC command")
void generateMnemonicTest() throws Exception { void generateMnemonicTest() throws Exception {
// Security condition violation: SecureChannel not open // Security condition violation: SecureChannel not open
ResponseAPDU response = cmdSet.getStatus(); ResponseAPDU response = cmdSet.generateMnemonic(4);
assertEquals(0x6985, response.getSW()); assertEquals(0x6985, response.getSW());
cmdSet.openSecureChannel(); cmdSet.openSecureChannel();
@ -362,6 +365,46 @@ public class WalletAppletTest {
assertMnemonic(24, secureChannel.decryptAPDU(response.getData())); assertMnemonic(24, secureChannel.decryptAPDU(response.getData()));
} }
@Test
@DisplayName("DERIVE KEY command")
void deriveKeyTest() throws Exception {
// Security condition violation: SecureChannel not open
ResponseAPDU response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x00});
assertEquals(0x6985, response.getSW());
cmdSet.openSecureChannel();
// Security condition violation: PIN is not verified
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x00});
assertEquals(0x6985, response.getSW());
response = cmdSet.verifyPIN("000000");
assertEquals(0x9000, response.getSW());
KeyPairGenerator g = keypairGenerator();
KeyPair keyPair = g.generateKeyPair();
byte[] chainCode = new byte[32];
new Random().nextBytes(chainCode);
// Condition violation: keyset is not extended
response = cmdSet.loadKey(keyPair);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x00});
assertEquals(0x6985, response.getSW());
response = cmdSet.loadKey(keyPair, false, chainCode);
assertEquals(0x9000, response.getSW());
// Wrong data format (data length not a multiple of 4)
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00});
assertEquals(0x6A80, response.getSW());
// Correct example
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x00});
assertEquals(0x9000, response.getSW());
verifyKeyDerivation(keyPair, chainCode, new int[] { 1 });
}
@Test @Test
@DisplayName("SIGN command") @DisplayName("SIGN command")
void signTest() throws Exception { void signTest() throws Exception {
@ -590,6 +633,24 @@ 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));
}
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);
sig = extractSignature(sig);
assertTrue(key.verify(hash, sig));
assertArrayEquals(key.getPubKeyPoint().getEncoded(false), publicKey);
}
private Sign.SignatureData signMessage(byte[] message) throws Exception { private Sign.SignatureData signMessage(byte[] message) throws Exception {
byte[] messageHash = Hash.sha3(message); byte[] messageHash = Hash.sha3(message);