add data in cash applet

This commit is contained in:
Michele Balistreri 2019-10-16 14:53:36 +03:00
parent 1b716790f0
commit 4cc3f1576c
No known key found for this signature in database
GPG Key ID: E9567DA33A4F791A
7 changed files with 139 additions and 106 deletions

View File

@ -9,7 +9,7 @@ buildscript {
dependencies {
classpath 'com.fidesmo:gradle-javacard:0.2.7'
classpath 'com.github.status-im.status-keycard-java:desktop:4a69788'
classpath 'com.github.status-im.status-keycard-java:desktop:8cb43e6'
}
}
@ -31,7 +31,7 @@ javacard {
aid = '0xA0:0x00:0x00:0x08:0x04:0x00:0x01:0x03'
className = 'CashApplet'
}
version = '2.3'
version = '3.0'
}
}
@ -55,7 +55,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:4a69788')
testCompile('com.github.status-im.status-keycard-java:desktop:8cb43e6')
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")

View File

@ -4,5 +4,5 @@ repositories {
}
dependencies {
compile 'com.github.status-im.status-keycard-java:desktop:4a69788'
compile 'com.github.status-im.status-keycard-java:desktop:8cb43e6'
}

View File

@ -5,6 +5,7 @@ import javacard.security.*;
public class CashApplet extends Applet {
private static final short SIGN_OUT_OFF = ISO7816.OFFSET_CDATA + MessageDigest.LENGTH_SHA_256;
private static final byte TLV_PUB_DATA = (byte) 0x82;
private KeyPair keypair;
private ECPublicKey publicKey;
@ -51,6 +52,14 @@ public class CashApplet extends Applet {
signature = Signature.getInstance(Signature.ALG_ECDSA_SHA_256, false);
signature.init(privateKey, Signature.MODE_SIGN);
short c9Off = (short)(bOffset + bArray[bOffset] + 1); // Skip AID
c9Off += (short)(bArray[c9Off] + 1); // Skip Privileges and parameter length
short dataLen = Util.makeShort((byte) 0x00, bArray[c9Off]);
if (dataLen > 0) {
Util.arrayCopyNonAtomic(bArray, c9Off, SharedMemory.cashDataFile, (short) 0, (short)(dataLen + 1));
}
register(bArray, (short) (bOffset + 1), bArray[bOffset]);
}
@ -99,6 +108,11 @@ public class CashApplet extends Applet {
Util.setShort(apduBuffer, off, KeycardApplet.APPLICATION_VERSION);
off += 2;
apduBuffer[off++] = TLV_PUB_DATA;
apduBuffer[off++] = SharedMemory.cashDataFile[0];
Util.arrayCopyNonAtomic(SharedMemory.cashDataFile, (short) 1, apduBuffer, off, SharedMemory.cashDataFile[0]);
off += SharedMemory.cashDataFile[0];
apduBuffer[lenoff] = (byte)(off - lenoff - 1);
apdu.setOutgoingAndSend((short) 0, off);
}

View File

@ -4,14 +4,15 @@ import javacard.framework.*;
import javacard.security.*;
import javacardx.crypto.Cipher;
import static javacard.framework.ISO7816.OFFSET_P1;
/**
* The applet's main class. All incoming commands a processed by this class.
*/
public class KeycardApplet extends Applet {
static final short APPLICATION_VERSION = (short) 0x0203;
static final short APPLICATION_VERSION = (short) 0x0300;
static final byte INS_GET_STATUS = (byte) 0xF2;
static final byte INS_SET_NDEF = (byte) 0xF3;
static final byte INS_INIT = (byte) 0xFE;
static final byte INS_VERIFY_PIN = (byte) 0x20;
static final byte INS_CHANGE_PIN = (byte) 0x21;
@ -74,6 +75,10 @@ public class KeycardApplet extends Applet {
static final byte EXPORT_KEY_P2_PRIVATE_AND_PUBLIC = 0x00;
static final byte EXPORT_KEY_P2_PUBLIC_ONLY = 0x01;
static final byte STORE_DATA_P1_PUBLIC = 0x00;
static final byte STORE_DATA_P1_NDEF = 0x01;
static final byte STORE_DATA_P1_CASH = 0x02;
static final byte TLV_SIGNATURE_TEMPLATE = (byte) 0xA0;
static final byte TLV_KEY_TEMPLATE = (byte) 0xA1;
@ -239,9 +244,6 @@ public class KeycardApplet extends Applet {
case INS_GET_STATUS:
getStatus(apdu);
break;
case INS_SET_NDEF:
setNDEF(apdu);
break;
case INS_VERIFY_PIN:
verifyPIN(apdu);
break;
@ -434,9 +436,9 @@ public class KeycardApplet extends Applet {
short len;
if (apduBuffer[ISO7816.OFFSET_P1] == GET_STATUS_P1_APPLICATION) {
if (apduBuffer[OFFSET_P1] == GET_STATUS_P1_APPLICATION) {
len = getApplicationStatus(apduBuffer, SecureChannel.SC_OUT_OFFSET);
} else if (apduBuffer[ISO7816.OFFSET_P1] == GET_STATUS_P1_KEY_PATH) {
} else if (apduBuffer[OFFSET_P1] == GET_STATUS_P1_KEY_PATH) {
len = getKeyStatus(apduBuffer, SecureChannel.SC_OUT_OFFSET);
} else {
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
@ -446,36 +448,6 @@ public class KeycardApplet extends Applet {
secureChannel.respond(apdu, len, ISO7816.SW_NO_ERROR);
}
/**
* Sets the content of the NDEF data file returned by the NDEF applet. Requires a secure channel to be already open
* and the PIN to be verified.
*
* @param apdu the JCRE-owned APDU object.
*/
private void setNDEF(APDU apdu) {
byte[] apduBuffer = apdu.getBuffer();
secureChannel.preprocessAPDU(apduBuffer);
if (!pin.isValidated()) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
short dataLen = Util.makeShort((byte) 0x00, apduBuffer[ISO7816.OFFSET_LC]);
short offset;
if (Util.makeShort(apduBuffer[ISO7816.OFFSET_CDATA], apduBuffer[(short)(ISO7816.OFFSET_CDATA + 1)]) != (short)(dataLen - 2)) {
offset = ISO7816.OFFSET_P2;
apduBuffer[ISO7816.OFFSET_P2] = 0;
dataLen += 2;
} else {
offset = ISO7816.OFFSET_CDATA;
}
JCSystem.beginTransaction();
Util.arrayCopy(apduBuffer, offset, SharedMemory.ndefDataFile, (short) 0, dataLen);
JCSystem.commitTransaction();
}
/**
* Writes the Application Status Template to the APDU buffer. Invoked internally by the getStatus method. This
* template is useful to understand if the card is blocked, if it has valid keys and if public key derivation is
@ -550,7 +522,7 @@ public class KeycardApplet extends Applet {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
switch(apduBuffer[ISO7816.OFFSET_P1]) {
switch(apduBuffer[OFFSET_P1]) {
case CHANGE_PIN_P1_USER_PIN:
changeUserPIN(apduBuffer, len);
break;
@ -653,7 +625,7 @@ public class KeycardApplet extends Applet {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
switch (apduBuffer[ISO7816.OFFSET_P1]) {
switch (apduBuffer[OFFSET_P1]) {
case LOAD_KEY_P1_EC:
case LOAD_KEY_P1_EXT_EC:
loadKeyPair(apduBuffer);
@ -814,7 +786,7 @@ public class KeycardApplet extends Applet {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
doDerive(apduBuffer, (short) 0, len, apduBuffer[ISO7816.OFFSET_P1], true);
doDerive(apduBuffer, (short) 0, len, apduBuffer[OFFSET_P1], true);
}
/**
@ -968,7 +940,7 @@ public class KeycardApplet extends Applet {
byte[] apduBuffer = apdu.getBuffer();
secureChannel.preprocessAPDU(apduBuffer);
short csLen = apduBuffer[ISO7816.OFFSET_P1];
short csLen = apduBuffer[OFFSET_P1];
if (csLen < GENERATE_MNEMONIC_P1_CS_MIN || csLen > GENERATE_MNEMONIC_P1_CS_MAX) {
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
@ -1105,7 +1077,7 @@ public class KeycardApplet extends Applet {
ECPrivateKey signingKey;
ECPublicKey outputKey;
switch((byte) (apduBuffer[ISO7816.OFFSET_P1] & ~DERIVE_P1_SOURCE_MASK)) {
switch((byte) (apduBuffer[OFFSET_P1] & ~DERIVE_P1_SOURCE_MASK)) {
case SIGN_P1_CURRENT_KEY:
signingKey = privateKey;
outputKey = publicKey;
@ -1154,7 +1126,7 @@ public class KeycardApplet extends Applet {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
byte derivationSource = (byte) (apduBuffer[ISO7816.OFFSET_P1] & DERIVE_P1_SOURCE_MASK);
byte derivationSource = (byte) (apduBuffer[OFFSET_P1] & DERIVE_P1_SOURCE_MASK);
doDerive(apduBuffer, MessageDigest.LENGTH_SHA_256, pathLen, derivationSource, makeCurrent);
} else {
if (len != MessageDigest.LENGTH_SHA_256) {
@ -1257,9 +1229,9 @@ public class KeycardApplet extends Applet {
boolean derive = false;
boolean makeCurrent = false;
byte derivationSource = (byte) (apduBuffer[ISO7816.OFFSET_P1] & DERIVE_P1_SOURCE_MASK);
byte derivationSource = (byte) (apduBuffer[OFFSET_P1] & DERIVE_P1_SOURCE_MASK);
switch ((byte) (apduBuffer[ISO7816.OFFSET_P1] & ~DERIVE_P1_SOURCE_MASK)) {
switch ((byte) (apduBuffer[OFFSET_P1] & ~DERIVE_P1_SOURCE_MASK)) {
case EXPORT_KEY_P1_CURRENT:
break;
case EXPORT_KEY_P1_DERIVE_AND_MAKE_CURRENT:
@ -1339,8 +1311,25 @@ public class KeycardApplet extends Applet {
secureChannel.preprocessAPDU(apduBuffer);
}
short outLen = Util.makeShort((byte) 0x00, data[0]);
Util.arrayCopyNonAtomic(data, (short) 1, apduBuffer, SecureChannel.SC_OUT_OFFSET, outLen);
byte[] dst;
switch (apduBuffer[OFFSET_P1]) {
case STORE_DATA_P1_PUBLIC:
dst = data;
break;
case STORE_DATA_P1_NDEF:
dst = SharedMemory.ndefDataFile;
break;
case STORE_DATA_P1_CASH:
dst = SharedMemory.cashDataFile;
break;
default:
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
return;
}
short outLen = Util.makeShort((byte) 0x00, dst[0]);
Util.arrayCopyNonAtomic(dst, (short) 1, apduBuffer, SecureChannel.SC_OUT_OFFSET, outLen);
if (secureChannel.isOpen()) {
secureChannel.respond(apdu, outLen, ISO7816.SW_NO_ERROR);
@ -1362,14 +1351,31 @@ public class KeycardApplet extends Applet {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
byte[] dst;
switch (apduBuffer[OFFSET_P1]) {
case STORE_DATA_P1_PUBLIC:
dst = data;
break;
case STORE_DATA_P1_NDEF:
dst = SharedMemory.ndefDataFile;
break;
case STORE_DATA_P1_CASH:
dst = SharedMemory.cashDataFile;
break;
default:
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
return;
}
short dataLen = Util.makeShort((byte) 0x00, apduBuffer[ISO7816.OFFSET_LC]);
if (dataLen > MAX_DATA_LENGTH) {
if (dataLen >= (short) dst.length) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
JCSystem.beginTransaction();
Util.arrayCopy(apduBuffer, ISO7816.OFFSET_LC, data, (short) 0, (short)(dataLen + 1));
Util.arrayCopy(apduBuffer, ISO7816.OFFSET_LC, dst, (short) 0, (short)(dataLen + 1));
JCSystem.commitTransaction();
}

View File

@ -18,8 +18,8 @@ public class NDEFApplet extends Applet {
private static final short NDEF_READ_SIZE = (short) 0xff;
private static final byte[] NDEF_CAPS_FILE = {
(byte) 0x00, (byte) 0x0f, (byte) 0x20, (byte) 0x00, (byte) 0xff, (byte) 0x00, (byte) 0x01, (byte) 0x04,
(byte) 0x06, (byte) 0xe1, (byte) 0x04, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0xff
(byte) 0x0f, (byte) 0x00, (byte) 0x0f, (byte) 0x20, (byte) 0x00, (byte) 0xff, (byte) 0x00, (byte) 0x01,
(byte) 0x04, (byte) 0x06, (byte) 0xe1, (byte) 0x04, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0xff
};
private short selectedFile;
@ -50,9 +50,9 @@ public class NDEFApplet extends Applet {
short c9Off = (short)(bOffset + bArray[bOffset] + 1); // Skip AID
c9Off += (short)(bArray[c9Off] + 1); // Skip Privileges and parameter length
short dataLen = Util.makeShort((byte) 0x00, bArray[c9Off++]);
if ((dataLen > 2) && ((short)(dataLen - 2) == Util.makeShort(bArray[c9Off], bArray[(short)(c9Off + 1)]))) {
Util.arrayCopyNonAtomic(bArray, c9Off, SharedMemory.ndefDataFile, (short) 0, dataLen);
short dataLen = Util.makeShort((byte) 0x00, bArray[c9Off]);
if ((dataLen > 2) && ((short)(dataLen - 2) == Util.makeShort(bArray[(short)(c9Off + 1)], bArray[(short)(c9Off + 2)]))) {
Util.arrayCopyNonAtomic(bArray, c9Off, SharedMemory.ndefDataFile, (short) 0, (short)(dataLen + 1));
}
register(bArray, (short) (bOffset + 1), bArray[bOffset]);
@ -113,22 +113,20 @@ public class NDEFApplet extends Applet {
byte[] apduBuffer = apdu.getBuffer();
byte[] data;
short dataLen;
switch(selectedFile) {
case FILEID_NDEF_CAPS:
data = NDEF_CAPS_FILE;
dataLen = (short) NDEF_CAPS_FILE.length;
break;
case FILEID_NDEF_DATA:
data = SharedMemory.ndefDataFile;
dataLen = (short) (Util.makeShort(data[0], data[1]) + 2);
break;
default:
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
return;
}
short dataLen = Util.makeShort((byte) 0x00, data[0]);
short offset = Util.getShort(apduBuffer, ISO7816.OFFSET_P1);
if (offset < 0 || offset >= dataLen) {
@ -144,6 +142,9 @@ public class NDEFApplet extends Applet {
le = (short)(dataLen - offset);
}
// skip the len byte in data
offset++;
apdu.setOutgoingLength(le);
apdu.sendBytesLong(data, offset, le);
}

View File

@ -5,5 +5,8 @@ package im.status.keycard;
*/
class SharedMemory {
/** The NDEF data file. Read through the NDEFApplet. **/
static final byte[] ndefDataFile = new byte[SecureChannel.SC_MAX_PLAIN_LENGTH];
static final byte[] ndefDataFile = new byte[SecureChannel.SC_MAX_PLAIN_LENGTH + 1];
/** The Cash data file. Read through the CashApplet. **/
static final byte[] cashDataFile = new byte[KeycardApplet.MAX_DATA_LENGTH + 1];
}

View File

@ -153,6 +153,7 @@ public class KeycardTest {
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();
@ -497,40 +498,6 @@ public class KeycardTest {
assertNotEquals(null, path);
}
@Test
@DisplayName("SET NDEF command")
@Capabilities("ndef")
void setNDEFTest() throws Exception {
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
APDUResponse 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());
}
@Test
@DisplayName("VERIFY PIN command")
@Capabilities("credentialsManagement")
@ -1305,7 +1272,7 @@ public class KeycardTest {
if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) {
// Security condition violation: SecureChannel not open
response = cmdSet.storePublicData(new byte[20]);
response = cmdSet.storeData(new byte[20], KeycardCommandSet.STORE_DATA_P1_PUBLIC);
assertEquals(0x6985, response.getSw());
cmdSet.autoOpenSecureChannel();
@ -1313,7 +1280,7 @@ public class KeycardTest {
if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) {
// Security condition violation: PIN not verified
response = cmdSet.storePublicData(new byte[20]);
response = cmdSet.storeData(new byte[20], KeycardCommandSet.STORE_DATA_P1_PUBLIC);
assertEquals(0x6985, response.getSw());
response = cmdSet.verifyPIN("000000");
@ -1321,7 +1288,7 @@ public class KeycardTest {
}
// Data too long
response = cmdSet.storePublicData(new byte[128]);
response = cmdSet.storeData(new byte[128], KeycardCommandSet.STORE_DATA_P1_PUBLIC);
assertEquals(0x6A80, response.getSw());
byte[] data = new byte[127];
@ -1331,38 +1298,80 @@ public class KeycardTest {
}
// Correct data
response = cmdSet.storePublicData(data);
response = cmdSet.storeData(data, KeycardCommandSet.STORE_DATA_P1_PUBLIC);
assertEquals(0x9000, response.getSw());
// Read data back with secure channel
response = cmdSet.getPublicData();
response = cmdSet.getData(KeycardCommandSet.STORE_DATA_P1_PUBLIC);
assertEquals(0x9000, response.getSw());
assertArrayEquals(data, response.getData());
// Empty data
response = cmdSet.storePublicData(new byte[0]);
response = cmdSet.storeData(new byte[0], KeycardCommandSet.STORE_DATA_P1_PUBLIC);
assertEquals(0x9000, response.getSw());
response = cmdSet.getPublicData();
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.storePublicData(data);
response = cmdSet.storeData(data, KeycardCommandSet.STORE_DATA_P1_PUBLIC);
assertEquals(0x9000, response.getSw());
// GET DATA without Secure Channel
cmdSet.select().checkOK();
response = cmdSet.getPublicData();
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")
@Tag("manual")
void cashTest() throws Exception {
CashCommandSet cashCmdSet = new CashCommandSet(sdkChannel);
APDUResponse response = cashCmdSet.select();