diff --git a/.gitignore b/.gitignore index f44dc40..87d5c88 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ # IntelliJ IDEA project files .idea *.iml +/.vscode # Gradle output /.gradle /build +/bin /gradle.properties buildSrc/build buildSrc/.gradle \ No newline at end of file diff --git a/build.gradle b/build.gradle index 2f1baae..d820058 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { dependencies { classpath 'com.fidesmo:gradle-javacard:0.2.7' - classpath 'com.github.status-im.status-keycard-java:desktop:3.0.4' + classpath 'com.github.status-im.status-keycard-java:desktop:953c845' } } @@ -31,6 +31,10 @@ javacard { aid = '0xA0:0x00:0x00:0x08:0x04:0x00:0x01:0x03' className = 'CashApplet' } + applet { + aid = '0xA0:0x00:0x00:0x08:0x04:0x00:0x01:0x04' + className = 'IdentApplet' + } version = '3.1' } } @@ -55,7 +59,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:3.0.4') + testCompile('com.github.status-im.status-keycard-java:desktop:953c845') testCompile('org.bouncycastle:bcprov-jdk15on:1.65') testCompile("org.junit.jupiter:junit-jupiter-api:5.1.1") testRuntime("org.junit.jupiter:junit-jupiter-engine:5.1.1") diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 06669cb..4ddb95c 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -4,5 +4,5 @@ repositories { } dependencies { - compile 'com.github.status-im.status-keycard-java:desktop:3.0.4' + compile 'com.github.status-im.status-keycard-java:desktop:953c845' } \ No newline at end of file diff --git a/buildSrc/src/main/java/im/status/keycard/build/InstallTask.java b/buildSrc/src/main/java/im/status/keycard/build/InstallTask.java index 8f94e9f..1f4b12c 100644 --- a/buildSrc/src/main/java/im/status/keycard/build/InstallTask.java +++ b/buildSrc/src/main/java/im/status/keycard/build/InstallTask.java @@ -1,9 +1,11 @@ package im.status.keycard.build; +import im.status.keycard.applet.Identifiers; import im.status.keycard.desktop.PCSCCardChannel; import im.status.keycard.globalplatform.GlobalPlatformCommandSet; import im.status.keycard.globalplatform.LoadCallback; import im.status.keycard.io.APDUException; +import org.bouncycastle.util.encoders.Hex; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; import org.gradle.api.logging.Logger; @@ -69,6 +71,8 @@ public class InstallTask extends DefaultTask { cmdSet.installNDEFApplet(new byte[0]).checkOK(); logger.info("Installing the Cash Applet"); cmdSet.installCashApplet().checkOK(); + logger.info("Installing the Identifier Applet"); + cmdSet.installIdentApplet().checkOK(); } catch (IOException e) { throw new GradleException("I/O error", e); } catch (APDUException e) { diff --git a/src/main/java/im/status/keycard/CashApplet.java b/src/main/java/im/status/keycard/CashApplet.java index dab799d..7e017bc 100644 --- a/src/main/java/im/status/keycard/CashApplet.java +++ b/src/main/java/im/status/keycard/CashApplet.java @@ -12,7 +12,6 @@ public class CashApplet extends Applet { private ECPrivateKey privateKey; private Crypto crypto; - private SECP256k1 secp256k1; private Signature signature; @@ -40,17 +39,15 @@ public class CashApplet extends Applet { */ public CashApplet(byte[] bArray, short bOffset, byte bLength) { crypto = new Crypto(); - secp256k1 = new SECP256k1(); keypair = new KeyPair(KeyPair.ALG_EC_FP, SECP256k1.SECP256K1_KEY_SIZE); publicKey = (ECPublicKey) keypair.getPublic(); privateKey = (ECPrivateKey) keypair.getPrivate(); - secp256k1.setCurveParameters(publicKey); - secp256k1.setCurveParameters(privateKey); + SECP256k1.setCurveParameters(publicKey); + SECP256k1.setCurveParameters(privateKey); keypair.genKeyPair(); signature = Signature.getInstance(Signature.ALG_ECDSA_SHA_256, false); - signature.init(privateKey, Signature.MODE_SIGN); short c9Off = (short)(bOffset + bArray[bOffset] + 1); // Skip AID c9Off += (short)(bArray[c9Off] + 1); // Skip Privileges and parameter length @@ -129,6 +126,7 @@ public class CashApplet extends Applet { outLen += 5; short sigOff = (short) (SIGN_OUT_OFF + outLen); + signature.init(privateKey, Signature.MODE_SIGN); outLen += signature.signPreComputedHash(apduBuffer, ISO7816.OFFSET_CDATA, MessageDigest.LENGTH_SHA_256, apduBuffer, sigOff); outLen += crypto.fixS(apduBuffer, sigOff); diff --git a/src/main/java/im/status/keycard/IdentApplet.java b/src/main/java/im/status/keycard/IdentApplet.java new file mode 100644 index 0000000..c1e20f5 --- /dev/null +++ b/src/main/java/im/status/keycard/IdentApplet.java @@ -0,0 +1,91 @@ +package im.status.keycard; + +import javacard.framework.*; +import javacard.security.*; + +/** + * The applet's main class. All incoming commands a processed by this class. + */ +public class IdentApplet extends Applet { + /** + * Invoked during applet installation. Creates an instance of this class. The installation parameters are passed in + * the given buffer. + * + * @param bArray installation parameters buffer + * @param bOffset offset where the installation parameters begin + * @param bLength length of the installation parameters + */ + public static void install(byte[] bArray, short bOffset, byte bLength) { + new IdentApplet(bArray, bOffset, bLength); + } + + /** + * Application constructor. All memory allocation is done here. The reason for this is two-fold: first the card might + * not have Garbage Collection so dynamic allocation will eventually eat all memory. The second reason is to be sure + * that if the application installs successfully, there is no risk of running out of memory because of other applets + * allocating memory. The constructor also registers the applet with the JCRE so that it becomes selectable. + * + * @param bArray installation parameters buffer + * @param bOffset offset where the installation parameters begin + * @param bLength length of the installation parameters + */ + public IdentApplet(byte[] bArray, short bOffset, byte bLength) { + SharedMemory.idPrivate = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, SECP256k1.SECP256K1_KEY_SIZE, false); + SECP256k1.setCurveParameters(SharedMemory.idPrivate); + SharedMemory.idCert[0] = 0; + register(bArray, (short) (bOffset + 1), bArray[bOffset]); + } + + /** + * This method is called on every incoming APDU. This method is just a dispatcher which invokes the correct method + * depending on the INS of the APDU. + * + * @param apdu the JCRE-owned APDU object. + * @throws ISOException any processing error + */ + public void process(APDU apdu) throws ISOException { + if (selectingApplet()) { + processSelect(apdu); + return; + } + + byte[] apduBuffer = apdu.getBuffer(); + + switch (apduBuffer[ISO7816.OFFSET_INS]) { + case KeycardApplet.INS_STORE_DATA: + processStoreData(apdu); + break; + default: + ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); + break; + } + } + + private void processSelect(APDU apdu) { + byte[] apduBuffer = apdu.getBuffer(); + apdu.setIncomingAndReceive(); + + if (SharedMemory.idCert[0] == SharedMemory.CERT_VALID) { + Util.arrayCopyNonAtomic(SharedMemory.idCert, (short) 1, apduBuffer, (short) 0, SharedMemory.CERT_LEN); + apdu.setOutgoingAndSend((short) 0, SharedMemory.CERT_LEN); + } + + } + + private void processStoreData(APDU apdu) { + if (SharedMemory.idCert[0] == SharedMemory.CERT_VALID) { + ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); + } + + byte[] apduBuffer = apdu.getBuffer(); + apdu.setIncomingAndReceive(); + + if (Util.makeShort((byte) 0, apduBuffer[ISO7816.OFFSET_LC]) != (SharedMemory.CERT_LEN + Crypto.KEY_SECRET_SIZE)) { + ISOException.throwIt(ISO7816.SW_WRONG_DATA); + } + + Util.arrayCopyNonAtomic(apduBuffer, ISO7816.OFFSET_CDATA, SharedMemory.idCert, (short) 1, SharedMemory.CERT_LEN); + SharedMemory.idPrivate.setS(apduBuffer, (short) (ISO7816.OFFSET_CDATA + SharedMemory.CERT_LEN), Crypto.KEY_SECRET_SIZE); + SharedMemory.idCert[0] = SharedMemory.CERT_VALID; + } +} diff --git a/src/main/java/im/status/keycard/KeycardApplet.java b/src/main/java/im/status/keycard/KeycardApplet.java index ade23f2..f6fd1f3 100644 --- a/src/main/java/im/status/keycard/KeycardApplet.java +++ b/src/main/java/im/status/keycard/KeycardApplet.java @@ -13,6 +13,7 @@ public class KeycardApplet extends Applet { static final byte INS_GET_STATUS = (byte) 0xF2; static final byte INS_INIT = (byte) 0xFE; + static final byte INS_IDENTIFY_CARD = (byte) 0x14; static final byte INS_VERIFY_PIN = (byte) 0x20; static final byte INS_CHANGE_PIN = (byte) 0x21; static final byte INS_UNBLOCK_PIN = (byte) 0x22; @@ -98,6 +99,7 @@ public class KeycardApplet extends Applet { static final byte TLV_UID = (byte) 0x8F; static final byte TLV_KEY_UID = (byte) 0x8E; static final byte TLV_CAPABILITIES = (byte) 0x8D; + static final byte TLV_CERT = (byte) 0x8A; static final byte CAPABILITY_SECURE_CHANNEL = (byte) 0x01; static final byte CAPABILITY_KEY_MANAGEMENT = (byte) 0x02; @@ -247,6 +249,9 @@ public class KeycardApplet extends Applet { case SecureChannel.INS_UNPAIR: unpair(apdu); break; + case INS_IDENTIFY_CARD: + identifyCard(apdu); + break; case INS_GET_STATUS: getStatus(apdu); break; @@ -363,6 +368,8 @@ public class KeycardApplet extends Applet { puk.update(apduBuffer, (short)(ISO7816.OFFSET_CDATA + PIN_LENGTH), PUK_LENGTH); JCSystem.commitTransaction(); + } else if (apduBuffer[ISO7816.OFFSET_INS] == INS_IDENTIFY_CARD) { + identifyCard(apdu); } else { ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); } @@ -472,6 +479,53 @@ public class KeycardApplet extends Applet { secureChannel.respond(apdu, len, ISO7816.SW_NO_ERROR); } + /** + * Processes the IDENTIFY CARD command according to the application's specifications. + * + * @param apdu the JCRE-owned APDU object. + */ + private void identifyCard(APDU apdu) { + byte[] apduBuffer = apdu.getBuffer(); + + short len; + + if (secureChannel.isOpen()) { + len = secureChannel.preprocessAPDU(apduBuffer); + } else { + len = (short) (apduBuffer[ISO7816.OFFSET_LC] & (short) 0xff); + } + + if (SharedMemory.idCert[0] != SharedMemory.CERT_VALID) { + ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); + } + + if (len != MessageDigest.LENGTH_SHA_256) { + ISOException.throwIt(ISO7816.SW_WRONG_DATA); + } + + short off = SecureChannel.SC_OUT_OFFSET; + apduBuffer[off++] = TLV_SIGNATURE_TEMPLATE; + apduBuffer[off++] = (byte) 0x81; + off++; + apduBuffer[off++] = TLV_CERT; + apduBuffer[off++] = (byte) SharedMemory.CERT_LEN; + Util.arrayCopyNonAtomic(SharedMemory.idCert, (short) 1, apduBuffer, off, SharedMemory.CERT_LEN); + off += SharedMemory.CERT_LEN; + + short outLen = (short)(SharedMemory.CERT_LEN + 5); + signature.init(SharedMemory.idPrivate, Signature.MODE_SIGN); + outLen += signature.signPreComputedHash(apduBuffer, ISO7816.OFFSET_CDATA, MessageDigest.LENGTH_SHA_256, apduBuffer, off); + + apduBuffer[(short)(SecureChannel.SC_OUT_OFFSET + 2)] = (byte)(outLen - 3); + + if (secureChannel.isOpen()) { + secureChannel.respond(apdu, outLen, ISO7816.SW_NO_ERROR); + } else { + apdu.setOutgoingAndSend(SecureChannel.SC_OUT_OFFSET, outLen); + } + + } + /** * Writes the Application Status Template to the APDU buffer. Invoked internally by the getStatus method. This * template is useful to understand if the card is blocked, if it has valid keys and if public key derivation is @@ -685,7 +739,7 @@ public class KeycardApplet extends Applet { */ private void resetKeyStatus() { parentPrivateKey.clearKey(); - secp256k1.setCurveParameters(parentPrivateKey); + SECP256k1.setCurveParameters(parentPrivateKey); keyPathLen = 0; } @@ -1437,16 +1491,16 @@ public class KeycardApplet extends Applet { * Set curve parameters to cleared keys */ private void resetCurveParameters() { - secp256k1.setCurveParameters(masterPublic); - secp256k1.setCurveParameters(masterPrivate); + SECP256k1.setCurveParameters(masterPublic); + SECP256k1.setCurveParameters(masterPrivate); - secp256k1.setCurveParameters(parentPublicKey); - secp256k1.setCurveParameters(parentPrivateKey); + SECP256k1.setCurveParameters(parentPublicKey); + SECP256k1.setCurveParameters(parentPrivateKey); - secp256k1.setCurveParameters(publicKey); - secp256k1.setCurveParameters(privateKey); + SECP256k1.setCurveParameters(publicKey); + SECP256k1.setCurveParameters(privateKey); - secp256k1.setCurveParameters(pinlessPublicKey); - secp256k1.setCurveParameters(pinlessPrivateKey); + 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 5f53de7..4fe7c9e 100644 --- a/src/main/java/im/status/keycard/SECP256k1.java +++ b/src/main/java/im/status/keycard/SECP256k1.java @@ -70,7 +70,7 @@ public class SECP256k1 { * * @param key the key where the curve parameters must be set */ - void setCurveParameters(ECKey key) { + static void setCurveParameters(ECKey key) { key.setA(SECP256K1_A, (short) 0x00, (short) SECP256K1_A.length); key.setB(SECP256K1_B, (short) 0x00, (short) SECP256K1_B.length); key.setFieldFP(SECP256K1_FP, (short) 0x00, (short) SECP256K1_FP.length); diff --git a/src/main/java/im/status/keycard/SecureChannel.java b/src/main/java/im/status/keycard/SecureChannel.java index dbaf38a..df9daf6 100644 --- a/src/main/java/im/status/keycard/SecureChannel.java +++ b/src/main/java/im/status/keycard/SecureChannel.java @@ -60,8 +60,8 @@ public class SecureChannel { pairingKeys = new byte[(short)(PAIRING_KEY_LENGTH * pairingLimit)]; scKeypair = new KeyPair(KeyPair.ALG_EC_FP, SC_KEY_LENGTH); - secp256k1.setCurveParameters((ECKey) scKeypair.getPrivate()); - secp256k1.setCurveParameters((ECKey) scKeypair.getPublic()); + SECP256k1.setCurveParameters((ECKey) scKeypair.getPrivate()); + SECP256k1.setCurveParameters((ECKey) scKeypair.getPublic()); scKeypair.genKeyPair(); remainingSlots = pairingLimit; diff --git a/src/main/java/im/status/keycard/SharedMemory.java b/src/main/java/im/status/keycard/SharedMemory.java index ea751b3..16593c0 100644 --- a/src/main/java/im/status/keycard/SharedMemory.java +++ b/src/main/java/im/status/keycard/SharedMemory.java @@ -1,12 +1,24 @@ package im.status.keycard; +import javacard.security.*; + /** * Keep references to data structures shared across applet instances of this package. */ class SharedMemory { + static final byte CERT_VALID = (byte) 0xAA; + static final short CERT_LEN = 98; + /** The NDEF data file. Read through the NDEFApplet. **/ static final byte[] ndefDataFile = new byte[SecureChannel.SC_MAX_PLAIN_LENGTH + 1]; /** The Cash data file. Read through the CashApplet. **/ static final byte[] cashDataFile = new byte[KeycardApplet.MAX_DATA_LENGTH + 1]; + + /** The identification private key **/ + static ECPrivateKey idPrivate = null; + + /** The certificate. It is the concatenation of: compressed id public key, CA signature. + * The signature is in the format r,s,v where v allows recovering the signer public key. */ + static final byte[] idCert = new byte[(short)(CERT_LEN + 1)]; } diff --git a/src/test/java/im/status/keycard/KeycardTest.java b/src/test/java/im/status/keycard/KeycardTest.java index fe589dc..a27d17f 100644 --- a/src/test/java/im/status/keycard/KeycardTest.java +++ b/src/test/java/im/status/keycard/KeycardTest.java @@ -4,6 +4,7 @@ import com.licel.jcardsim.smartcardio.CardSimulator; import com.licel.jcardsim.smartcardio.CardTerminalSimulator; import com.licel.jcardsim.utils.AIDUtil; import im.status.keycard.applet.*; +import im.status.keycard.applet.Certificate; import im.status.keycard.desktop.LedgerUSBManager; import im.status.keycard.desktop.PCSCCardChannel; import im.status.keycard.io.APDUCommand; @@ -56,6 +57,7 @@ public class KeycardTest { private static CardChannel apduChannel; private static im.status.keycard.io.CardChannel sdkChannel; private static CardSimulator simulator; + private static KeyPair caKeyPair; private static LedgerUSBManager usbManager; @@ -102,6 +104,8 @@ public class KeycardTest { throw new IllegalStateException("Unknown target"); } + caKeyPair = Certificate.generateIdentKeyPair(); + initIfNeeded(); } @@ -157,6 +161,15 @@ public class KeycardTest { simulator.installApplet(aid, CashApplet.class, bos.toByteArray(), (short) 0, (byte) bos.size()); bos.reset(); + // Install CashApplet + aid = AIDUtil.create(Identifiers.IDENT_AID); + bos.write(Identifiers.IDENT_INSTANCE_AID.length); + bos.write(Identifiers.IDENT_INSTANCE_AID); + bos.write(new byte[] {0x01, 0x00, 0x02, (byte) 0xC9, 0x00}); + + simulator.installApplet(aid, IdentApplet.class, bos.toByteArray(), (short) 0, (byte) bos.size()); + bos.reset(); + cardTerminal = CardTerminalSimulator.terminal(simulator); openPCSCChannel(); @@ -198,6 +211,12 @@ public class KeycardTest { } private static void initIfNeeded() throws Exception { + KeyPair identKeyPair = Certificate.generateIdentKeyPair(); + Certificate cert = Certificate.createCertificate(caKeyPair, identKeyPair); + IdentCommandSet idCmdSet = new IdentCommandSet(sdkChannel); + idCmdSet.select().checkOK(); + idCmdSet.storeData(cert.toStoreData()).checkOK(); + KeycardCommandSet cmdSet = new KeycardCommandSet(sdkChannel); cmdSet.select().checkOK(); @@ -255,6 +274,19 @@ public class KeycardTest { assertTrue(new ApplicationInfo(data).isInitializedCard()); } + @Test + @DisplayName("IDENT command") + void identTest() throws Exception { + byte[] challenge = new byte[32]; + Random random = new Random(); + random.nextBytes(challenge); + APDUResponse response = cmdSet.identifyCard(challenge); + assertEquals(0x9000, response.getSw()); + byte[] caPub = Certificate.verifyIdentity(challenge, response.getData()); + byte[] expectedCaPub = ((ECPublicKey) caKeyPair.getPublic()).getQ().getEncoded(true); + assertArrayEquals(expectedCaPub, caPub); + } + @Test @DisplayName("OPEN SECURE CHANNEL command") @Capabilities("secureChannel")