From 8a9d2758bfc1b2f9575a6d333506303927fa97fa Mon Sep 17 00:00:00 2001 From: Michele Balistreri Date: Tue, 26 Feb 2019 15:25:49 +0300 Subject: [PATCH] partial adaptation for Ledger --- README.md | 2 +- build.gradle | 19 +- .../java/im/status/keycard/KeycardTest.java | 224 +++++++++++++----- 3 files changed, 182 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index eb27322..5b298c0 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ log and there is no way to set breakpoints in the applet. In order to test with the simulator with an IDE, you need to pass these additional parameters to the JVM -```-noverify -Dim.status.keycard.test.simulated=true``` +```-noverify -Dim.status.keycard.test.target=simulator``` ## Compilation 1. Download and install the JavaCard 3.0.4 SDK from [Oracle](http://www.oracle.com/technetwork/java/javasebusiness/downloads/java-archive-downloads-javame-419430.html#java_card_kit-classic-3_0_4-rr-bin-do) diff --git a/build.gradle b/build.gradle index e6bb62c..cde559c 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,12 @@ javacard { } } +def testTarget = project.properties['im.status.keycard.test.target'] +if (!testTarget) { + testTarget = 'card' +} + + repositories { mavenCentral() maven { url 'https://jitpack.io' } @@ -43,7 +49,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:2.0.0') + testCompile('com.github.status-im.status-keycard-java:desktop:f9df2be') 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") @@ -63,7 +69,7 @@ task wrapper(type: Wrapper) { task install (type: im.status.keycard.build.InstallTask) -if (project.properties['im.status.keycard.test.simulated'] != 'true') { +if (testTarget == 'card') { tasks.install.dependsOn(convertJavacard) tasks.test.dependsOn(install) } @@ -79,8 +85,11 @@ compileTestJava { } afterEvaluate { - if (project.properties['im.status.keycard.test.simulated'] == 'true') { - def junitPlatformTestTask = tasks.getByName('junitPlatformTest') - junitPlatformTestTask.jvmArgs(['-noverify', '-Dim.status.keycard.test.simulated=true']) + def jvmArgs = ["-Dim.status.keycard.test.target=${testTarget}"] + if (testTarget == 'simulator') { + jvmArgs.push('-noverify') } + + def junitPlatformTestTask = tasks.getByName('junitPlatformTest') + junitPlatformTestTask.jvmArgs(jvmArgs) } diff --git a/src/test/java/im/status/keycard/KeycardTest.java b/src/test/java/im/status/keycard/KeycardTest.java index 56c77a3..8ef930e 100644 --- a/src/test/java/im/status/keycard/KeycardTest.java +++ b/src/test/java/im/status/keycard/KeycardTest.java @@ -6,8 +6,10 @@ import com.licel.jcardsim.utils.AIDUtil; import im.status.keycard.applet.ApplicationInfo; import im.status.keycard.applet.Identifiers; import im.status.keycard.applet.KeycardCommandSet; +import im.status.keycard.desktop.LedgerUSBManager; import im.status.keycard.desktop.PCSCCardChannel; import im.status.keycard.io.APDUResponse; +import im.status.keycard.io.CardListener; import javacard.framework.AID; import org.bitcoinj.core.ECKey; import org.bitcoinj.crypto.ChildNumber; @@ -52,41 +54,97 @@ public class KeycardTest { public static final byte[] SHARED_SECRET = Hex.decode("2194524CF8A99C34B9F6C6894D07245AA2D5CFE6327C27D5ACDCC95DA203ED28"); private static CardTerminal cardTerminal; private static CardChannel apduChannel; - private static PCSCCardChannel sdkChannel; + private static im.status.keycard.io.CardChannel sdkChannel; private static CardSimulator simulator; + private static LedgerUSBManager usbManager; + private TestSecureChannelSession secureChannel; private TestKeycardCommandSet cmdSet; - private static final boolean USE_SIMULATOR; + private static final int TARGET_SIMULATOR = 0; + private static final int TARGET_CARD = 1; + private static final int TARGET_LEDGERUSB = 2; + + private static final int TARGET; static { - USE_SIMULATOR = !System.getProperty("im.status.keycard.test.simulated", "false").equals("false"); + switch(System.getProperty("im.status.keycard.test.target", "card")) { + case "simulator": + TARGET = TARGET_SIMULATOR; + break; + case "card": + TARGET = TARGET_CARD; + break; + case "ledgerusb": + TARGET = TARGET_LEDGERUSB; + break; + default: + throw new RuntimeException("Unknown target"); + } } @BeforeAll static void initAll() throws Exception { - if (USE_SIMULATOR) { - simulator = new CardSimulator(); - AID appletAID = AIDUtil.create(Identifiers.getKeycardInstanceAID()); - simulator.installApplet(appletAID, KeycardApplet.class); - cardTerminal = CardTerminalSimulator.terminal(simulator); - } else { - TerminalFactory tf = TerminalFactory.getDefault(); + switch(TARGET) { + case TARGET_SIMULATOR: + openSimulatorChannel(); + break; + case TARGET_CARD: + openCardChannel(); + break; + case TARGET_LEDGERUSB: + openLedgerUSBChannel(); + break; + default: + throw new IllegalStateException("Unknown target"); + } - for (CardTerminal t : tf.terminals().list()) { - if (t.isCardPresent()) { - cardTerminal = t; - break; - } + initIfNeeded(); + } + + private static void openSimulatorChannel() throws Exception { + simulator = new CardSimulator(); + AID appletAID = AIDUtil.create(Identifiers.getKeycardInstanceAID()); + simulator.installApplet(appletAID, KeycardApplet.class); + cardTerminal = CardTerminalSimulator.terminal(simulator); + + openPCSCChannel(); + } + + private static void openCardChannel() throws Exception { + TerminalFactory tf = TerminalFactory.getDefault(); + + for (CardTerminal t : tf.terminals().list()) { + if (t.isCardPresent()) { + cardTerminal = t; + break; } } + openPCSCChannel(); + } + + private static void openPCSCChannel() throws Exception { Card apduCard = cardTerminal.connect("*"); apduChannel = apduCard.getBasicChannel(); sdkChannel = new PCSCCardChannel(apduChannel); + } - initIfNeeded(); + private static void openLedgerUSBChannel() { + usbManager = new LedgerUSBManager(new CardListener() { + @Override + public void onConnected(im.status.keycard.io.CardChannel channel) { + sdkChannel = channel; + } + + @Override + public void onDisconnected() { + throw new RuntimeException("Ledger was disconnected during test run!"); + } + }); + + usbManager.start(); } private static void initIfNeeded() throws IOException { @@ -104,19 +162,31 @@ public class KeycardTest { cmdSet.setSecureChannel(secureChannel); cmdSet.select().checkOK(); cmdSet.setSecureChannel(secureChannel); - cmdSet.autoPair(SHARED_SECRET); + + if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { + cmdSet.autoPair(SHARED_SECRET); + } } @AfterEach void tearDown() throws Exception { resetAndSelectAndOpenSC(); - APDUResponse response = cmdSet.verifyPIN("000000"); - assertEquals(0x9000, response.getSw()); - cmdSet.autoUnpair(); + + if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { + APDUResponse response = cmdSet.verifyPIN("000000"); + assertEquals(0x9000, response.getSw()); + } + + if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { + cmdSet.autoUnpair(); + } } @AfterAll static void tearDownAll() { + if (usbManager != null) { + usbManager.stop(); + } } @Test @@ -137,6 +207,7 @@ public class KeycardTest { @Test @DisplayName("OPEN SECURE CHANNEL command") + @Tag("secureChannel") void openSecureChannelTest() throws Exception { // Wrong P1 APDUResponse response = cmdSet.openSecureChannel((byte)(secureChannel.getPairingIndex() + 1), new byte[65]); @@ -184,6 +255,7 @@ public class KeycardTest { @Test @DisplayName("MUTUALLY AUTHENTICATE command") + @Tag("secureChannel") void mutuallyAuthenticateTest() throws Exception { // Mutual authentication before opening a Secure Channel APDUResponse response = cmdSet.mutuallyAuthenticate(); @@ -227,6 +299,7 @@ public class KeycardTest { @Test @DisplayName("PAIR command") + @Tag("secureChannel") void pairTest() throws Exception { // Wrong data length APDUResponse response = cmdSet.pair(SecureChannel.PAIR_P1_FIRST_STEP, new byte[31]); @@ -285,6 +358,7 @@ public class KeycardTest { @Test @DisplayName("UNPAIR command") + @Tag("secureChannel") void unpairTest() throws Exception { // Add a spare keyset byte sparePairingIndex = secureChannel.getPairingIndex(); @@ -356,6 +430,7 @@ public class KeycardTest { @Test @DisplayName("SET NDEF command") + @Tag("ndef") void setNDEFTest() throws Exception { byte[] ndefData = { (byte) 0x00, (byte) 0x24, (byte) 0xd4, (byte) 0x0f, (byte) 0x12, (byte) 0x61, (byte) 0x6e, (byte) 0x64, @@ -390,6 +465,7 @@ public class KeycardTest { @Test @DisplayName("VERIFY PIN command") + @Tag("credentialsManagement") void verifyPinTest() throws Exception { // Security condition violation: SecureChannel not open APDUResponse response = cmdSet.verifyPIN("000000"); @@ -425,6 +501,7 @@ public class KeycardTest { @Test @DisplayName("CHANGE PIN command") + @Tag("credentialsManagement") void changePinTest() throws Exception { // Security condition violation: SecureChannel not open APDUResponse response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "123456"); @@ -528,6 +605,7 @@ public class KeycardTest { @Test @DisplayName("UNBLOCK PIN command") + @Tag("credentialsManagement") void unblockPinTest() throws Exception { // Security condition violation: SecureChannel not open APDUResponse response = cmdSet.unblockPIN("123456789012", "000000"); @@ -577,6 +655,7 @@ public class KeycardTest { @Test @DisplayName("LOAD KEY command") + @Tag("keyManagement") void loadKeyTest() throws Exception { KeyPairGenerator g = keypairGenerator(); KeyPair keyPair = g.generateKeyPair(); @@ -605,7 +684,7 @@ public class KeycardTest { response = cmdSet.loadKey(new byte[]{(byte) 0xA1, 0x02, (byte) 0x80, 0x00}, KeycardApplet.LOAD_KEY_P1_EC); assertEquals(0x6A80, response.getSw()); - if (!USE_SIMULATOR) { // the simulator does not check the key format + if (TARGET != TARGET_SIMULATOR) { // the simulator does not check the key format response = cmdSet.loadKey(new byte[]{(byte) 0xA1, 0x06, (byte) 0x80, 0x01, 0x01, (byte) 0x81, 0x01, 0x02}, KeycardApplet.LOAD_KEY_P1_EC); assertEquals(0x6A80, response.getSw()); } @@ -640,6 +719,7 @@ public class KeycardTest { @Test @DisplayName("GENERATE MNEMONIC command") + @Tag("keyManagement") void generateMnemonicTest() throws Exception { // Security condition violation: SecureChannel not open APDUResponse response = cmdSet.generateMnemonic(4); @@ -677,6 +757,7 @@ public class KeycardTest { @Test @DisplayName("REMOVE KEY command") + @Tag("keyManagement") void removeKeyTest() throws Exception { KeyPairGenerator g = keypairGenerator(); KeyPair keyPair = g.generateKeyPair(); @@ -721,6 +802,7 @@ public class KeycardTest { @Test @DisplayName("GENERATE KEY command") + @Tag("keyManagement") void generateKeyTest() throws Exception { // Security condition violation: SecureChannel not open APDUResponse response = cmdSet.generateKey(); @@ -750,32 +832,40 @@ public class KeycardTest { @Test @DisplayName("DERIVE KEY command") void deriveKeyTest() throws Exception { - // Security condition violation: SecureChannel not open - APDUResponse response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x00}); - assertEquals(0x6985, response.getSw()); + APDUResponse response; - cmdSet.autoOpenSecureChannel(); + if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { + // Security condition violation: SecureChannel not open + response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x00}); + assertEquals(0x6985, response.getSw()); - // Security condition violation: PIN is not verified - response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x00}); - assertEquals(0x6985, response.getSw()); + cmdSet.autoOpenSecureChannel(); + } - response = cmdSet.verifyPIN("000000"); - assertEquals(0x9000, response.getSw()); + if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { + // 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()); + if (cmdSet.getApplicationInfo().hasKeyManagementCapability()) { + // 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()); + response = cmdSet.loadKey(keyPair, false, chainCode); + assertEquals(0x9000, response.getSw()); + } // Wrong data format response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00}); @@ -828,25 +918,33 @@ public class KeycardTest { byte[] data = "some data to be hashed".getBytes(); byte[] hash = sha256(data); - // Security condition violation: SecureChannel not open - APDUResponse response = cmdSet.sign(hash); - assertEquals(0x6985, response.getSw()); + APDUResponse response; - cmdSet.autoOpenSecureChannel(); + if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { + // Security condition violation: SecureChannel not open + response = cmdSet.sign(hash); + assertEquals(0x6985, response.getSw()); - // Security condition violation: PIN not verified - response = cmdSet.sign(hash); - assertEquals(0x6985, response.getSw()); + cmdSet.autoOpenSecureChannel(); + } - response = cmdSet.verifyPIN("000000"); - assertEquals(0x9000, response.getSw()); + if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { + // Security condition violation: PIN not verified + response = cmdSet.sign(hash); + assertEquals(0x6985, response.getSw()); + + response = cmdSet.verifyPIN("000000"); + assertEquals(0x9000, response.getSw()); + } - KeyPair keyPair = keypairGenerator().generateKeyPair(); Signature signature = Signature.getInstance("SHA256withECDSA", "BC"); - signature.initVerify(keyPair.getPublic()); - response = cmdSet.loadKey(keyPair); - assertEquals(0x9000, response.getSw()); + if (cmdSet.getApplicationInfo().hasKeyManagementCapability()) { + KeyPair keyPair = keypairGenerator().generateKeyPair(); + signature.initVerify(keyPair.getPublic()); + response = cmdSet.loadKey(keyPair); + assertEquals(0x9000, response.getSw()); + } // Wrong Data length response = cmdSet.sign(data); @@ -1011,6 +1109,7 @@ public class KeycardTest { @Test @DisplayName("DUPLICATE KEY command") + @Tag("keyManagement") void duplicateTest() throws Exception { int secretCount = 5; Random random = new Random(); @@ -1230,17 +1329,24 @@ public class KeycardTest { } private void reset() { - if (USE_SIMULATOR) { - simulator.reset(); - } else { - apduChannel.getCard().getATR(); + switch(TARGET) { + case TARGET_SIMULATOR: + simulator.reset(); + break; + case TARGET_CARD: + apduChannel.getCard().getATR(); + break; + default: + break; } } private void resetAndSelectAndOpenSC() throws Exception { - reset(); - cmdSet.select(); - cmdSet.autoOpenSecureChannel(); + if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { + reset(); + cmdSet.select(); + cmdSet.autoOpenSecureChannel(); + } } private void assertMnemonic(int expectedLength, byte[] data) { @@ -1274,6 +1380,10 @@ public class KeycardTest { } private void verifyKeyDerivation(KeyPair keyPair, byte[] chainCode, int[] path) throws Exception { + if (!cmdSet.getApplicationInfo().hasKeyManagementCapability()) { + return; + } + DeterministicKey key = deriveKey(keyPair, chainCode, path); byte[] hash = Hash.sha3(new byte[8]);