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