diff --git a/build.gradle b/build.gradle index c245e65..63d00ef 100644 --- a/build.gradle +++ b/build.gradle @@ -54,7 +54,7 @@ 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('com.github.status-im.status-keycard-java:desktop:425d085') + testCompile('com.github.status-im.status-keycard-java:desktop:4ec4e07') testCompile('org.bouncycastle:bcprov-jdk15on:1.60') testCompile("org.junit.jupiter:junit-jupiter-api:5.1.1") testRuntime("org.junit.jupiter:junit-jupiter-engine:5.1.1") diff --git a/src/main/java/im/status/keycard/KeycardApplet.java b/src/main/java/im/status/keycard/KeycardApplet.java index 2a20052..9d5962c 100644 --- a/src/main/java/im/status/keycard/KeycardApplet.java +++ b/src/main/java/im/status/keycard/KeycardApplet.java @@ -8,7 +8,7 @@ import javacardx.crypto.Cipher; * The applet's main class. All incoming commands a processed by this class. */ public class KeycardApplet extends Applet { - static final short APPLICATION_VERSION = (short) 0x0201; + static final short APPLICATION_VERSION = (short) 0x0202; static final byte INS_GET_STATUS = (byte) 0xF2; static final byte INS_SET_NDEF = (byte) 0xF3; @@ -26,6 +26,8 @@ public class KeycardApplet extends Applet { static final byte INS_SET_PINLESS_PATH = (byte) 0xC1; static final byte INS_EXPORT_KEY = (byte) 0xC2; + static final short SW_REFERENCED_DATA_NOT_FOUND = (short) 0x6A88; + static final byte PUK_LENGTH = 12; static final byte PUK_MAX_RETRIES = 5; static final byte PIN_LENGTH = 6; @@ -63,6 +65,11 @@ public class KeycardApplet extends Applet { static final byte DUPLICATE_KEY_P1_EXPORT = 0x02; static final byte DUPLICATE_KEY_P1_IMPORT = 0x03; + static final byte SIGN_P1_CURRENT_KEY = 0x00; + static final byte SIGN_P1_DERIVE = 0x01; + static final byte SIGN_P1_DERIVE_AND_MAKE_CURRENT = 0x02; + static final byte SIGN_P1_PINLESS = 0x03; + static final byte EXPORT_KEY_P1_CURRENT = 0x00; static final byte EXPORT_KEY_P1_DERIVE = 0x01; static final byte EXPORT_KEY_P1_DERIVE_AND_MAKE_CURRENT = 0x02; @@ -113,6 +120,9 @@ public class KeycardApplet extends Applet { private ECPrivateKey privateKey; private byte[] chainCode; + private ECPublicKey pinlessPublicKey; + private ECPrivateKey pinlessPrivateKey; + private byte[] keyPath; private short keyPathLen; @@ -171,6 +181,9 @@ public class KeycardApplet extends Applet { publicKey = (ECPublicKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PUBLIC, SECP256k1.SECP256K1_KEY_SIZE, false); privateKey = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, SECP256k1.SECP256K1_KEY_SIZE, false); + pinlessPublicKey = (ECPublicKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PUBLIC, SECP256k1.SECP256K1_KEY_SIZE, false); + pinlessPrivateKey = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, SECP256k1.SECP256K1_KEY_SIZE, false); + masterChainCode = new byte[CHAIN_CODE_SIZE]; parentChainCode = new byte[CHAIN_CODE_SIZE]; chainCode = new byte[CHAIN_CODE_SIZE]; @@ -646,6 +659,7 @@ public class KeycardApplet extends Applet { break; } + pinlessPathLen = 0; generateKeyUIDAndRespond(apdu, apduBuffer); } @@ -789,21 +803,22 @@ public class KeycardApplet extends Applet { byte[] apduBuffer = apdu.getBuffer(); short len = secureChannel.preprocessAPDU(apduBuffer); - if (!((pin.isValidated() || (pinlessPathLen > 0)))) { + if (!pin.isValidated()) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } - doDerive(apduBuffer, len, apduBuffer[ISO7816.OFFSET_P1], true); + doDerive(apduBuffer, (short) 0, len, apduBuffer[ISO7816.OFFSET_P1], true); } /** * Internal derivation function, called by DERIVE KEY and EXPORT KEY * @param apduBuffer the APDU buffer - * @param len the APDU len + * @param off the offset in the APDU buffer relative to the data field + * @param len the len of the path * @param source derivation source * @param makeCurrent whether the results should be saved or not */ - private void doDerive(byte[] apduBuffer, short len, byte source, boolean makeCurrent) { + private void doDerive(byte[] apduBuffer, short off, short len, byte source, boolean makeCurrent) { if (!isExtended) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } @@ -862,7 +877,8 @@ public class KeycardApplet extends Applet { ISOException.throwIt(ISO7816.SW_WRONG_DATA); } - short scratchOff = (short) (ISO7816.OFFSET_CDATA + len); + short pathOff = (short) (ISO7816.OFFSET_CDATA + off); + short scratchOff = (short) (pathOff + len); short dataOff = (short) (scratchOff + Crypto.KEY_DERIVATION_SCRATCH_SIZE); short pubKeyOff = (short) (dataOff + sourcePriv.getS(apduBuffer, dataOff)); @@ -874,8 +890,8 @@ public class KeycardApplet extends Applet { apduBuffer[pubKeyOff] = 0; } - for (short i = ISO7816.OFFSET_CDATA; i < scratchOff; i += 4) { - if (i > ISO7816.OFFSET_CDATA) { + for (short i = pathOff; i < scratchOff; i += 4) { + if (i > pathOff) { Util.arrayCopyNonAtomic(derivationOutput, (short) 0, apduBuffer, dataOff, (short) (Crypto.KEY_SECRET_SIZE + CHAIN_CODE_SIZE)); if (!crypto.bip32IsHardened(apduBuffer, i)) { @@ -908,7 +924,7 @@ public class KeycardApplet extends Applet { secp256k1.derivePublicKey(privateKey, apduBuffer, scratchOff); publicKey.setW(apduBuffer, scratchOff, Crypto.KEY_PUB_SIZE); - Util.arrayCopy(apduBuffer, ISO7816.OFFSET_CDATA, keyPath, pathLenOff, len); + Util.arrayCopy(apduBuffer, pathOff, keyPath, pathLenOff, len); keyPathLen = newPathLen; JCSystem.commitTransaction(); } @@ -1030,6 +1046,8 @@ public class KeycardApplet extends Applet { masterPublic.clearKey(); parentPrivateKey.clearKey(); parentPublicKey.clearKey(); + pinlessPrivateKey.clearKey(); + pinlessPublicKey.clearKey(); resetCurveParameters(); Util.arrayFillNonAtomic(chainCode, (short) 0, (short) chainCode.length, (byte) 0); Util.arrayFillNonAtomic(parentChainCode, (short) 0, (short) parentChainCode.length, (byte) 0); @@ -1057,6 +1075,7 @@ public class KeycardApplet extends Applet { crypto.random.generateData(apduBuffer, ISO7816.OFFSET_CDATA, BIP39_SEED_SIZE); loadSeed(apduBuffer); + pinlessPathLen = 0; generateKeyUIDAndRespond(apdu, apduBuffer); } @@ -1094,6 +1113,7 @@ public class KeycardApplet extends Applet { break; case DUPLICATE_KEY_P1_IMPORT: importDuplicate(apduBuffer); + pinlessPathLen = 0; generateKeyUIDAndRespond(apdu, apduBuffer); break; default: @@ -1180,32 +1200,96 @@ public class KeycardApplet extends Applet { */ private void sign(APDU apdu) { byte[] apduBuffer = apdu.getBuffer(); - short len = secureChannel.preprocessAPDU(apduBuffer); + boolean usePinless = false; + boolean derive = false; + boolean makeCurrent = false; - if (!((pin.isValidated() || isPinless()) && privateKey.isInitialized())) { + ECPrivateKey signingKey; + ECPublicKey outputKey; + + switch((byte) (apduBuffer[ISO7816.OFFSET_P1] & ~DERIVE_P1_SOURCE_MASK)) { + case SIGN_P1_CURRENT_KEY: + signingKey = privateKey; + outputKey = publicKey; + break; + case SIGN_P1_DERIVE: + signingKey = secp256k1.tmpECPrivateKey; + outputKey = null; + derive = true; + break; + case SIGN_P1_DERIVE_AND_MAKE_CURRENT: + signingKey = privateKey; + outputKey = publicKey; + derive = true; + makeCurrent = true; + break; + case SIGN_P1_PINLESS: + usePinless = true; + signingKey = pinlessPrivateKey; + outputKey = pinlessPublicKey; + break; + default: + ISOException.throwIt(ISO7816.SW_WRONG_P1P2); + return; + } + + short len; + + if (usePinless && !secureChannel.isOpen()) { + len = (short) (apduBuffer[ISO7816.OFFSET_LC] & (short) 0xff); + } else { + len = secureChannel.preprocessAPDU(apduBuffer); + } + + if (usePinless && pinlessPathLen == 0) { + ISOException.throwIt(SW_REFERENCED_DATA_NOT_FOUND); + } + + if (!((pin.isValidated() || usePinless || isPinless()) && privateKey.isInitialized())) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } - if (len != MessageDigest.LENGTH_SHA_256) { - ISOException.throwIt(ISO7816.SW_WRONG_DATA); - } + if (derive) { + short pathLen = (short) (len - MessageDigest.LENGTH_SHA_256); - signature.init(privateKey, Signature.MODE_SIGN); + if (pathLen <= 0) { + ISOException.throwIt(ISO7816.SW_WRONG_DATA); + } + + byte derivationSource = (byte) (apduBuffer[ISO7816.OFFSET_P1] & DERIVE_P1_SOURCE_MASK); + doDerive(apduBuffer, MessageDigest.LENGTH_SHA_256, pathLen, derivationSource, makeCurrent); + } else { + if (len != MessageDigest.LENGTH_SHA_256) { + ISOException.throwIt(ISO7816.SW_WRONG_DATA); + } + } apduBuffer[SecureChannel.SC_OUT_OFFSET] = TLV_SIGNATURE_TEMPLATE; apduBuffer[(short)(SecureChannel.SC_OUT_OFFSET + 3)] = TLV_PUB_KEY; - short outLen = apduBuffer[(short)(SecureChannel.SC_OUT_OFFSET + 4)] = (byte) publicKey.getW(apduBuffer, (short) (SecureChannel.SC_OUT_OFFSET + 5)); + short outLen = apduBuffer[(short)(SecureChannel.SC_OUT_OFFSET + 4)] = Crypto.KEY_PUB_SIZE; + + if (outputKey != null) { + outputKey.getW(apduBuffer, (short) (SecureChannel.SC_OUT_OFFSET + 5)); + } else { + secp256k1.derivePublicKey(derivationOutput, (short) 0, apduBuffer, (short) (SecureChannel.SC_OUT_OFFSET + 5)); + } outLen += 5; short sigOff = (short) (SecureChannel.SC_OUT_OFFSET + outLen); - outLen += signature.signPreComputedHash(apduBuffer, ISO7816.OFFSET_CDATA, len, apduBuffer, sigOff); + signature.init(signingKey, Signature.MODE_SIGN); + + outLen += signature.signPreComputedHash(apduBuffer, ISO7816.OFFSET_CDATA, MessageDigest.LENGTH_SHA_256, apduBuffer, sigOff); outLen += crypto.fixS(apduBuffer, sigOff); apduBuffer[(short)(SecureChannel.SC_OUT_OFFSET + 1)] = (byte) 0x81; apduBuffer[(short)(SecureChannel.SC_OUT_OFFSET + 2)] = (byte) (outLen - 3); - secureChannel.respond(apdu, outLen, ISO7816.SW_NO_ERROR); + if (secureChannel.isOpen()) { + secureChannel.respond(apdu, outLen, ISO7816.SW_NO_ERROR); + } else { + apdu.setOutgoingAndSend(SecureChannel.SC_OUT_OFFSET, outLen); + } } /** @@ -1231,6 +1315,14 @@ public class KeycardApplet extends Applet { JCSystem.beginTransaction(); pinlessPathLen = len; Util.arrayCopy(apduBuffer, ISO7816.OFFSET_CDATA, pinlessPath, (short) 0, len); + + if (pinlessPathLen > 0) { + doDerive(apduBuffer, (short) 0, len, DERIVE_P1_SOURCE_MASTER, false); + pinlessPrivateKey.setS(derivationOutput, (short) 0, Crypto.KEY_SECRET_SIZE); + secp256k1.derivePublicKey(pinlessPrivateKey, apduBuffer, (short) 0); + pinlessPublicKey.setW(apduBuffer, (short) 0, Crypto.KEY_PUB_SIZE); + } + JCSystem.commitTransaction(); } @@ -1292,7 +1384,7 @@ public class KeycardApplet extends Applet { } if (derive) { - doDerive(apduBuffer, dataLen, derivationSource, makeCurrent); + doDerive(apduBuffer, (short) 0, dataLen, derivationSource, makeCurrent); } short off = SecureChannel.SC_OUT_OFFSET; @@ -1379,5 +1471,8 @@ public class KeycardApplet extends Applet { secp256k1.setCurveParameters(publicKey); secp256k1.setCurveParameters(privateKey); + + secp256k1.setCurveParameters(pinlessPublicKey); + secp256k1.setCurveParameters(pinlessPrivateKey); } } diff --git a/src/main/java/im/status/keycard/SECP256k1.java b/src/main/java/im/status/keycard/SECP256k1.java index 1a9bbc2..ad33bba 100644 --- a/src/main/java/im/status/keycard/SECP256k1.java +++ b/src/main/java/im/status/keycard/SECP256k1.java @@ -55,7 +55,7 @@ public class SECP256k1 { private KeyAgreement ecPointMultiplier; private Crypto crypto; - private ECPrivateKey tmpECPrivateKey; + ECPrivateKey tmpECPrivateKey; /** * Allocates objects needed by this class. Must be invoked during the applet installation exactly 1 time. diff --git a/src/test/java/im/status/keycard/KeycardTest.java b/src/test/java/im/status/keycard/KeycardTest.java index f0e5a61..aaf6fee 100644 --- a/src/test/java/im/status/keycard/KeycardTest.java +++ b/src/test/java/im/status/keycard/KeycardTest.java @@ -37,11 +37,11 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.Signature; +import java.security.*; + import org.bouncycastle.jce.interfaces.ECPublicKey; + +import java.security.spec.InvalidKeySpecException; import java.util.Arrays; import java.util.HashSet; import java.util.Random; @@ -1002,11 +1002,8 @@ public class KeycardTest { assertEquals(0x9000, response.getSw()); } - Signature signature = Signature.getInstance("SHA256withECDSA", "BC"); - if (!cmdSet.getApplicationInfo().hasMasterKey()) { - KeyPair keyPair = keypairGenerator().generateKeyPair(); - response = cmdSet.loadKey(keyPair); + response = cmdSet.generateKey(); assertEquals(0x9000, response.getSw()); } @@ -1016,6 +1013,52 @@ public class KeycardTest { // Correctly sign a precomputed hash response = cmdSet.sign(hash); + verifySignResp(data, response); + + // Sign and derive + String currentPath = new KeyPath(cmdSet.getStatus(KeycardCommandSet.GET_STATUS_P1_KEY_PATH).checkOK().getData()).toString(); + String updatedPath = new KeyPath(currentPath + "/2").toString(); + response = cmdSet.signWithPath(hash, updatedPath, false); + verifySignResp(data, response); + assertEquals(currentPath, new KeyPath(cmdSet.getStatus(KeycardCommandSet.GET_STATUS_P1_KEY_PATH).checkOK().getData()).toString()); + response = cmdSet.signWithPath(hash, updatedPath, true); + verifySignResp(data, response); + assertEquals(updatedPath, new KeyPath(cmdSet.getStatus(KeycardCommandSet.GET_STATUS_P1_KEY_PATH).checkOK().getData()).toString()); + + // Sign with PINless + String pinlessPath = currentPath + "/3"; + response = cmdSet.setPinlessPath(pinlessPath); + assertEquals(0x9000, response.getSw()); + + // No secure channel or PIN auth + response = cmdSet.select(); + assertEquals(0x9000, response.getSw()); + + response = cmdSet.signPinless(hash); + verifySignResp(data, response); + + // With secure channel + if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { + cmdSet.autoOpenSecureChannel(); + response = cmdSet.signPinless(hash); + verifySignResp(data, response); + } + + // No pinless path + if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { + response = cmdSet.verifyPIN("000000"); + assertEquals(0x9000, response.getSw()); + } + + response = cmdSet.resetPinlessPath(); + assertEquals(0x9000, response.getSw()); + + response = cmdSet.signPinless(hash); + assertEquals(0x6A88, response.getSw()); + } + + private void verifySignResp(byte[] data, APDUResponse response) throws Exception { + Signature signature = Signature.getInstance("SHA256withECDSA", "BC"); assertEquals(0x9000, response.getSw()); byte[] sig = response.getData(); byte[] keyData = extractPublicKeyFromSignature(sig); @@ -1082,12 +1125,19 @@ public class KeycardTest { resetAndSelectAndOpenSC(); response = cmdSet.sign(hash); assertEquals(0x6985, response.getSw()); + + if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { + response = cmdSet.verifyPIN("000000"); + assertEquals(0x9000, response.getSw()); + } + response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01}, KeycardApplet.DERIVE_P1_SOURCE_MASTER); assertEquals(0x9000, response.getSw()); - response = cmdSet.sign(hash); - assertEquals(0x6985, response.getSw()); response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, KeycardApplet.DERIVE_P1_SOURCE_CURRENT); assertEquals(0x9000, response.getSw()); + + resetAndSelectAndOpenSC(); + response = cmdSet.sign(hash); assertEquals(0x9000, response.getSw()); @@ -1102,8 +1152,16 @@ public class KeycardTest { resetAndSelectAndOpenSC(); response = cmdSet.sign(hash); assertEquals(0x6985, response.getSw()); + + + if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { + response = cmdSet.verifyPIN("000000"); + assertEquals(0x9000, response.getSw()); + } + response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01}, KeycardApplet.DERIVE_P1_SOURCE_MASTER); assertEquals(0x9000, response.getSw()); + resetAndSelectAndOpenSC(); response = cmdSet.sign(hash); assertEquals(0x9000, response.getSw());