From 32fbcfdcd5578e956cc33d4010870061306779f8 Mon Sep 17 00:00:00 2001 From: Michele Balistreri Date: Wed, 18 Oct 2017 14:30:56 +0300 Subject: [PATCH] implement DERIVE KEY test --- APPLICATION.MD | 2 +- build.gradle | 1 + .../java/im/status/wallet/WalletApplet.java | 33 +++++++++- .../status/wallet/WalletAppletCommandSet.java | 5 ++ .../im/status/wallet/WalletAppletTest.java | 63 ++++++++++++++++++- 5 files changed, 101 insertions(+), 3 deletions(-) diff --git a/APPLICATION.MD b/APPLICATION.MD index 1233c91..5262e8d 100644 --- a/APPLICATION.MD +++ b/APPLICATION.MD @@ -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 * 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. ### GENERATE MNEMONIC diff --git a/build.gradle b/build.gradle index ed7a29f..5d3095e 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,7 @@ repositories { dependencies { testCompile(files("../jcardsim/jcardsim-3.0.5-SNAPSHOT.jar")) testCompile('org.web3j:core:2.3.1') + testCompile('org.bitcoinj:bitcoinj-core:0.14.5') testCompile("org.bouncycastle:bcprov-jdk15on:1.58") testCompile("org.junit.jupiter:junit-jupiter-api:5.0.0") testRuntime("org.junit.jupiter:junit-jupiter-engine:5.0.0") diff --git a/src/main/java/im/status/wallet/WalletApplet.java b/src/main/java/im/status/wallet/WalletApplet.java index d3f2a5f..1fef6cc 100644 --- a/src/main/java/im/status/wallet/WalletApplet.java +++ b/src/main/java/im/status/wallet/WalletApplet.java @@ -343,7 +343,38 @@ public class WalletApplet extends Applet { } 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) { diff --git a/src/test/java/im/status/wallet/WalletAppletCommandSet.java b/src/test/java/im/status/wallet/WalletAppletCommandSet.java index fd8acf2..d2d9fd6 100644 --- a/src/test/java/im/status/wallet/WalletAppletCommandSet.java +++ b/src/test/java/im/status/wallet/WalletAppletCommandSet.java @@ -176,4 +176,9 @@ public class WalletAppletCommandSet { CommandAPDU sign = new CommandAPDU(0x80, WalletApplet.INS_SIGN, dataType, p2, secureChannel.encryptAPDU(data)); 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); + } } diff --git a/src/test/java/im/status/wallet/WalletAppletTest.java b/src/test/java/im/status/wallet/WalletAppletTest.java index 70afe4e..e23b136 100644 --- a/src/test/java/im/status/wallet/WalletAppletTest.java +++ b/src/test/java/im/status/wallet/WalletAppletTest.java @@ -4,6 +4,9 @@ import com.licel.jcardsim.smartcardio.CardSimulator; import com.licel.jcardsim.smartcardio.CardTerminalSimulator; import com.licel.jcardsim.utils.AIDUtil; 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.spec.ECParameterSpec; import org.bouncycastle.util.encoders.Hex; @@ -329,7 +332,7 @@ public class WalletAppletTest { @DisplayName("GENERATE MNEMONIC command") void generateMnemonicTest() throws Exception { // Security condition violation: SecureChannel not open - ResponseAPDU response = cmdSet.getStatus(); + ResponseAPDU response = cmdSet.generateMnemonic(4); assertEquals(0x6985, response.getSW()); cmdSet.openSecureChannel(); @@ -362,6 +365,46 @@ public class WalletAppletTest { 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 @DisplayName("SIGN command") 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 { byte[] messageHash = Hash.sha3(message);