1917 lines
70 KiB
Java
1917 lines
70 KiB
Java
package im.status.keycard;
|
|
|
|
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;
|
|
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;
|
|
import org.bitcoinj.crypto.DeterministicKey;
|
|
import org.bitcoinj.crypto.HDKeyDerivation;
|
|
import org.bouncycastle.jce.ECNamedCurveTable;
|
|
import org.bouncycastle.jce.spec.ECParameterSpec;
|
|
import org.bouncycastle.jce.spec.ECPublicKeySpec;
|
|
import org.bouncycastle.util.encoders.Hex;
|
|
import org.junit.jupiter.api.*;
|
|
import org.web3j.crypto.*;
|
|
import org.web3j.protocol.Web3j;
|
|
import org.web3j.protocol.core.DefaultBlockParameterName;
|
|
import org.web3j.protocol.core.methods.request.RawTransaction;
|
|
import org.web3j.protocol.core.methods.response.EthSendTransaction;
|
|
import org.web3j.protocol.http.HttpService;
|
|
import org.web3j.tx.Transfer;
|
|
import org.web3j.utils.Convert;
|
|
import org.web3j.utils.Numeric;
|
|
|
|
import javax.smartcardio.*;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.lang.reflect.Constructor;
|
|
import java.lang.reflect.Field;
|
|
import java.lang.reflect.Method;
|
|
import java.math.BigDecimal;
|
|
import java.math.BigInteger;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.ByteOrder;
|
|
import java.security.*;
|
|
|
|
import org.bouncycastle.jce.interfaces.ECPublicKey;
|
|
|
|
import java.util.Arrays;
|
|
import java.util.HashSet;
|
|
import java.util.Random;
|
|
|
|
import static org.apache.commons.codec.digest.DigestUtils.sha256;
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
|
|
@DisplayName("Test the Keycard Applet")
|
|
public class KeycardTest {
|
|
// Pairing key is KeycardTest
|
|
private static CardTerminal cardTerminal;
|
|
private static CardChannel apduChannel;
|
|
private static im.status.keycard.io.CardChannel sdkChannel;
|
|
private static CardSimulator simulator;
|
|
private static KeyPair caKeyPair;
|
|
|
|
private static LedgerUSBManager usbManager;
|
|
|
|
private static byte[] sharedSecret;
|
|
|
|
private TestSecureChannelSession secureChannel;
|
|
private TestKeycardCommandSet cmdSet;
|
|
|
|
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 {
|
|
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 {
|
|
switch(TARGET) {
|
|
case TARGET_SIMULATOR:
|
|
openSimulatorChannel();
|
|
break;
|
|
case TARGET_CARD:
|
|
openCardChannel();
|
|
break;
|
|
case TARGET_LEDGERUSB:
|
|
openLedgerUSBChannel();
|
|
break;
|
|
default:
|
|
throw new IllegalStateException("Unknown target");
|
|
}
|
|
|
|
caKeyPair = Certificate.generateIdentKeyPair();
|
|
|
|
initIfNeeded();
|
|
}
|
|
|
|
private static void initCapabilities(ApplicationInfo info) {
|
|
HashSet<String> capabilities = new HashSet<>();
|
|
|
|
if (info.hasSecureChannelCapability()) {
|
|
capabilities.add("secureChannel");
|
|
}
|
|
|
|
if (info.hasCredentialsManagementCapability()) {
|
|
capabilities.add("credentialsManagement");
|
|
}
|
|
|
|
if (info.hasKeyManagementCapability()) {
|
|
capabilities.add("keyManagement");
|
|
}
|
|
|
|
if (info.hasNDEFCapability()) {
|
|
capabilities.add("ndef");
|
|
}
|
|
|
|
if (info.hasFactoryResetCapability()) {
|
|
capabilities.add("factoryReset");
|
|
}
|
|
|
|
CapabilityCondition.availableCapabilities = capabilities;
|
|
}
|
|
|
|
private static void openSimulatorChannel() throws Exception {
|
|
simulator = new CardSimulator();
|
|
|
|
// Install KeycardApplet
|
|
AID aid = AIDUtil.create(Identifiers.KEYCARD_AID);
|
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
|
bos.write(Identifiers.getKeycardInstanceAID().length);
|
|
bos.write(Identifiers.getKeycardInstanceAID());
|
|
|
|
simulator.installApplet(aid, KeycardApplet.class, bos.toByteArray(), (short) 0, (byte) bos.size());
|
|
bos.reset();
|
|
|
|
// Install NDEFApplet
|
|
aid = AIDUtil.create(Identifiers.NDEF_AID);
|
|
bos.write(Identifiers.NDEF_INSTANCE_AID.length);
|
|
bos.write(Identifiers.NDEF_INSTANCE_AID);
|
|
bos.write(new byte[] {0x01, 0x00, 0x02, (byte) 0xC9, 0x00});
|
|
|
|
simulator.installApplet(aid, NDEFApplet.class, bos.toByteArray(), (short) 0, (byte) bos.size());
|
|
bos.reset();
|
|
|
|
// Install CashApplet
|
|
aid = AIDUtil.create(Identifiers.CASH_AID);
|
|
bos.write(Identifiers.CASH_INSTANCE_AID.length);
|
|
bos.write(Identifiers.CASH_INSTANCE_AID);
|
|
bos.write(new byte[] {0x01, 0x00, 0x02, (byte) 0xC9, 0x00});
|
|
|
|
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();
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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 initCard(KeycardCommandSet cmdSet) throws Exception {
|
|
assertEquals(0x9000, cmdSet.init("000000", "024680", "012345678901", sharedSecret, (byte) 3, (byte) 5).getSw());
|
|
cmdSet.select().checkOK();
|
|
}
|
|
|
|
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();
|
|
|
|
initCapabilities(cmdSet.getApplicationInfo());
|
|
|
|
sharedSecret = cmdSet.pairingPasswordToSecret(System.getProperty("im.status.keycard.test.pairing", "KeycardDefaultPairing"));
|
|
|
|
if (!cmdSet.getApplicationInfo().isInitializedCard()) {
|
|
initCard(cmdSet);
|
|
initCapabilities(cmdSet.getApplicationInfo());
|
|
}
|
|
}
|
|
|
|
@BeforeEach
|
|
void init() throws Exception {
|
|
reset();
|
|
cmdSet = new TestKeycardCommandSet(sdkChannel);
|
|
secureChannel = new TestSecureChannelSession();
|
|
cmdSet.setSecureChannel(secureChannel);
|
|
cmdSet.select().checkOK();
|
|
|
|
if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) {
|
|
cmdSet.autoPair(sharedSecret);
|
|
}
|
|
}
|
|
|
|
@AfterEach
|
|
void tearDown() throws Exception {
|
|
resetAndSelectAndOpenSC();
|
|
|
|
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
|
|
@DisplayName("SELECT command")
|
|
void selectTest() throws Exception {
|
|
APDUResponse response = cmdSet.select();
|
|
assertEquals(0x9000, response.getSw());
|
|
byte[] data = response.getData();
|
|
assertTrue(new ApplicationInfo(data).isInitializedCard());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("IDENT command")
|
|
void identTest() throws Exception {
|
|
APDUResponse response = cmdSet.identifyCard(new byte[33]);
|
|
assertEquals(0x6a80, response.getSw());
|
|
|
|
byte[] challenge = new byte[32];
|
|
Random random = new Random();
|
|
byte[] expectedCaPub = ((ECPublicKey) caKeyPair.getPublic()).getQ().getEncoded(true);
|
|
|
|
|
|
random.nextBytes(challenge);
|
|
response = cmdSet.identifyCard(challenge);
|
|
assertEquals(0x9000, response.getSw());
|
|
byte[] caPub = Certificate.verifyIdentity(challenge, response.getData());
|
|
assertArrayEquals(expectedCaPub, caPub);
|
|
|
|
cmdSet.autoOpenSecureChannel();
|
|
|
|
random.nextBytes(challenge);
|
|
response = cmdSet.identifyCard(challenge);
|
|
assertEquals(0x9000, response.getSw());
|
|
caPub = Certificate.verifyIdentity(challenge, response.getData());
|
|
assertArrayEquals(expectedCaPub, caPub);
|
|
|
|
random.nextBytes(challenge);
|
|
CashCommandSet cashCmdSet = new CashCommandSet(sdkChannel);
|
|
response = cashCmdSet.select();
|
|
assertEquals(0x9000, response.getSw());
|
|
response = cashCmdSet.identifyCard(challenge);
|
|
assertEquals(0x9000, response.getSw());
|
|
caPub = Certificate.verifyIdentity(challenge, response.getData());
|
|
assertArrayEquals(expectedCaPub, caPub);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("OPEN SECURE CHANNEL command")
|
|
@Capabilities("secureChannel")
|
|
void openSecureChannelTest() throws Exception {
|
|
// Wrong P1
|
|
APDUResponse response = cmdSet.openSecureChannel((byte)(secureChannel.getPairingIndex() + 1), new byte[65]);
|
|
assertEquals(0x6A86, response.getSw());
|
|
|
|
// Wrong data
|
|
response = cmdSet.openSecureChannel(secureChannel.getPairingIndex(), new byte[66]);
|
|
assertEquals(0x6A80, response.getSw());
|
|
|
|
// Good case
|
|
response = cmdSet.openSecureChannel(secureChannel.getPairingIndex(), secureChannel.getPublicKey());
|
|
assertEquals(0x9000, response.getSw());
|
|
assertEquals(SecureChannel.SC_SECRET_LENGTH + SecureChannel.SC_BLOCK_SIZE, response.getData().length);
|
|
secureChannel.processOpenSecureChannelResponse(response);
|
|
|
|
// Send command before MUTUALLY AUTHENTICATE
|
|
secureChannel.reset();
|
|
response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_APPLICATION);
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
// Perform mutual authentication
|
|
secureChannel.setOpen();
|
|
response = cmdSet.mutuallyAuthenticate();
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
try {
|
|
secureChannel.verifyMutuallyAuthenticateResponse(response);
|
|
} catch (Exception e) {
|
|
fail("invalid mutually authenticate response");
|
|
}
|
|
|
|
// Verify that the channel is open
|
|
response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_APPLICATION);
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("MUTUALLY AUTHENTICATE command")
|
|
@Capabilities("secureChannel")
|
|
void mutuallyAuthenticateTest() throws Exception {
|
|
// Mutual authentication before opening a Secure Channel
|
|
APDUResponse response = cmdSet.mutuallyAuthenticate();
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
response = cmdSet.openSecureChannel(secureChannel.getPairingIndex(), secureChannel.getPublicKey());
|
|
assertEquals(0x9000, response.getSw());
|
|
secureChannel.processOpenSecureChannelResponse(response);
|
|
|
|
// Wrong data format
|
|
response = cmdSet.mutuallyAuthenticate(new byte[31]);
|
|
assertEquals(0x6982, response.getSw());
|
|
|
|
// Verify that after wrong authentication, the command does not work
|
|
response = cmdSet.mutuallyAuthenticate();
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
// Wrong authentication data
|
|
response = cmdSet.openSecureChannel(secureChannel.getPairingIndex(), secureChannel.getPublicKey());
|
|
assertEquals(0x9000, response.getSw());
|
|
secureChannel.processOpenSecureChannelResponse(response);
|
|
APDUResponse resp2 = sdkChannel.send(new APDUCommand(0x80, SecureChannel.INS_MUTUALLY_AUTHENTICATE, 0, 0, new byte[48]));
|
|
assertEquals(0x6982, resp2.getSw());
|
|
secureChannel.reset();
|
|
response = cmdSet.mutuallyAuthenticate();
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
// Good case
|
|
cmdSet.autoOpenSecureChannel();
|
|
|
|
// MUTUALLY AUTHENTICATE has no effect on an already open secure channel
|
|
response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_APPLICATION);
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
response = cmdSet.mutuallyAuthenticate();
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_APPLICATION);
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("PAIR command")
|
|
@Capabilities("secureChannel")
|
|
void pairTest() throws Exception {
|
|
// Wrong data length
|
|
APDUResponse response = cmdSet.pair(SecureChannel.PAIR_P1_FIRST_STEP, new byte[31]);
|
|
assertEquals(0x6A80, response.getSw());
|
|
|
|
// Wrong P1
|
|
response = cmdSet.pair(SecureChannel.PAIR_P1_LAST_STEP, new byte[32]);
|
|
assertEquals(0x6A86, response.getSw());
|
|
|
|
// Wrong client cryptogram
|
|
byte[] challenge = new byte[32];
|
|
Random random = new Random();
|
|
random.nextBytes(challenge);
|
|
response = cmdSet.pair(SecureChannel.PAIR_P1_FIRST_STEP, challenge);
|
|
assertEquals(0x9000, response.getSw());
|
|
response = cmdSet.pair(SecureChannel.PAIR_P1_LAST_STEP, challenge);
|
|
assertEquals(0x6982, response.getSw());
|
|
|
|
// Interrupt session
|
|
random.nextBytes(challenge);
|
|
response = cmdSet.pair(SecureChannel.PAIR_P1_FIRST_STEP, challenge);
|
|
assertEquals(0x9000, response.getSw());
|
|
cmdSet.openSecureChannel(secureChannel.getPairingIndex(), secureChannel.getPublicKey());
|
|
response = cmdSet.pair(SecureChannel.PAIR_P1_LAST_STEP, challenge);
|
|
assertEquals(0x6A86, response.getSw());
|
|
|
|
// Open secure channel
|
|
cmdSet.autoOpenSecureChannel();
|
|
response = cmdSet.pair(SecureChannel.PAIR_P1_FIRST_STEP, challenge);
|
|
assertTrue((0x6985 == response.getSw()) || (0x6982 == response.getSw()));
|
|
cmdSet.openSecureChannel(secureChannel.getPairingIndex(), secureChannel.getPublicKey());
|
|
|
|
// Pair multiple indexes
|
|
for (int i = 1; i < KeycardApplet.PAIRING_MAX_CLIENT_COUNT; i++) {
|
|
cmdSet.autoPair(sharedSecret);
|
|
assertEquals(i, secureChannel.getPairingIndex());
|
|
cmdSet.autoOpenSecureChannel();
|
|
cmdSet.openSecureChannel(secureChannel.getPairingIndex(), secureChannel.getPublicKey());
|
|
}
|
|
|
|
// Too many paired indexes
|
|
response = cmdSet.pair(SecureChannel.PAIR_P1_FIRST_STEP, challenge);
|
|
assertEquals(0x6A84, response.getSw());
|
|
|
|
// Unpair all (except the last, which will be unpaired in the tearDown phase)
|
|
cmdSet.autoOpenSecureChannel();
|
|
|
|
if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) {
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
for (byte i = 0; i < (KeycardApplet.PAIRING_MAX_CLIENT_COUNT - 1); i++) {
|
|
response = cmdSet.unpair(i);
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("UNPAIR command")
|
|
@Capabilities("secureChannel")
|
|
void unpairTest() throws Exception {
|
|
// Add a spare keyset
|
|
byte sparePairingIndex = secureChannel.getPairingIndex();
|
|
cmdSet.autoPair(sharedSecret);
|
|
|
|
// Proof that the old keyset is still usable
|
|
APDUResponse response = cmdSet.openSecureChannel(sparePairingIndex, secureChannel.getPublicKey());
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// Security condition violation: SecureChannel not open
|
|
response = cmdSet.unpair(sparePairingIndex);
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
// Not authenticated
|
|
cmdSet.autoOpenSecureChannel();
|
|
|
|
if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) {
|
|
response = cmdSet.unpair(sparePairingIndex);
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
// Wrong P1
|
|
response = cmdSet.unpair((byte) 5);
|
|
assertEquals(0x6A86, response.getSw());
|
|
|
|
// Unpair spare keyset
|
|
response = cmdSet.unpair(sparePairingIndex);
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// Proof that unpaired is not usable
|
|
response = cmdSet.openSecureChannel(sparePairingIndex, secureChannel.getPublicKey());
|
|
assertEquals(0x6A86, response.getSw());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("GET STATUS command")
|
|
void getStatusTest() throws Exception {
|
|
APDUResponse response;
|
|
|
|
if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) {
|
|
// Security condition violation: SecureChannel not open
|
|
response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_APPLICATION);
|
|
assertEquals(0x6985, response.getSw());
|
|
cmdSet.autoOpenSecureChannel();
|
|
}
|
|
|
|
// Good case. Since the order of test execution is undefined, the test cannot know if the keys are initialized or not.
|
|
// Additionally, support for public key derivation is hw dependent.
|
|
response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_APPLICATION);
|
|
assertEquals(0x9000, response.getSw());
|
|
ApplicationStatus status = new ApplicationStatus(response.getData());
|
|
|
|
if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) {
|
|
assertEquals(3, status.getPINRetryCount());
|
|
assertEquals(5, status.getPUKRetryCount());
|
|
|
|
response = cmdSet.verifyPIN("123456");
|
|
assertEquals(0x63C2, response.getSw());
|
|
response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_APPLICATION);
|
|
assertEquals(0x9000, response.getSw());
|
|
status = new ApplicationStatus(response.getData());
|
|
assertEquals(2, status.getPINRetryCount());
|
|
assertEquals(5, status.getPUKRetryCount());
|
|
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_APPLICATION);
|
|
assertEquals(0x9000, response.getSw());
|
|
status = new ApplicationStatus(response.getData());
|
|
assertEquals(3, status.getPINRetryCount());
|
|
assertEquals(5, status.getPUKRetryCount());
|
|
} else {
|
|
assertEquals((byte) 0xff, status.getPINRetryCount());
|
|
assertEquals((byte) 0xff, status.getPUKRetryCount());
|
|
}
|
|
|
|
// Check that key path is valid
|
|
response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_KEY_PATH);
|
|
assertEquals(0x9000, response.getSw());
|
|
KeyPath path = new KeyPath(response.getData());
|
|
assertNotEquals(null, path);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("VERIFY PIN command")
|
|
@Capabilities("credentialsManagement")
|
|
void verifyPinTest() throws Exception {
|
|
// Security condition violation: SecureChannel not open
|
|
APDUResponse response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
cmdSet.autoOpenSecureChannel();
|
|
|
|
// Wrong format
|
|
response = cmdSet.verifyPIN("12345");
|
|
assertEquals(0x6a80, response.getSw());
|
|
|
|
response = cmdSet.verifyPIN("12345a");
|
|
assertEquals(0x6a80, response.getSw());
|
|
|
|
// Wrong PIN
|
|
response = cmdSet.verifyPIN("123456");
|
|
assertEquals(0x63C2, response.getSw());
|
|
|
|
// Correct PIN
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// Alt PIN
|
|
response = cmdSet.verifyPIN("024680");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// Check max retry counter
|
|
response = cmdSet.verifyPIN("123456");
|
|
assertEquals(0x63C2, response.getSw());
|
|
|
|
response = cmdSet.verifyPIN("123456");
|
|
assertEquals(0x63C1, response.getSw());
|
|
|
|
response = cmdSet.verifyPIN("123456");
|
|
assertEquals(0x63C0, response.getSw());
|
|
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x63C0, response.getSw());
|
|
|
|
response = cmdSet.verifyPIN("024680");
|
|
assertEquals(0x63C0, response.getSw());
|
|
|
|
// Unblock PIN to make further tests possible
|
|
response = cmdSet.unblockPIN("012345678901", "024680");
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("CHANGE PIN command")
|
|
@Capabilities("credentialsManagement")
|
|
void changePinTest() throws Exception {
|
|
// Security condition violation: SecureChannel not open
|
|
APDUResponse response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "123456");
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
cmdSet.autoOpenSecureChannel();
|
|
|
|
// Security condition violation: PIN not verified
|
|
response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "123456");
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// Wrong P1
|
|
response = cmdSet.changePIN(0x03, "123456");
|
|
assertEquals(0x6a86, response.getSw());
|
|
|
|
// Test wrong PIN formats (non-digits, too short, too long)
|
|
response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "654a21");
|
|
assertEquals(0x6A80, response.getSw());
|
|
|
|
response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "54321");
|
|
assertEquals(0x6A80, response.getSw());
|
|
|
|
response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "7654321");
|
|
assertEquals(0x6A80, response.getSw());
|
|
|
|
// Test wrong PUK formats
|
|
response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_PUK, "210987654a21");
|
|
assertEquals(0x6A80, response.getSw());
|
|
|
|
response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_PUK, "10987654321");
|
|
assertEquals(0x6A80, response.getSw());
|
|
|
|
response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_PUK, "3210987654321");
|
|
assertEquals(0x6A80, response.getSw());
|
|
|
|
// Test wrong pairing secret format (too long, too short)
|
|
response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_PAIRING_SECRET, "abcdefghilmnopqrstuvz123456789012");
|
|
assertEquals(0x6A80, response.getSw());
|
|
|
|
response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_PAIRING_SECRET, "abcdefghilmnopqrstuvz1234567890");
|
|
assertEquals(0x6A80, response.getSw());
|
|
|
|
// Change PIN correctly, check that after PIN change the PIN remains validated
|
|
response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "123456");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "654321");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// Reset card and verify that the new PIN has really been set
|
|
resetAndSelectAndOpenSC();
|
|
|
|
response = cmdSet.verifyPIN("654321");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// Change PUK
|
|
response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_PUK, "210987654321");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
resetAndSelectAndOpenSC();
|
|
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x63C2, response.getSw());
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x63C1, response.getSw());
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x63C0, response.getSw());
|
|
|
|
// Reset the PIN with the new PUK
|
|
response = cmdSet.unblockPIN("210987654321", "000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// Reset PUK
|
|
response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_PUK, "012345678901");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// Change the pairing secret
|
|
response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_PAIRING_SECRET, "abcdefghilmnopqrstuvz12345678901");
|
|
assertEquals(0x9000, response.getSw());
|
|
cmdSet.autoUnpair();
|
|
reset();
|
|
response = cmdSet.select();
|
|
assertEquals(0x9000, response.getSw());
|
|
cmdSet.autoPair("abcdefghilmnopqrstuvz12345678901".getBytes());
|
|
|
|
// Reset pairing secret
|
|
cmdSet.autoOpenSecureChannel();
|
|
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_PAIRING_SECRET, sharedSecret);
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// Alt PIN
|
|
response = cmdSet.verifyPIN("024680");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "123456");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
resetAndSelectAndOpenSC();
|
|
|
|
response = cmdSet.verifyPIN("123456");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "024680");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
resetAndSelectAndOpenSC();
|
|
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("UNBLOCK PIN command")
|
|
@Capabilities("credentialsManagement")
|
|
void unblockPinTest() throws Exception {
|
|
// Security condition violation: SecureChannel not open
|
|
APDUResponse response = cmdSet.unblockPIN("012345678901", "000000");
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
cmdSet.autoOpenSecureChannel();
|
|
|
|
// Condition violation: PIN is not blocked
|
|
response = cmdSet.unblockPIN("012345678901", "000000");
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
// Block the PIN
|
|
response = cmdSet.verifyPIN("123456");
|
|
assertEquals(0x63C2, response.getSw());
|
|
|
|
response = cmdSet.verifyPIN("123456");
|
|
assertEquals(0x63C1, response.getSw());
|
|
|
|
response = cmdSet.verifyPIN("123456");
|
|
assertEquals(0x63C0, response.getSw());
|
|
|
|
// Wrong PUK formats (too short, too long)
|
|
response = cmdSet.unblockPIN("12345678901", "000000");
|
|
assertEquals(0x6A80, response.getSw());
|
|
|
|
response = cmdSet.unblockPIN("1234567890123", "000000");
|
|
assertEquals(0x6A80, response.getSw());
|
|
|
|
// Wrong PUK
|
|
response = cmdSet.unblockPIN("123456789010", "000000");
|
|
assertEquals(0x63C4, response.getSw());
|
|
|
|
// Correct PUK
|
|
response = cmdSet.unblockPIN("012345678901", "654321");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// Check that PIN has been changed and unblocked
|
|
resetAndSelectAndOpenSC();
|
|
|
|
response = cmdSet.verifyPIN("654321");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// Reset the PIN to make further tests possible
|
|
response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("LOAD KEY command")
|
|
@Capabilities("keyManagement")
|
|
void loadKeyTest() throws Exception {
|
|
KeyPairGenerator g = keypairGenerator();
|
|
KeyPair keyPair = g.generateKeyPair();
|
|
APDUResponse response;
|
|
|
|
if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) {
|
|
// Security condition violation: SecureChannel not open
|
|
response = cmdSet.loadKey(keyPair);
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
cmdSet.autoOpenSecureChannel();
|
|
}
|
|
|
|
if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) {
|
|
// Security condition violation: PIN not verified
|
|
response = cmdSet.loadKey(keyPair);
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
// Wrong key type
|
|
response = cmdSet.loadKey(new byte[] { (byte) 0xAA, 0x02, (byte) 0x80, 0x00}, (byte) 0x00);
|
|
assertEquals(0x6A86, response.getSw());
|
|
|
|
// Wrong data (wrong template, missing private key, invalid keys)
|
|
response = cmdSet.loadKey(new byte[]{(byte) 0xAA, 0x02, (byte) 0x80, 0x00}, KeycardApplet.LOAD_KEY_P1_EC);
|
|
assertEquals(0x6A80, response.getSw());
|
|
|
|
response = cmdSet.loadKey(new byte[]{(byte) 0xA1, 0x02, (byte) 0x80, 0x00}, KeycardApplet.LOAD_KEY_P1_EC);
|
|
assertEquals(0x6A80, response.getSw());
|
|
|
|
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());
|
|
}
|
|
|
|
byte[] chainCode = new byte[32];
|
|
new Random().nextBytes(chainCode);
|
|
|
|
// Correct LOAD KEY
|
|
response = cmdSet.loadKey(keyPair);
|
|
assertEquals(0x9000, response.getSw());
|
|
verifyKeyUID(response.getData(), ((ECPublicKey) keyPair.getPublic()));
|
|
|
|
keyPair = g.generateKeyPair();
|
|
|
|
// Check extended key
|
|
response = cmdSet.loadKey(keyPair, false, chainCode);
|
|
assertEquals(0x9000, response.getSw());
|
|
verifyKeyUID(response.getData(), ((ECPublicKey) keyPair.getPublic()));
|
|
|
|
// Check omitted public key
|
|
response = cmdSet.loadKey(keyPair, true, null);
|
|
assertEquals(0x9000, response.getSw());
|
|
verifyKeyUID(response.getData(), ((ECPublicKey) keyPair.getPublic()));
|
|
response = cmdSet.loadKey(keyPair, true, chainCode);
|
|
assertEquals(0x9000, response.getSw());
|
|
verifyKeyUID(response.getData(), ((ECPublicKey) keyPair.getPublic()));
|
|
|
|
// Check seed load
|
|
response = cmdSet.loadKey(keyPair.getPrivate(), chainCode);
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("GENERATE MNEMONIC command")
|
|
@Capabilities("keyManagement")
|
|
void generateMnemonicTest() throws Exception {
|
|
// Security condition violation: SecureChannel not open
|
|
APDUResponse response;
|
|
|
|
if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) {
|
|
response = cmdSet.generateMnemonic(4);
|
|
assertEquals(0x6985, response.getSw());
|
|
cmdSet.autoOpenSecureChannel();
|
|
}
|
|
|
|
// Wrong P1 (too short, too long)
|
|
response = cmdSet.generateMnemonic(3);
|
|
assertEquals(0x6A86, response.getSw());
|
|
|
|
response = cmdSet.generateMnemonic(9);
|
|
assertEquals(0x6A86, response.getSw());
|
|
|
|
// Good cases
|
|
response = cmdSet.generateMnemonic(4);
|
|
assertEquals(0x9000, response.getSw());
|
|
assertMnemonic(12, response.getData());
|
|
|
|
response = cmdSet.generateMnemonic(5);
|
|
assertEquals(0x9000, response.getSw());
|
|
assertMnemonic(15, response.getData());
|
|
|
|
response = cmdSet.generateMnemonic(6);
|
|
assertEquals(0x9000, response.getSw());
|
|
assertMnemonic(18, response.getData());
|
|
|
|
response = cmdSet.generateMnemonic(7);
|
|
assertEquals(0x9000, response.getSw());
|
|
assertMnemonic(21, response.getData());
|
|
|
|
response = cmdSet.generateMnemonic(8);
|
|
assertEquals(0x9000, response.getSw());
|
|
assertMnemonic(24, response.getData());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("REMOVE KEY command")
|
|
@Capabilities("keyManagement")
|
|
void removeKeyTest() throws Exception {
|
|
KeyPairGenerator g = keypairGenerator();
|
|
KeyPair keyPair = g.generateKeyPair();
|
|
APDUResponse response;
|
|
|
|
if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) {
|
|
// Security condition violation: SecureChannel not open
|
|
response = cmdSet.removeKey();
|
|
assertEquals(0x6985, response.getSw());
|
|
cmdSet.autoOpenSecureChannel();
|
|
}
|
|
|
|
if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) {
|
|
// Security condition violation: PIN not verified
|
|
response = cmdSet.removeKey();
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
response = cmdSet.loadKey(keyPair);
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
response = cmdSet.select();
|
|
assertEquals(0x9000, response.getSw());
|
|
ApplicationInfo info = new ApplicationInfo(response.getData());
|
|
verifyKeyUID(info.getKeyUID(), (ECPublicKey) keyPair.getPublic());
|
|
|
|
if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) {
|
|
cmdSet.autoOpenSecureChannel();
|
|
}
|
|
|
|
if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) {
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
assertTrue(cmdSet.getKeyInitializationStatus());
|
|
|
|
// Good case
|
|
response = cmdSet.removeKey();
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
assertFalse(cmdSet.getKeyInitializationStatus());
|
|
|
|
response = cmdSet.select();
|
|
assertEquals(0x9000, response.getSw());
|
|
info = new ApplicationInfo(response.getData());
|
|
assertEquals(0, info.getKeyUID().length);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("FACTORY RESET command")
|
|
@Capabilities("factoryReset")
|
|
void factoryResetTest() throws Exception {
|
|
KeyPairGenerator g = keypairGenerator();
|
|
KeyPair keyPair = g.generateKeyPair();
|
|
|
|
// Invalid P1 P2
|
|
APDUResponse response = sdkChannel.send(new APDUCommand(0x80, KeycardApplet.INS_FACTORY_RESET, 0, 0, new byte[0]));
|
|
assertEquals(0x6a86, response.getSw());
|
|
|
|
// Good case
|
|
response = cmdSet.factoryReset();
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
response = cmdSet.getStatus(KeycardCommandSet.GET_STATUS_P1_KEY_PATH);
|
|
assertEquals(0x6d00, response.getSw());
|
|
|
|
response = cmdSet.select();
|
|
assertEquals(0x9000, response.getSw());
|
|
assertFalse(cmdSet.getApplicationInfo().isInitializedCard());
|
|
|
|
initCard(cmdSet);
|
|
|
|
response = cmdSet.select();
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) {
|
|
cmdSet.autoPair(sharedSecret);
|
|
cmdSet.autoOpenSecureChannel();
|
|
}
|
|
|
|
if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) {
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
assertFalse(cmdSet.getKeyInitializationStatus());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("GENERATE KEY command")
|
|
@Capabilities("keyManagement")
|
|
void generateKeyTest() throws Exception {
|
|
APDUResponse response;
|
|
|
|
if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) {
|
|
// Security condition violation: SecureChannel not open
|
|
response = cmdSet.generateKey();
|
|
assertEquals(0x6985, response.getSw());
|
|
cmdSet.autoOpenSecureChannel();
|
|
}
|
|
|
|
if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) {
|
|
// Security condition violation: PIN not verified
|
|
response = cmdSet.generateKey();
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
// Good case
|
|
response = cmdSet.generateKey();
|
|
assertEquals(0x9000, response.getSw());
|
|
byte[] keyUID = response.getData();
|
|
|
|
response = cmdSet.exportCurrentKey(true);
|
|
assertEquals(0x9000, response.getSw());
|
|
byte[] pubKey = response.getData();
|
|
|
|
verifyKeyUID(keyUID, Arrays.copyOfRange(pubKey, 4, pubKey.length));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("DERIVE KEY command")
|
|
void deriveKeyTest() throws Exception {
|
|
APDUResponse response;
|
|
|
|
if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) {
|
|
// Security condition violation: SecureChannel not open
|
|
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x00});
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
cmdSet.autoOpenSecureChannel();
|
|
}
|
|
|
|
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);
|
|
|
|
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());
|
|
}
|
|
|
|
// Wrong data format
|
|
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00});
|
|
assertEquals(0x6A80, response.getSw());
|
|
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x00, 0x00});
|
|
assertEquals(0x6A80, response.getSw());
|
|
|
|
// Correct
|
|
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x01});
|
|
assertEquals(0x9000, response.getSw());
|
|
verifyKeyDerivation(keyPair, chainCode, new int[]{1});
|
|
|
|
// 3 levels with hardened key
|
|
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x01, (byte) 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02});
|
|
assertEquals(0x9000, response.getSw());
|
|
verifyKeyDerivation(keyPair, chainCode, new int[]{1, 0x80000000, 2});
|
|
|
|
// From parent
|
|
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x03}, KeycardApplet.DERIVE_P1_SOURCE_PARENT);
|
|
assertEquals(0x9000, response.getSw());
|
|
verifyKeyDerivation(keyPair, chainCode, new int[]{1, 0x80000000, 3});
|
|
|
|
// Reset master key
|
|
response = cmdSet.deriveKey(new byte[0]);
|
|
assertEquals(0x9000, response.getSw());
|
|
verifyKeyDerivation(keyPair, chainCode, new int[0]);
|
|
|
|
// Try parent when none available
|
|
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x03}, KeycardApplet.DERIVE_P1_SOURCE_PARENT);
|
|
assertEquals(0x6B00, response.getSw());
|
|
|
|
// 3 levels with hardened key using separate commands
|
|
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x01}, KeycardApplet.DERIVE_P1_SOURCE_MASTER);
|
|
assertEquals(0x9000, response.getSw());
|
|
response = cmdSet.deriveKey(new byte[]{(byte) 0x80, 0x00, 0x00, 0x00}, KeycardApplet.DERIVE_P1_SOURCE_CURRENT);
|
|
assertEquals(0x9000, response.getSw());
|
|
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x02}, KeycardApplet.DERIVE_P1_SOURCE_CURRENT);
|
|
assertEquals(0x9000, response.getSw());
|
|
verifyKeyDerivation(keyPair, chainCode, new int[]{1, 0x80000000, 2});
|
|
|
|
// Reset master key
|
|
response = cmdSet.deriveKey(new byte[0]);
|
|
assertEquals(0x9000, response.getSw());
|
|
verifyKeyDerivation(keyPair, chainCode, new int[0]);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("SIGN command")
|
|
void signTest() throws Exception {
|
|
byte[] data = "some data to be hashed".getBytes();
|
|
byte[] hash = sha256(data);
|
|
|
|
APDUResponse response;
|
|
|
|
if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) {
|
|
// Security condition violation: SecureChannel not open
|
|
response = cmdSet.sign(hash);
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
cmdSet.autoOpenSecureChannel();
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
if (!cmdSet.getApplicationInfo().hasMasterKey()) {
|
|
response = cmdSet.generateKey();
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
// Wrong Data length
|
|
response = cmdSet.sign(data);
|
|
assertEquals(0x6A80, response.getSw());
|
|
|
|
// 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());
|
|
|
|
// Alt PIN
|
|
response = cmdSet.verifyPIN("024680");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
response = cmdSet.signWithPath(hash, updatedPath, false);
|
|
verifySignResp(data, response);
|
|
}
|
|
|
|
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);
|
|
sig = extractSignature(sig);
|
|
|
|
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
|
|
ECPublicKeySpec cardKeySpec = new ECPublicKeySpec(ecSpec.getCurve().decodePoint(keyData), ecSpec);
|
|
ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA", "BC").generatePublic(cardKeySpec);
|
|
|
|
signature.initVerify(cardKey);
|
|
assertEquals((SecureChannel.SC_KEY_LENGTH * 2 / 8) + 1, keyData.length);
|
|
signature.update(data);
|
|
assertTrue(signature.verify(sig));
|
|
assertFalse(isMalleable(sig));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("SET PINLESS PATH command")
|
|
@Capabilities("credentialsManagement") // The current test is not adapted to run automatically on devices without credentials management, since the tester must know what button to press
|
|
void setPinlessPathTest() throws Exception {
|
|
byte[] data = "some data to be hashed".getBytes();
|
|
byte[] hash = sha256(data);
|
|
|
|
KeyPairGenerator g = keypairGenerator();
|
|
KeyPair keyPair = g.generateKeyPair();
|
|
byte[] chainCode = new byte[32];
|
|
new Random().nextBytes(chainCode);
|
|
|
|
APDUResponse response;
|
|
|
|
if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) {
|
|
// Security condition violation: SecureChannel not open
|
|
response = cmdSet.setPinlessPath(new byte[]{0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02});
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
cmdSet.autoOpenSecureChannel();
|
|
}
|
|
|
|
if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) {
|
|
// Security condition violation: PIN not verified
|
|
response = cmdSet.setPinlessPath(new byte[]{0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02});
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
if (!cmdSet.getApplicationInfo().hasMasterKey()) {
|
|
response = cmdSet.loadKey(keyPair, false, chainCode);
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
// Wrong data
|
|
response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00});
|
|
assertEquals(0x6a80, response.getSw());
|
|
response = cmdSet.setPinlessPath(new byte[(KeycardApplet.KEY_PATH_MAX_DEPTH + 1)* 4]);
|
|
assertEquals(0x6a80, response.getSw());
|
|
|
|
// Correct
|
|
response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02});
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// Verify that only PINless path can be used without PIN
|
|
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.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());
|
|
|
|
// Verify changing path
|
|
if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) {
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01});
|
|
assertEquals(0x9000, response.getSw());
|
|
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());
|
|
|
|
// Reset
|
|
if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) {
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
response = cmdSet.setPinlessPath(new byte[] {});
|
|
assertEquals(0x9000, response.getSw());
|
|
resetAndSelectAndOpenSC();
|
|
response = cmdSet.sign(hash);
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) {
|
|
response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x02}, KeycardApplet.DERIVE_P1_SOURCE_MASTER);
|
|
assertEquals(0x6985, response.getSw());
|
|
}
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("EXPORT KEY command")
|
|
void exportKey() throws Exception {
|
|
KeyPairGenerator g = keypairGenerator();
|
|
KeyPair keyPair = g.generateKeyPair();
|
|
byte[] chainCode = new byte[32];
|
|
new Random().nextBytes(chainCode);
|
|
|
|
APDUResponse response;
|
|
|
|
if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) {
|
|
// Security condition violation: SecureChannel not open
|
|
response = cmdSet.exportCurrentKey(true);
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
cmdSet.autoOpenSecureChannel();
|
|
}
|
|
|
|
if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) {
|
|
// Security condition violation: PIN not verified
|
|
response = cmdSet.exportCurrentKey(true);
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
if (cmdSet.getApplicationInfo().hasKeyManagementCapability()) {
|
|
response = cmdSet.loadKey(keyPair, false, chainCode);
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
response = cmdSet.deriveKey(new byte[0], KeycardApplet.DERIVE_P1_SOURCE_MASTER);
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// Security condition violation: current key is not exportable
|
|
response = cmdSet.exportCurrentKey(false);
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
response = cmdSet.deriveKey(new byte[] {(byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2c, (byte) 0x00, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00}, KeycardApplet.DERIVE_P1_SOURCE_MASTER);
|
|
assertEquals(0x9000, response.getSw());
|
|
response = cmdSet.exportCurrentKey(false);
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
response = cmdSet.deriveKey(new byte[] {(byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2D, (byte) 0x00, 0x00, 0x00, 0x00}, KeycardApplet.DERIVE_P1_SOURCE_MASTER);
|
|
assertEquals(0x9000, response.getSw());
|
|
response = cmdSet.exportCurrentKey(false);
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
// Export current public key
|
|
response = cmdSet.exportCurrentKey(true);
|
|
assertEquals(0x9000, response.getSw());
|
|
byte[] keyTemplate = response.getData();
|
|
verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 0x8000002b, 0x8000003c, 0x8000062d, 0x00000000 }, true, false);
|
|
|
|
// Derive & Make current
|
|
response = cmdSet.exportKey(new byte[] {(byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2D, (byte) 0x00, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00}, KeycardApplet.DERIVE_P1_SOURCE_MASTER, true, false);
|
|
assertEquals(0x9000, response.getSw());
|
|
keyTemplate = response.getData();
|
|
verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 0x8000002b, 0x8000003c, 0x8000062d, 0x00000000, 0x00000000 }, false, false);
|
|
|
|
// Derive without making current
|
|
response = cmdSet.exportKey(new byte[] {(byte) 0x00, 0x00, 0x00, 0x01}, KeycardApplet.DERIVE_P1_SOURCE_PARENT, false,false);
|
|
assertEquals(0x9000, response.getSw());
|
|
keyTemplate = response.getData();
|
|
verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 0x8000002b, 0x8000003c, 0x8000062d, 0x00000000, 0x00000001 }, false, false);
|
|
response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_KEY_PATH);
|
|
assertEquals(0x9000, response.getSw());
|
|
assertArrayEquals(new byte[] {(byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2D, (byte) 0x00, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00}, response.getData());
|
|
|
|
// Export current
|
|
response = cmdSet.exportCurrentKey(false);
|
|
assertEquals(0x9000, response.getSw());
|
|
keyTemplate = response.getData();
|
|
verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 0x8000002b, 0x8000003c, 0x8000062d, 0x00000000, 0x00000000 }, false, false);
|
|
|
|
// Export extended public
|
|
response = cmdSet.exportKey(new byte[] {(byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2D, (byte) 0x00, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00}, KeycardApplet.DERIVE_P1_SOURCE_MASTER, false, KeycardCommandSet.EXPORT_KEY_P2_EXTENDED_PUBLIC);
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
response = cmdSet.exportKey(new byte[] {(byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2c, (byte) 0x00, 0x00, 0x00, 0x00}, KeycardApplet.DERIVE_P1_SOURCE_MASTER, false, KeycardCommandSet.EXPORT_KEY_P2_EXTENDED_PUBLIC);
|
|
assertEquals(0x9000, response.getSw());
|
|
keyTemplate = response.getData();
|
|
verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 0x8000002b, 0x8000003c, 0x8000062c, 0x00000000 }, true, true);
|
|
|
|
// Reset
|
|
response = cmdSet.deriveKey(new byte[0], KeycardApplet.DERIVE_P1_SOURCE_MASTER);
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// Alt PIN
|
|
response = cmdSet.verifyPIN("024680");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
response = cmdSet.exportKey(new byte[] {(byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2c, (byte) 0x00, 0x00, 0x00, 0x00}, KeycardApplet.DERIVE_P1_SOURCE_MASTER, false, KeycardCommandSet.EXPORT_KEY_P2_EXTENDED_PUBLIC);
|
|
assertEquals(0x9000, response.getSw());
|
|
keyTemplate = response.getData();
|
|
verifyExportedKey(keyTemplate, keyPair, sha256(chainCode), new int[] { 0x8000002b, 0x8000003c, 0x8000062c, 0x00000000 }, true, true);
|
|
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("STORE/GET DATA")
|
|
void storeGetDataTest() throws Exception {
|
|
APDUResponse response;
|
|
|
|
if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) {
|
|
// Security condition violation: SecureChannel not open
|
|
response = cmdSet.storeData(new byte[20], KeycardCommandSet.STORE_DATA_P1_PUBLIC);
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
cmdSet.autoOpenSecureChannel();
|
|
}
|
|
|
|
if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) {
|
|
// Security condition violation: PIN not verified
|
|
response = cmdSet.storeData(new byte[20], KeycardCommandSet.STORE_DATA_P1_PUBLIC);
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
// Data too long
|
|
response = cmdSet.storeData(new byte[128], KeycardCommandSet.STORE_DATA_P1_PUBLIC);
|
|
assertEquals(0x6A80, response.getSw());
|
|
|
|
byte[] data = new byte[127];
|
|
|
|
for (int i = 0; i < 127; i++) {
|
|
data[i] = (byte) i;
|
|
}
|
|
|
|
// Correct data
|
|
response = cmdSet.storeData(data, KeycardCommandSet.STORE_DATA_P1_PUBLIC);
|
|
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// Read data back with secure channel
|
|
response = cmdSet.getData(KeycardCommandSet.STORE_DATA_P1_PUBLIC);
|
|
assertEquals(0x9000, response.getSw());
|
|
assertArrayEquals(data, response.getData());
|
|
|
|
// Empty data
|
|
response = cmdSet.storeData(new byte[0], KeycardCommandSet.STORE_DATA_P1_PUBLIC);
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
response = cmdSet.getData(KeycardCommandSet.STORE_DATA_P1_PUBLIC);
|
|
assertEquals(0x9000, response.getSw());
|
|
assertEquals(0, response.getData().length);
|
|
|
|
// Shorter data
|
|
data = Arrays.copyOf(data, 20);
|
|
response = cmdSet.storeData(data, KeycardCommandSet.STORE_DATA_P1_PUBLIC);
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// GET DATA without Secure Channel
|
|
cmdSet.select().checkOK();
|
|
|
|
response = cmdSet.getData(KeycardCommandSet.STORE_DATA_P1_PUBLIC);
|
|
assertEquals(0x9000, response.getSw());
|
|
assertArrayEquals(data, response.getData());
|
|
|
|
if (cmdSet.getApplicationInfo().hasNDEFCapability()) {
|
|
byte[] ndefData = {
|
|
(byte) 0x00, (byte) 0x24, (byte) 0xd4, (byte) 0x0f, (byte) 0x12, (byte) 0x61, (byte) 0x6e, (byte) 0x64,
|
|
(byte) 0x72, (byte) 0x6f, (byte) 0x69, (byte) 0x64, (byte) 0x2e, (byte) 0x63, (byte) 0x6f, (byte) 0x6d,
|
|
(byte) 0x3a, (byte) 0x70, (byte) 0x6b, (byte) 0x67, (byte) 0x69, (byte) 0x6d, (byte) 0x2e, (byte) 0x73,
|
|
(byte) 0x74, (byte) 0x61, (byte) 0x74, (byte) 0x75, (byte) 0x73, (byte) 0x2e, (byte) 0x65, (byte) 0x74,
|
|
(byte) 0x68, (byte) 0x65, (byte) 0x72, (byte) 0x65, (byte) 0x75, (byte) 0x6d
|
|
};
|
|
|
|
// Security condition violation: SecureChannel not open
|
|
response = cmdSet.setNDEF(ndefData);
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
cmdSet.autoOpenSecureChannel();
|
|
|
|
// Security condition violation: PIN not verified
|
|
response = cmdSet.setNDEF(ndefData);
|
|
assertEquals(0x6985, response.getSw());
|
|
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// Good case.
|
|
response = cmdSet.setNDEF(ndefData);
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// Good case with no length.
|
|
response = cmdSet.setNDEF(Arrays.copyOfRange(ndefData, 2, ndefData.length));
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
data[0] = (byte) 0xAA;
|
|
|
|
response = cmdSet.storeData(data, KeycardCommandSet.STORE_DATA_P1_CASH);
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
CashCommandSet cashCmdSet = new CashCommandSet(sdkChannel);
|
|
response = cashCmdSet.select();
|
|
assertEquals(0x9000, response.getSw());
|
|
CashApplicationInfo info = new CashApplicationInfo(response.getData());
|
|
assertArrayEquals(data, info.getPubData());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Test the Cash applet")
|
|
void cashTest() throws Exception {
|
|
CashCommandSet cashCmdSet = new CashCommandSet(sdkChannel);
|
|
APDUResponse response = cashCmdSet.select();
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
CashApplicationInfo info = new CashApplicationInfo(response.getData());
|
|
assertTrue(info.getAppVersion() > 0);
|
|
|
|
byte[] data = "some data to be hashed".getBytes();
|
|
byte[] hash = sha256(data);
|
|
|
|
response = cashCmdSet.sign(hash);
|
|
verifySignResp(data, response);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Mnemonic load and derivation")
|
|
@Tag("manual")
|
|
void mnemonicTest() throws Exception {
|
|
if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) {
|
|
cmdSet.autoOpenSecureChannel();
|
|
}
|
|
|
|
APDUResponse response;
|
|
|
|
if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) {
|
|
response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
byte[] seed = Mnemonic.toBinarySeed("legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will", "");
|
|
response = cmdSet.loadKey(seed);
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
response = cmdSet.exportCurrentKey(true);
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
BIP32KeyPair pubKey = BIP32KeyPair.fromTLV(response.getData());
|
|
assertEquals("04cc620f846055ed43995391ca5e490c52251ea40453f64a0515bef84c24a653a7c4e02b9de56f66d9ee58dc6b591b534f5a20c0550b2c33a086b90b866cf70799", Hex.toHexString(pubKey.getPublicKey()));
|
|
|
|
response = cmdSet.exportKey("m/43'/60'/1581'/0'/0", false, true);
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
pubKey = BIP32KeyPair.fromTLV(response.getData());
|
|
assertEquals("04e7370d118461e1ab01f3e86e88c4b0c7b92cecb79c5e320cef73dda912f173beae74df15090b6405a274963c054cdfe6ac7843a302c260390d1fe776008f310e", Hex.toHexString(pubKey.getPublicKey()));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Sign actual Ethereum transaction")
|
|
@Tag("manual")
|
|
void signTransactionTest() throws Exception {
|
|
// Initialize credentials
|
|
Web3j web3j = Web3j.build(new HttpService());
|
|
Credentials wallet1 = WalletUtils.loadCredentials("testwallet", "testwallets/wallet1.json");
|
|
Credentials wallet2 = WalletUtils.loadCredentials("testwallet", "testwallets/wallet2.json");
|
|
|
|
// Load keys on card
|
|
cmdSet.autoOpenSecureChannel();
|
|
APDUResponse response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
response = cmdSet.loadKey(wallet1.getEcKeyPair());
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
// Verify balance
|
|
System.out.println("Wallet 1 balance: " + web3j.ethGetBalance(wallet1.getAddress(), DefaultBlockParameterName.LATEST).send().getBalance());
|
|
System.out.println("Wallet 2 balance: " + web3j.ethGetBalance(wallet2.getAddress(), DefaultBlockParameterName.LATEST).send().getBalance());
|
|
|
|
// Create transaction
|
|
BigInteger gasPrice = web3j.ethGasPrice().send().getGasPrice();
|
|
BigInteger weiValue = Convert.toWei(BigDecimal.valueOf(1.0), Convert.Unit.FINNEY).toBigIntegerExact();
|
|
BigInteger nonce = web3j.ethGetTransactionCount(wallet1.getAddress(), DefaultBlockParameterName.LATEST).send().getTransactionCount();
|
|
|
|
RawTransaction rawTransaction = RawTransaction.createEtherTransaction(nonce, gasPrice, Transfer.GAS_LIMIT, wallet2.getAddress(), weiValue);
|
|
|
|
// Sign transaction
|
|
byte[] txBytes = TransactionEncoder.encode(rawTransaction);
|
|
Sign.SignatureData signature = signMessage(txBytes);
|
|
|
|
Method encode = TransactionEncoder.class.getDeclaredMethod("encode", RawTransaction.class, Sign.SignatureData.class);
|
|
encode.setAccessible(true);
|
|
|
|
// Send transaction
|
|
byte[] signedMessage = (byte[]) encode.invoke(null, rawTransaction, signature);
|
|
String hexValue = "0x" + Hex.toHexString(signedMessage);
|
|
EthSendTransaction ethSendTransaction = web3j.ethSendRawTransaction(hexValue).send();
|
|
|
|
if (ethSendTransaction.hasError()) {
|
|
System.out.println("Transaction Error: " + ethSendTransaction.getError().getMessage());
|
|
}
|
|
|
|
assertFalse(ethSendTransaction.hasError());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Performance Test")
|
|
@Tag("manual")
|
|
void performanceTest() throws Exception {
|
|
long time, deriveAccount = 0, deriveParent = 0, deriveParentHardened = 0;
|
|
final long SAMPLE_COUNT = 10;
|
|
|
|
System.out.println("Measuring key derivation performance. All times are expressed in milliseconds");
|
|
System.out.println("***********************************************" );
|
|
|
|
// Prepare the card
|
|
cmdSet.autoOpenSecureChannel();
|
|
APDUResponse response = cmdSet.verifyPIN("000000");
|
|
assertEquals(0x9000, response.getSw());
|
|
KeyPairGenerator g = keypairGenerator();
|
|
KeyPair keyPair = g.generateKeyPair();
|
|
byte[] chainCode = new byte[32];
|
|
new Random().nextBytes(chainCode);
|
|
|
|
response = cmdSet.loadKey(keyPair, false, chainCode);
|
|
assertEquals(0x9000, response.getSw());
|
|
|
|
for (int i = 0; i < SAMPLE_COUNT; i++) {
|
|
time = System.currentTimeMillis();
|
|
response = cmdSet.deriveKey(new byte[] { (byte) 0x80, 0x00, 0x00, 0x2C, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00}, KeycardApplet.DERIVE_P1_SOURCE_MASTER);
|
|
deriveAccount += System.currentTimeMillis() - time;
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
deriveAccount /= SAMPLE_COUNT;
|
|
|
|
for (int i = 0; i < SAMPLE_COUNT; i++) {
|
|
time = System.currentTimeMillis();
|
|
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, (byte) i}, KeycardApplet.DERIVE_P1_SOURCE_PARENT);
|
|
deriveParent += System.currentTimeMillis() - time;
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
deriveParent /= SAMPLE_COUNT;
|
|
|
|
for (int i = 0; i < SAMPLE_COUNT; i++) {
|
|
time = System.currentTimeMillis();
|
|
response = cmdSet.deriveKey(new byte[] {(byte) 0x80, 0x00, 0x00, (byte) i}, KeycardApplet.DERIVE_P1_SOURCE_PARENT);
|
|
deriveParentHardened += System.currentTimeMillis() - time;
|
|
assertEquals(0x9000, response.getSw());
|
|
}
|
|
|
|
deriveParentHardened /= SAMPLE_COUNT;
|
|
|
|
System.out.println("Time to derive m/44'/60'/0'/0/0: " + deriveAccount);
|
|
System.out.println("Time to switch m/44'/60'/0'/0/0': " + deriveParentHardened);
|
|
System.out.println("Time to switch back to m/44'/60'/0'/0/0: " + deriveParent);
|
|
}
|
|
|
|
private KeyPairGenerator keypairGenerator() throws Exception {
|
|
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
|
|
KeyPairGenerator g = KeyPairGenerator.getInstance("ECDH", "BC");
|
|
g.initialize(ecSpec);
|
|
|
|
return g;
|
|
}
|
|
|
|
private byte[] extractSignature(byte[] sig) {
|
|
int off = sig[4] + 5;
|
|
return Arrays.copyOfRange(sig, off, off + sig[off + 1] + 2);
|
|
}
|
|
|
|
private byte[] extractPublicKeyFromSignature(byte[] sig) {
|
|
assertEquals(KeycardApplet.TLV_SIGNATURE_TEMPLATE, sig[0]);
|
|
assertEquals((byte) 0x81, sig[1]);
|
|
assertEquals(KeycardApplet.TLV_PUB_KEY, sig[3]);
|
|
|
|
return Arrays.copyOfRange(sig, 5, 5 + sig[4]);
|
|
}
|
|
|
|
private void reset() {
|
|
switch(TARGET) {
|
|
case TARGET_SIMULATOR:
|
|
simulator.reset();
|
|
break;
|
|
case TARGET_CARD:
|
|
apduChannel.getCard().getATR();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void resetAndSelectAndOpenSC() throws Exception {
|
|
if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) {
|
|
reset();
|
|
cmdSet.select();
|
|
cmdSet.autoOpenSecureChannel();
|
|
}
|
|
}
|
|
|
|
private void assertMnemonic(int expectedLength, byte[] data) {
|
|
short[] shorts = new short[data.length / 2];
|
|
assertEquals(expectedLength, shorts.length);
|
|
ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN).asShortBuffer().get(shorts);
|
|
|
|
boolean[] bits = new boolean[11 * shorts.length];
|
|
int i = 0;
|
|
|
|
for (short mIdx : shorts) {
|
|
assertTrue(mIdx >= 0 && mIdx < 2048);
|
|
for (int j = 0; j < 11; ++j) {
|
|
bits[i++] = (mIdx & (1 << (10 - j))) > 0;
|
|
}
|
|
}
|
|
|
|
data = new byte[bits.length / 33 * 4];
|
|
|
|
for (i = 0; i < bits.length / 33 * 32; ++i) {
|
|
data[i / 8] |= (bits[i] ? 1 : 0) << (7 - (i % 8));
|
|
}
|
|
|
|
byte[] check = sha256(data);
|
|
|
|
for (i = bits.length / 33 * 32; i < bits.length; ++i) {
|
|
if ((check[(i - bits.length / 33 * 32) / 8] & (1 << (7 - (i % 8))) ^ (bits[i] ? 1 : 0) << (7 - (i % 8))) != 0) {
|
|
fail("Checksum is invalid");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void verifyKeyDerivation(KeyPair keyPair, byte[] chainCode, int[] path) throws Exception {
|
|
byte[] hash = sha256(new byte[8]);
|
|
APDUResponse resp = cmdSet.sign(hash);
|
|
assertEquals(0x9000, resp.getSw());
|
|
byte[] sig = resp.getData();
|
|
byte[] publicKey = extractPublicKeyFromSignature(sig);
|
|
sig = extractSignature(sig);
|
|
|
|
if (cmdSet.getApplicationInfo().hasKeyManagementCapability()) {
|
|
DeterministicKey key = deriveKey(keyPair, chainCode, path);
|
|
|
|
assertTrue(key.verify(hash, sig));
|
|
assertArrayEquals(key.getPubKeyPoint().getEncoded(false), publicKey);
|
|
} else {
|
|
Signature signature = Signature.getInstance("SHA256withECDSA", "BC");
|
|
|
|
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
|
|
ECPublicKeySpec cardKeySpec = new ECPublicKeySpec(ecSpec.getCurve().decodePoint(publicKey), ecSpec);
|
|
ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA", "BC").generatePublic(cardKeySpec);
|
|
|
|
signature.initVerify(cardKey);
|
|
signature.update(new byte[8]);
|
|
assertTrue(signature.verify(sig));
|
|
}
|
|
|
|
resp = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_KEY_PATH);
|
|
assertEquals(0x9000, resp.getSw());
|
|
byte[] rawPath = resp.getData();
|
|
|
|
assertEquals(path.length * 4, rawPath.length);
|
|
|
|
for (int i = 0; i < path.length; i++) {
|
|
int k = path[i];
|
|
int k1 = (rawPath[i * 4] << 24) | (rawPath[(i * 4) + 1] << 16) | (rawPath[(i * 4) + 2] << 8) | rawPath[(i * 4) + 3];
|
|
assertEquals(k, k1);
|
|
}
|
|
}
|
|
|
|
private void verifyExportedKey(byte[] keyTemplate, KeyPair keyPair, byte[] chainCode, int[] path, boolean publicOnly, boolean extendedPublic) {
|
|
if (!cmdSet.getApplicationInfo().hasKeyManagementCapability()) {
|
|
return;
|
|
}
|
|
|
|
DeterministicKey dk = deriveKey(keyPair, chainCode, path);
|
|
ECKey key = dk.decompress();
|
|
assertEquals(KeycardApplet.TLV_KEY_TEMPLATE, keyTemplate[0]);
|
|
|
|
if (publicOnly) {
|
|
assertEquals(KeycardApplet.TLV_PUB_KEY, keyTemplate[2]);
|
|
byte[] pubKey = Arrays.copyOfRange(keyTemplate, 4, 4 + keyTemplate[3]);
|
|
|
|
assertArrayEquals(key.getPubKey(), pubKey);
|
|
int templateLen = 2 + pubKey.length;
|
|
|
|
if (extendedPublic) {
|
|
byte[] chain = Arrays.copyOfRange(keyTemplate, templateLen + 4, templateLen + 4 + keyTemplate[3 + templateLen]);
|
|
assertEquals(KeycardApplet.TLV_CHAIN_CODE, keyTemplate[2 + templateLen]);
|
|
assertArrayEquals(dk.getChainCode(), chain);
|
|
templateLen += 2 + chain.length;
|
|
}
|
|
|
|
assertEquals(templateLen, keyTemplate[1]);
|
|
assertEquals(templateLen + 2, keyTemplate.length);
|
|
} else {
|
|
assertEquals(KeycardApplet.TLV_PRIV_KEY, keyTemplate[2]);
|
|
byte[] privateKey = Arrays.copyOfRange(keyTemplate, 4, 4 + keyTemplate[3]);
|
|
|
|
byte[] tPrivKey = key.getPrivKey().toByteArray();
|
|
|
|
if (tPrivKey[0] == 0x00) {
|
|
tPrivKey = Arrays.copyOfRange(tPrivKey, 1, tPrivKey.length);
|
|
}
|
|
|
|
assertArrayEquals(tPrivKey, privateKey);
|
|
}
|
|
}
|
|
|
|
private DeterministicKey deriveKey(KeyPair keyPair, byte[] chainCode, int[] path) {
|
|
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));
|
|
}
|
|
|
|
return key;
|
|
}
|
|
|
|
private boolean isMalleable(byte[] sig) {
|
|
int rLen = sig[3];
|
|
int sOff = 6 + rLen;
|
|
int sLen = sig.length - rLen - 6;
|
|
|
|
BigInteger s = new BigInteger(Arrays.copyOfRange(sig, sOff, sOff + sLen));
|
|
BigInteger limit = new BigInteger("7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0", 16);
|
|
|
|
return s.compareTo(limit) >= 1;
|
|
}
|
|
|
|
/**
|
|
* Signs a signature using the card. Returns a SignatureData object which contains v, r and s. The algorithm to do
|
|
* this is as follow:
|
|
*
|
|
* 1) The Keccak-256 hash of transaction is generated off-card
|
|
* 2) A SIGN command is sent to the card to sign the precomputed hash
|
|
* 3) The returned data is the public key and the signature
|
|
* 4) The signature and public key can be used to generate the v value. The v value allows to recover the public key
|
|
* from the signature. Here we use the web3j implementation through reflection
|
|
* 5) v, r and s are the final signature to append to the transaction
|
|
*
|
|
* @param message the raw transaction
|
|
* @return the signature data
|
|
*/
|
|
private Sign.SignatureData signMessage(byte[] message) throws Exception {
|
|
byte[] messageHash = Hash.sha3(message);
|
|
|
|
APDUResponse response = cmdSet.sign(messageHash);
|
|
assertEquals(0x9000, response.getSw());
|
|
byte[] respData = response.getData();
|
|
byte[] rawSig = extractSignature(respData);
|
|
|
|
int rLen = rawSig[3];
|
|
int sOff = 6 + rLen;
|
|
int sLen = rawSig.length - rLen - 6;
|
|
|
|
BigInteger r = new BigInteger(Arrays.copyOfRange(rawSig, 4, 4 + rLen));
|
|
BigInteger s = new BigInteger(Arrays.copyOfRange(rawSig, sOff, sOff + sLen));
|
|
|
|
Class<?> ecdsaSignature = Class.forName("org.web3j.crypto.Sign$ECDSASignature");
|
|
Constructor ecdsaSignatureConstructor = ecdsaSignature.getDeclaredConstructor(BigInteger.class, BigInteger.class);
|
|
ecdsaSignatureConstructor.setAccessible(true);
|
|
Object sig = ecdsaSignatureConstructor.newInstance(r, s);
|
|
Method m = ecdsaSignature.getMethod("toCanonicalised");
|
|
m.setAccessible(true);
|
|
sig = m.invoke(sig);
|
|
|
|
Method recoverFromSignature = Sign.class.getDeclaredMethod("recoverFromSignature", int.class, ecdsaSignature, byte[].class);
|
|
recoverFromSignature.setAccessible(true);
|
|
|
|
byte[] pubData = extractPublicKeyFromSignature(respData);
|
|
BigInteger publicKey = new BigInteger(Arrays.copyOfRange(pubData, 1, pubData.length));
|
|
|
|
int recId = -1;
|
|
for (int i = 0; i < 4; i++) {
|
|
BigInteger k = (BigInteger) recoverFromSignature.invoke(null, i, sig, messageHash);
|
|
if (k != null && k.equals(publicKey)) {
|
|
recId = i;
|
|
break;
|
|
}
|
|
}
|
|
if (recId == -1) {
|
|
throw new RuntimeException("Could not construct a recoverable key. This should never happen.");
|
|
}
|
|
|
|
int headerByte = recId + 27;
|
|
|
|
Field rF = ecdsaSignature.getDeclaredField("r");
|
|
rF.setAccessible(true);
|
|
Field sF = ecdsaSignature.getDeclaredField("s");
|
|
sF.setAccessible(true);
|
|
r = (BigInteger) rF.get(sig);
|
|
s = (BigInteger) sF.get(sig);
|
|
|
|
// 1 header + 32 bytes for R + 32 bytes for S
|
|
byte v = (byte) headerByte;
|
|
byte[] rB = Numeric.toBytesPadded(r, 32);
|
|
byte[] sB = Numeric.toBytesPadded(s, 32);
|
|
|
|
return new Sign.SignatureData(v, rB, sB);
|
|
}
|
|
|
|
private void verifyKeyUID(byte[] keyUID, ECPublicKey pubKey) {
|
|
verifyKeyUID(keyUID, pubKey.getQ().getEncoded(false));
|
|
}
|
|
|
|
private void verifyKeyUID(byte[] keyUID, byte[] pubKey) {
|
|
assertArrayEquals(sha256(pubKey), keyUID);
|
|
}
|
|
}
|