make MAC actually work
This commit is contained in:
parent
9772f17efa
commit
9cab2f993a
|
@ -55,7 +55,7 @@ The seed IV is used by the client as the IV for the next encrypted APDU.
|
||||||
* Data = 256-bit random number
|
* Data = 256-bit random number
|
||||||
* Response Data = 256-bit random number
|
* Response Data = 256-bit random number
|
||||||
* Response SW = 0x9000 on success, 0x6985 if the previous successfully executed APDU was not OPEN SECURE CHANNEL, 0x6982
|
* Response SW = 0x9000 on success, 0x6985 if the previous successfully executed APDU was not OPEN SECURE CHANNEL, 0x6982
|
||||||
if authentication failed, 0x6A80 if the data is not exactly 64 bytes long
|
if authentication failed or the data is not 256-bit long
|
||||||
|
|
||||||
This APDU allows both parties to verify that the keys generated in the OPEN SECURE CHANNEL step are matching and thus
|
This APDU allows both parties to verify that the keys generated in the OPEN SECURE CHANNEL step are matching and thus
|
||||||
guarantee authentication of the counterpart. The data sent by both parties is a 256-bit random number The APDU data is
|
guarantee authentication of the counterpart. The data sent by both parties is a 256-bit random number The APDU data is
|
||||||
|
@ -63,7 +63,6 @@ sent encrypted with the keys generated in the OPEN SECURE CHANNEL step. Each par
|
||||||
APDU. If the MAC can be verified, it means that both parties are using the same keys. Only after this step has been
|
APDU. If the MAC can be verified, it means that both parties are using the same keys. Only after this step has been
|
||||||
executed the secure channel can be considered to be open and other commands can be sent. If the authentication fails
|
executed the secure channel can be considered to be open and other commands can be sent. If the authentication fails
|
||||||
the card must respond with 0x6982. In this case the OPEN SECURE CHANNEL command must be repeated to generate new keys.
|
the card must respond with 0x6982. In this case the OPEN SECURE CHANNEL command must be repeated to generate new keys.
|
||||||
This greatly slows down brute-force attempts.
|
|
||||||
|
|
||||||
### PAIR
|
### PAIR
|
||||||
|
|
||||||
|
@ -123,9 +122,15 @@ happen.
|
||||||
### Encrypted APDUs
|
### Encrypted APDUs
|
||||||
|
|
||||||
After a successful OPEN SECURE CHANNEL command all communication between card and client is encrypted. Note that only
|
After a successful OPEN SECURE CHANNEL command all communication between card and client is encrypted. Note that only
|
||||||
the data fields of C-APDU and R-APDU are encrypted, which means that CLA, INS, P1, P2 for C-APDU and SW1SW2 for R-APDU
|
the data fields of C-APDU are encrypted, which means that CLA, INS, P1, P2 for C-APDU are plaintext. This means no
|
||||||
are plaintext. This means no sensitive data should be sent in these parameters. Additionally a MAC is calculated for the
|
sensitive data should be sent in these parameters. Additionally a MAC is calculated for the entire APDU, including
|
||||||
entire APDU, including the unencrypted fields.
|
the unencrypted fields.
|
||||||
|
|
||||||
|
Because R-APDU can only contain data if their SW is a success or warning status word (0x9000, 0x62XX, 0x63XX), when the
|
||||||
|
secure channel is open all responses will have SW 0x9000. The actual SW is always appended at the end of the response
|
||||||
|
data before encryption, which means the client must interpret the last two bytes of the plaintext response as the SW.
|
||||||
|
An exception to this is SW 0x6982, which indicates that the SecureChannel has been aborted and as such is returned
|
||||||
|
without any MAC.
|
||||||
|
|
||||||
To encrypt the data both the card and the client do the following:
|
To encrypt the data both the card and the client do the following:
|
||||||
|
|
||||||
|
@ -146,8 +151,8 @@ of transmitted bytes and guarantees protection from replay attacks. For the MAC
|
||||||
MAC generation for C-APDUs is calculated on the concatenation of CLA INS P1 P2 LC 00 00 00 00 00 00 00 00 00 00 00 and
|
MAC generation for C-APDUs is calculated on the concatenation of CLA INS P1 P2 LC 00 00 00 00 00 00 00 00 00 00 00 and
|
||||||
the encrypted data field. The 11-byte long padding does not become part of the data field and does not affect LC
|
the encrypted data field. The 11-byte long padding does not become part of the data field and does not affect LC
|
||||||
|
|
||||||
MAC generation fo R-APDUs is calculated on the concatenation of Lr SW1 SW2 00 00 00 00 00 00 00 00 00 00 00 00 00 and
|
MAC generation fo R-APDUs is calculated on the concatenation of Lr 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 and
|
||||||
the encrypted data field. The 13-byte long padding does not become part of the response field. Lr is the length of the
|
the encrypted data field. The 15-byte long padding does not become part of the response field. Lr is the length of the
|
||||||
encrypted response data field and is not transmitted.
|
encrypted response data field and is not transmitted.
|
||||||
|
|
||||||
Because AES in CBC mode requires the data field length in bytes to be a multiple of 16, the maximum effective APDU
|
Because AES in CBC mode requires the data field length in bytes to be a multiple of 16, the maximum effective APDU
|
||||||
|
|
|
@ -25,7 +25,7 @@ public class SecureChannel {
|
||||||
private AESKey scEncKey;
|
private AESKey scEncKey;
|
||||||
private AESKey scMacKey;
|
private AESKey scMacKey;
|
||||||
private Cipher scCipher;
|
private Cipher scCipher;
|
||||||
private Signature scSignature;
|
private Signature scMac;
|
||||||
private KeyPair scKeypair;
|
private KeyPair scKeypair;
|
||||||
private byte[] secret;
|
private byte[] secret;
|
||||||
private byte[] pairingSecret;
|
private byte[] pairingSecret;
|
||||||
|
@ -45,7 +45,8 @@ public class SecureChannel {
|
||||||
*/
|
*/
|
||||||
public SecureChannel(byte pairingLimit, byte[] aPairingSecret, short off) {
|
public SecureChannel(byte pairingLimit, byte[] aPairingSecret, short off) {
|
||||||
scCipher = Cipher.getInstance(Cipher.ALG_AES_CBC_ISO9797_M2,false);
|
scCipher = Cipher.getInstance(Cipher.ALG_AES_CBC_ISO9797_M2,false);
|
||||||
scSignature = Signature.getInstance(Signature.ALG_AES_MAC_128_NOPAD, false);
|
|
||||||
|
scMac = Signature.getInstance(Signature.ALG_AES_MAC_128_NOPAD, false);
|
||||||
|
|
||||||
scEncKey = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES_TRANSIENT_DESELECT, KeyBuilder.LENGTH_AES_256, false);
|
scEncKey = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES_TRANSIENT_DESELECT, KeyBuilder.LENGTH_AES_256, false);
|
||||||
scMacKey = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES_TRANSIENT_DESELECT, KeyBuilder.LENGTH_AES_256, false);
|
scMacKey = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES_TRANSIENT_DESELECT, KeyBuilder.LENGTH_AES_256, false);
|
||||||
|
@ -62,7 +63,6 @@ public class SecureChannel {
|
||||||
Util.arrayCopyNonAtomic(aPairingSecret, off, pairingSecret, (short) 0, SC_SECRET_LENGTH);
|
Util.arrayCopyNonAtomic(aPairingSecret, off, pairingSecret, (short) 0, SC_SECRET_LENGTH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes the OPEN SECURE CHANNEL command.
|
* Processes the OPEN SECURE CHANNEL command.
|
||||||
*
|
*
|
||||||
|
@ -99,7 +99,7 @@ public class SecureChannel {
|
||||||
scEncKey.setKey(secret, (short) 0);
|
scEncKey.setKey(secret, (short) 0);
|
||||||
scMacKey.setKey(secret, SC_SECRET_LENGTH);
|
scMacKey.setKey(secret, SC_SECRET_LENGTH);
|
||||||
Util.arrayCopyNonAtomic(apduBuffer, SC_SECRET_LENGTH, secret, (short) 0, SC_BLOCK_SIZE);
|
Util.arrayCopyNonAtomic(apduBuffer, SC_SECRET_LENGTH, secret, (short) 0, SC_BLOCK_SIZE);
|
||||||
Util.arrayFillNonAtomic(secret, SC_BLOCK_SIZE, SC_SECRET_LENGTH, (byte) 0);
|
Util.arrayFillNonAtomic(secret, SC_BLOCK_SIZE, (short) (secret.length - SC_BLOCK_SIZE), (byte) 0);
|
||||||
apdu.setOutgoingAndSend((short) 0, (short) (SC_SECRET_LENGTH + SC_BLOCK_SIZE));
|
apdu.setOutgoingAndSend((short) 0, (short) (SC_SECRET_LENGTH + SC_BLOCK_SIZE));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,29 +109,26 @@ public class SecureChannel {
|
||||||
* @param apdu the JCRE-owned APDU object.
|
* @param apdu the JCRE-owned APDU object.
|
||||||
*/
|
*/
|
||||||
public void mutuallyAuthenticate(APDU apdu) {
|
public void mutuallyAuthenticate(APDU apdu) {
|
||||||
if (!scEncKey.isInitialized() || mutuallyAuthenticated) {
|
if (!scEncKey.isInitialized()) {
|
||||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] apduBuffer = apdu.getBuffer();
|
boolean oldMutuallyAuthenticated = mutuallyAuthenticated;
|
||||||
|
mutuallyAuthenticated = true;
|
||||||
|
|
||||||
|
byte[] apduBuffer = apdu.getBuffer();
|
||||||
short len = preprocessAPDU(apduBuffer);
|
short len = preprocessAPDU(apduBuffer);
|
||||||
|
|
||||||
if (len != (short) (SC_SECRET_LENGTH * 2)) {
|
if (oldMutuallyAuthenticated) {
|
||||||
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
Crypto.sha256.doFinal(apduBuffer, ISO7816.OFFSET_CDATA, SC_SECRET_LENGTH, apduBuffer, ISO7816.OFFSET_CDATA);
|
if (len != SC_SECRET_LENGTH) {
|
||||||
|
|
||||||
if (Util.arrayCompare(apduBuffer, ISO7816.OFFSET_CDATA, apduBuffer, (short) (ISO7816.OFFSET_CDATA + SC_SECRET_LENGTH), SC_SECRET_LENGTH) != 0) {
|
|
||||||
reset();
|
reset();
|
||||||
ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);
|
ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
mutuallyAuthenticated = true;
|
|
||||||
|
|
||||||
Crypto.random.generateData(apduBuffer, SC_OUT_OFFSET, SC_SECRET_LENGTH);
|
Crypto.random.generateData(apduBuffer, SC_OUT_OFFSET, SC_SECRET_LENGTH);
|
||||||
Crypto.sha256.doFinal(apduBuffer, SC_OUT_OFFSET, SC_SECRET_LENGTH, apduBuffer, (short) (SC_OUT_OFFSET + SC_SECRET_LENGTH));
|
|
||||||
respond(apdu, len, ISO7816.SW_NO_ERROR);
|
respond(apdu, len, ISO7816.SW_NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,11 +246,7 @@ public class SecureChannel {
|
||||||
|
|
||||||
short apduLen = (short)((short) apduBuffer[ISO7816.OFFSET_LC] & 0xff);
|
short apduLen = (short)((short) apduBuffer[ISO7816.OFFSET_LC] & 0xff);
|
||||||
|
|
||||||
scSignature.init(scMacKey, Signature.MODE_VERIFY);
|
if (!verifyAESMAC(apduBuffer, apduLen)) {
|
||||||
scSignature.update(apduBuffer, (short) 0, ISO7816.OFFSET_CDATA);
|
|
||||||
scSignature.update(secret, SC_BLOCK_SIZE, (short) (SC_BLOCK_SIZE - ISO7816.OFFSET_CDATA));
|
|
||||||
|
|
||||||
if (!scSignature.verify(apduBuffer, (short) (ISO7816.OFFSET_CDATA + SC_BLOCK_SIZE), (short) (apduLen - SC_BLOCK_SIZE), apduBuffer, ISO7816.OFFSET_CDATA, SC_BLOCK_SIZE)) {
|
|
||||||
reset();
|
reset();
|
||||||
ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);
|
ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);
|
||||||
}
|
}
|
||||||
|
@ -267,10 +260,18 @@ public class SecureChannel {
|
||||||
return len;
|
return len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean verifyAESMAC(byte[] apduBuffer, short apduLen) {
|
||||||
|
scMac.init(scMacKey, Signature.MODE_VERIFY);
|
||||||
|
scMac.update(apduBuffer, (short) 0, ISO7816.OFFSET_CDATA);
|
||||||
|
scMac.update(secret, SC_BLOCK_SIZE, (short) (SC_BLOCK_SIZE - ISO7816.OFFSET_CDATA));
|
||||||
|
|
||||||
|
return scMac.verify(apduBuffer, (short) (ISO7816.OFFSET_CDATA + SC_BLOCK_SIZE), (short) (apduLen - SC_BLOCK_SIZE), apduBuffer, ISO7816.OFFSET_CDATA, SC_BLOCK_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends the response to the command. This method always throws an ISOException with the given SW, so nothing can be
|
* Sends the response to the command. This the given SW is appended to the data automatically. The response data must
|
||||||
* called after its execution. The response data must be placed starting at the SecureChannel.SC_OUT_OFFSET offset, to
|
* be placed starting at the SecureChannel.SC_OUT_OFFSET offset, to leave place for the SecureChannel-specific data at
|
||||||
* leave place for the SecureChannel-specific data at the beginning of the APDU.
|
* the beginning of the APDU.
|
||||||
*
|
*
|
||||||
* @param apdu the APDU object
|
* @param apdu the APDU object
|
||||||
* @param len the length of the plaintext
|
* @param len the length of the plaintext
|
||||||
|
@ -278,21 +279,27 @@ public class SecureChannel {
|
||||||
public void respond(APDU apdu, short len, short sw) {
|
public void respond(APDU apdu, short len, short sw) {
|
||||||
byte[] apduBuffer = apdu.getBuffer();
|
byte[] apduBuffer = apdu.getBuffer();
|
||||||
|
|
||||||
Util.setShort(apduBuffer, (short) 0, sw);
|
Util.setShort(apduBuffer, (short) (SC_OUT_OFFSET + len), sw);
|
||||||
|
len += 2;
|
||||||
|
|
||||||
scCipher.init(scEncKey, Cipher.MODE_ENCRYPT, secret, (short) 0, SC_BLOCK_SIZE);
|
scCipher.init(scEncKey, Cipher.MODE_ENCRYPT, secret, (short) 0, SC_BLOCK_SIZE);
|
||||||
len = scCipher.doFinal(apduBuffer, SC_OUT_OFFSET, len, apduBuffer, (short)(ISO7816.OFFSET_CDATA + SC_BLOCK_SIZE));
|
len = scCipher.doFinal(apduBuffer, SC_OUT_OFFSET, len, apduBuffer, (short)(ISO7816.OFFSET_CDATA + SC_BLOCK_SIZE));
|
||||||
|
|
||||||
scSignature.init(scMacKey, Signature.MODE_SIGN);
|
apduBuffer[0] = (byte) (len + SC_BLOCK_SIZE);
|
||||||
scSignature.update(apduBuffer, (short) 0, (short) 2);
|
|
||||||
scSignature.update(secret, SC_BLOCK_SIZE, (short)(SC_BLOCK_SIZE - 2));
|
computeAESMAC(len, apduBuffer);
|
||||||
scSignature.sign(apduBuffer, SC_OUT_OFFSET, len, apduBuffer, ISO7816.OFFSET_CDATA);
|
|
||||||
|
|
||||||
Util.arrayCopyNonAtomic(apduBuffer, ISO7816.OFFSET_CDATA, secret, (short) 0, SC_BLOCK_SIZE);
|
Util.arrayCopyNonAtomic(apduBuffer, ISO7816.OFFSET_CDATA, secret, (short) 0, SC_BLOCK_SIZE);
|
||||||
|
|
||||||
len += SC_BLOCK_SIZE;
|
len += SC_BLOCK_SIZE;
|
||||||
apdu.setOutgoingAndSend(ISO7816.OFFSET_CDATA, len);
|
apdu.setOutgoingAndSend(ISO7816.OFFSET_CDATA, len);
|
||||||
ISOException.throwIt(sw);
|
}
|
||||||
|
|
||||||
|
public void computeAESMAC(short len, byte[] apduBuffer) {
|
||||||
|
scMac.init(scMacKey, Signature.MODE_SIGN);
|
||||||
|
scMac.update(apduBuffer, (short) 0, (short) 1);
|
||||||
|
scMac.update(secret, SC_BLOCK_SIZE, (short)(SC_BLOCK_SIZE - 1));
|
||||||
|
scMac.sign(apduBuffer, (short)(ISO7816.OFFSET_CDATA + SC_BLOCK_SIZE), len, apduBuffer, ISO7816.OFFSET_CDATA);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -231,18 +231,22 @@ public class WalletApplet extends Applet {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch(ISOException sw) {
|
} catch(ISOException sw) {
|
||||||
if (secureChannel.isOpen() && apdu.getCurrentState() != APDU.STATE_FULL_OUTGOING) {
|
if (shouldRespond(apdu) && (sw.getReason() != ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED)) {
|
||||||
secureChannel.respond(apdu, (short) 0, sw.getReason());
|
secureChannel.respond(apdu, (short) 0, sw.getReason());
|
||||||
} else {
|
} else {
|
||||||
throw sw;
|
throw sw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (secureChannel.isOpen()) {
|
if (shouldRespond(apdu)) {
|
||||||
secureChannel.respond(apdu, (short) 0, ISO7816.SW_NO_ERROR);
|
secureChannel.respond(apdu, (short) 0, ISO7816.SW_NO_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean shouldRespond(APDU apdu) {
|
||||||
|
return secureChannel.isOpen() && (apdu.getCurrentState() != APDU.STATE_FULL_OUTGOING);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks that the PIN is validated and if it is call the unpair method of the secure channel. If the PIN is not
|
* Checks that the PIN is validated and if it is call the unpair method of the secure channel. If the PIN is not
|
||||||
* validated the 0x6985 exception is thrown.
|
* validated the 0x6985 exception is thrown.
|
||||||
|
@ -292,11 +296,8 @@ public class WalletApplet extends Applet {
|
||||||
* @param apdu the JCRE-owned APDU object.
|
* @param apdu the JCRE-owned APDU object.
|
||||||
*/
|
*/
|
||||||
private void getStatus(APDU apdu) {
|
private void getStatus(APDU apdu) {
|
||||||
if (!secureChannel.isOpen()) {
|
|
||||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] apduBuffer = apdu.getBuffer();
|
byte[] apduBuffer = apdu.getBuffer();
|
||||||
|
secureChannel.preprocessAPDU(apduBuffer);
|
||||||
|
|
||||||
short len;
|
short len;
|
||||||
|
|
||||||
|
@ -367,10 +368,6 @@ public class WalletApplet extends Applet {
|
||||||
* @param apdu the JCRE-owned APDU object.
|
* @param apdu the JCRE-owned APDU object.
|
||||||
*/
|
*/
|
||||||
private void verifyPIN(APDU apdu) {
|
private void verifyPIN(APDU apdu) {
|
||||||
if (!secureChannel.isOpen()) {
|
|
||||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] apduBuffer = apdu.getBuffer();
|
byte[] apduBuffer = apdu.getBuffer();
|
||||||
byte len = (byte) secureChannel.preprocessAPDU(apduBuffer);
|
byte len = (byte) secureChannel.preprocessAPDU(apduBuffer);
|
||||||
|
|
||||||
|
@ -387,13 +384,13 @@ public class WalletApplet extends Applet {
|
||||||
* @param apdu the JCRE-owned APDU object.
|
* @param apdu the JCRE-owned APDU object.
|
||||||
*/
|
*/
|
||||||
private void changePIN(APDU apdu) {
|
private void changePIN(APDU apdu) {
|
||||||
if (!(secureChannel.isOpen() && pin.isValidated())) {
|
|
||||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] apduBuffer = apdu.getBuffer();
|
byte[] apduBuffer = apdu.getBuffer();
|
||||||
byte len = (byte) secureChannel.preprocessAPDU(apduBuffer);
|
byte len = (byte) secureChannel.preprocessAPDU(apduBuffer);
|
||||||
|
|
||||||
|
if (!pin.isValidated()) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||||
|
}
|
||||||
|
|
||||||
if (!(len == PIN_LENGTH && allDigits(apduBuffer, ISO7816.OFFSET_CDATA, len))) {
|
if (!(len == PIN_LENGTH && allDigits(apduBuffer, ISO7816.OFFSET_CDATA, len))) {
|
||||||
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
||||||
}
|
}
|
||||||
|
@ -411,13 +408,13 @@ public class WalletApplet extends Applet {
|
||||||
* @param apdu the JCRE-owned APDU object.
|
* @param apdu the JCRE-owned APDU object.
|
||||||
*/
|
*/
|
||||||
private void unblockPIN(APDU apdu) {
|
private void unblockPIN(APDU apdu) {
|
||||||
if (!(secureChannel.isOpen() && pin.getTriesRemaining() == 0)) {
|
|
||||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] apduBuffer = apdu.getBuffer();
|
byte[] apduBuffer = apdu.getBuffer();
|
||||||
byte len = (byte) secureChannel.preprocessAPDU(apduBuffer);
|
byte len = (byte) secureChannel.preprocessAPDU(apduBuffer);
|
||||||
|
|
||||||
|
if (pin.getTriesRemaining() != 0) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||||
|
}
|
||||||
|
|
||||||
if (!(len == (PUK_LENGTH + PIN_LENGTH) && allDigits(apduBuffer, ISO7816.OFFSET_CDATA, len))) {
|
if (!(len == (PUK_LENGTH + PIN_LENGTH) && allDigits(apduBuffer, ISO7816.OFFSET_CDATA, len))) {
|
||||||
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
||||||
}
|
}
|
||||||
|
@ -442,13 +439,13 @@ public class WalletApplet extends Applet {
|
||||||
* @param apdu the JCRE-owned APDU object.
|
* @param apdu the JCRE-owned APDU object.
|
||||||
*/
|
*/
|
||||||
private void loadKey(APDU apdu) {
|
private void loadKey(APDU apdu) {
|
||||||
if (!(secureChannel.isOpen() && pin.isValidated())) {
|
byte[] apduBuffer = apdu.getBuffer();
|
||||||
|
secureChannel.preprocessAPDU(apduBuffer);
|
||||||
|
|
||||||
|
if (!pin.isValidated()) {
|
||||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] apduBuffer = apdu.getBuffer();
|
|
||||||
|
|
||||||
secureChannel.preprocessAPDU(apduBuffer);
|
|
||||||
boolean newExtended = false;
|
boolean newExtended = false;
|
||||||
|
|
||||||
switch (apduBuffer[ISO7816.OFFSET_P1]) {
|
switch (apduBuffer[ISO7816.OFFSET_P1]) {
|
||||||
|
@ -586,14 +583,13 @@ public class WalletApplet extends Applet {
|
||||||
* @param apdu the JCRE-owned APDU object.
|
* @param apdu the JCRE-owned APDU object.
|
||||||
*/
|
*/
|
||||||
private void deriveKey(APDU apdu) {
|
private void deriveKey(APDU apdu) {
|
||||||
if (!(secureChannel.isOpen() && (pin.isValidated() || (pinlessPathLen > 0)) && isExtended)) {
|
byte[] apduBuffer = apdu.getBuffer();
|
||||||
|
short len = secureChannel.preprocessAPDU(apduBuffer);
|
||||||
|
|
||||||
|
if (!((pin.isValidated() || (pinlessPathLen > 0)) && isExtended)) {
|
||||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] apduBuffer = apdu.getBuffer();
|
|
||||||
|
|
||||||
short len = secureChannel.preprocessAPDU(apduBuffer);
|
|
||||||
|
|
||||||
boolean assistedDerivation = (apduBuffer[ISO7816.OFFSET_P1] & DERIVE_P1_ASSISTED_MASK) == DERIVE_P1_ASSISTED_MASK;
|
boolean assistedDerivation = (apduBuffer[ISO7816.OFFSET_P1] & DERIVE_P1_ASSISTED_MASK) == DERIVE_P1_ASSISTED_MASK;
|
||||||
boolean isPublicKey = apduBuffer[ISO7816.OFFSET_P2] == DERIVE_P2_PUBLIC_KEY;
|
boolean isPublicKey = apduBuffer[ISO7816.OFFSET_P2] == DERIVE_P2_PUBLIC_KEY;
|
||||||
boolean isReset = (apduBuffer[ISO7816.OFFSET_P1] & DERIVE_P1_APPEND_MASK) != DERIVE_P1_APPEND_MASK;
|
boolean isReset = (apduBuffer[ISO7816.OFFSET_P1] & DERIVE_P1_APPEND_MASK) != DERIVE_P1_APPEND_MASK;
|
||||||
|
@ -704,11 +700,9 @@ public class WalletApplet extends Applet {
|
||||||
* @param apdu the JCRE-owned APDU object.
|
* @param apdu the JCRE-owned APDU object.
|
||||||
*/
|
*/
|
||||||
private void generateMnemonic(APDU apdu) {
|
private void generateMnemonic(APDU apdu) {
|
||||||
if (!secureChannel.isOpen()) {
|
|
||||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] apduBuffer = apdu.getBuffer();
|
byte[] apduBuffer = apdu.getBuffer();
|
||||||
|
secureChannel.preprocessAPDU(apduBuffer);
|
||||||
|
|
||||||
short csLen = apduBuffer[ISO7816.OFFSET_P1];
|
short csLen = apduBuffer[ISO7816.OFFSET_P1];
|
||||||
|
|
||||||
if (csLen < GENERATE_MNEMONIC_P1_CS_MIN || csLen > GENERATE_MNEMONIC_P1_CS_MAX) {
|
if (csLen < GENERATE_MNEMONIC_P1_CS_MIN || csLen > GENERATE_MNEMONIC_P1_CS_MAX) {
|
||||||
|
@ -785,12 +779,13 @@ public class WalletApplet extends Applet {
|
||||||
* @param apdu the JCRE-owned APDU object.
|
* @param apdu the JCRE-owned APDU object.
|
||||||
*/
|
*/
|
||||||
private void sign(APDU apdu) {
|
private void sign(APDU apdu) {
|
||||||
if (!(secureChannel.isOpen() && (pin.isValidated() || isPinless()) && privateKey.isInitialized() && !expectPublicKey)) {
|
byte[] apduBuffer = apdu.getBuffer();
|
||||||
|
short len = secureChannel.preprocessAPDU(apduBuffer);
|
||||||
|
|
||||||
|
if (!((pin.isValidated() || isPinless()) && privateKey.isInitialized() && !expectPublicKey)) {
|
||||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] apduBuffer = apdu.getBuffer();
|
|
||||||
|
|
||||||
if ((apduBuffer[ISO7816.OFFSET_P2] & SIGN_P2_FIRST_BLOCK_MASK) == SIGN_P2_FIRST_BLOCK_MASK) {
|
if ((apduBuffer[ISO7816.OFFSET_P2] & SIGN_P2_FIRST_BLOCK_MASK) == SIGN_P2_FIRST_BLOCK_MASK) {
|
||||||
signInProgress = true;
|
signInProgress = true;
|
||||||
signature.init(privateKey, Signature.MODE_SIGN);
|
signature.init(privateKey, Signature.MODE_SIGN);
|
||||||
|
@ -798,8 +793,6 @@ public class WalletApplet extends Applet {
|
||||||
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
|
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
|
||||||
}
|
}
|
||||||
|
|
||||||
short len = secureChannel.preprocessAPDU(apduBuffer);
|
|
||||||
|
|
||||||
if ((apduBuffer[ISO7816.OFFSET_P2] & SIGN_P2_LAST_BLOCK_MASK) == SIGN_P2_LAST_BLOCK_MASK) {
|
if ((apduBuffer[ISO7816.OFFSET_P2] & SIGN_P2_LAST_BLOCK_MASK) == SIGN_P2_LAST_BLOCK_MASK) {
|
||||||
signInProgress = false;
|
signInProgress = false;
|
||||||
|
|
||||||
|
@ -838,13 +831,13 @@ public class WalletApplet extends Applet {
|
||||||
* @param apdu the JCRE-owned APDU object.
|
* @param apdu the JCRE-owned APDU object.
|
||||||
*/
|
*/
|
||||||
private void setPinlessPath(APDU apdu) {
|
private void setPinlessPath(APDU apdu) {
|
||||||
if (!(secureChannel.isOpen() && pin.isValidated())) {
|
|
||||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] apduBuffer = apdu.getBuffer();
|
byte[] apduBuffer = apdu.getBuffer();
|
||||||
short len = secureChannel.preprocessAPDU(apduBuffer);
|
short len = secureChannel.preprocessAPDU(apduBuffer);
|
||||||
|
|
||||||
|
if (!pin.isValidated()) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||||
|
}
|
||||||
|
|
||||||
if (((short) (len % 4) != 0) || (len > pinlessPath.length)) {
|
if (((short) (len % 4) != 0) || (len > pinlessPath.length)) {
|
||||||
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
||||||
}
|
}
|
||||||
|
@ -864,11 +857,13 @@ public class WalletApplet extends Applet {
|
||||||
* @param apdu the JCRE-owned APDU object.
|
* @param apdu the JCRE-owned APDU object.
|
||||||
*/
|
*/
|
||||||
private void exportKey(APDU apdu) {
|
private void exportKey(APDU apdu) {
|
||||||
if (!(secureChannel.isOpen() && pin.isValidated())) {
|
byte[] apduBuffer = apdu.getBuffer();
|
||||||
|
secureChannel.preprocessAPDU(apduBuffer);
|
||||||
|
|
||||||
|
if (!pin.isValidated()) {
|
||||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] apduBuffer = apdu.getBuffer();
|
|
||||||
byte[] toExport;
|
byte[] toExport;
|
||||||
|
|
||||||
switch (apduBuffer[ISO7816.OFFSET_P1]) {
|
switch (apduBuffer[ISO7816.OFFSET_P1]) {
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
package im.status.wallet;
|
package im.status.wallet;
|
||||||
|
|
||||||
|
import org.bouncycastle.crypto.engines.AESEngine;
|
||||||
|
import org.bouncycastle.crypto.macs.CBCBlockCipherMac;
|
||||||
|
import org.bouncycastle.crypto.params.KeyParameter;
|
||||||
import org.bouncycastle.jce.ECNamedCurveTable;
|
import org.bouncycastle.jce.ECNamedCurveTable;
|
||||||
import org.bouncycastle.jce.interfaces.ECPublicKey;
|
import org.bouncycastle.jce.interfaces.ECPublicKey;
|
||||||
import org.bouncycastle.jce.spec.ECParameterSpec;
|
import org.bouncycastle.jce.spec.ECParameterSpec;
|
||||||
|
@ -25,10 +28,14 @@ public class SecureChannelSession {
|
||||||
private byte[] secret;
|
private byte[] secret;
|
||||||
private byte[] publicKey;
|
private byte[] publicKey;
|
||||||
private byte[] pairingKey;
|
private byte[] pairingKey;
|
||||||
|
private byte[] iv;
|
||||||
private byte pairingIndex;
|
private byte pairingIndex;
|
||||||
private Cipher sessionCipher;
|
private Cipher sessionCipher;
|
||||||
private SecretKeySpec sessionKey;
|
private CBCBlockCipherMac sessionMac;
|
||||||
|
private SecretKeySpec sessionEncKey;
|
||||||
|
private KeyParameter sessionMacKey;
|
||||||
private SecureRandom random;
|
private SecureRandom random;
|
||||||
|
private boolean open;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a SecureChannel session on the client. The client should generate a fresh key pair for each session.
|
* Constructs a SecureChannel session on the client. The client should generate a fresh key pair for each session.
|
||||||
|
@ -55,6 +62,7 @@ public class SecureChannelSession {
|
||||||
keyAgreement.doPhase(cardKey, true);
|
keyAgreement.doPhase(cardKey, true);
|
||||||
secret = keyAgreement.generateSecret();
|
secret = keyAgreement.generateSecret();
|
||||||
|
|
||||||
|
open = false;
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
|
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
|
||||||
}
|
}
|
||||||
|
@ -78,10 +86,7 @@ public class SecureChannelSession {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Establishes a Secure Channel with the card. The command parameters are the public key generated in the first step.
|
* Establishes a Secure Channel with the card. The command parameters are the public key generated in the first step.
|
||||||
* The card returns a secret value which must be appended to the secret previously generated through the EC-DH
|
* Follows the specifications from the SECURE_CHANNEL.md document.
|
||||||
* algorithm. This entire value must be hashed using SHA-256. The hash will be used as the key for an AES CBC cipher
|
|
||||||
* using ISO9797-1 Method 2 padding. From this point all further APDU must be sent encrypted and all responses from
|
|
||||||
* the card must be decrypted using this secure channel.
|
|
||||||
*
|
*
|
||||||
* @param apduChannel the apdu channel
|
* @param apduChannel the apdu channel
|
||||||
* @return the card response
|
* @return the card response
|
||||||
|
@ -108,17 +113,24 @@ public class SecureChannelSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes the response from OPEN SECURE CHANNEL. This initialize the session key and cipher internally.
|
* Processes the response from OPEN SECURE CHANNEL. This initialize the session keys, Cipher and MAC internally.
|
||||||
*
|
*
|
||||||
* @param response the card response
|
* @param response the card response
|
||||||
*/
|
*/
|
||||||
public void processOpenSecureChannelResponse(ResponseAPDU response) {
|
public void processOpenSecureChannelResponse(ResponseAPDU response) {
|
||||||
try {
|
try {
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA256", "BC");
|
MessageDigest md = MessageDigest.getInstance("SHA512", "BC");
|
||||||
md.update(secret);
|
md.update(secret);
|
||||||
md.update(pairingKey);
|
md.update(pairingKey);
|
||||||
sessionKey = new SecretKeySpec(md.digest(response.getData()), "AES");
|
byte[] data = response.getData();
|
||||||
|
byte[] keyData = md.digest(Arrays.copyOf(data, SecureChannel.SC_SECRET_LENGTH));
|
||||||
|
iv = Arrays.copyOfRange(data, SecureChannel.SC_SECRET_LENGTH, data.length);
|
||||||
|
|
||||||
|
sessionEncKey = new SecretKeySpec(Arrays.copyOf(keyData, SecureChannel.SC_SECRET_LENGTH), "AES");
|
||||||
|
sessionMacKey = new KeyParameter(keyData, SecureChannel.SC_SECRET_LENGTH, SecureChannel.SC_SECRET_LENGTH);
|
||||||
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding", "BC");
|
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding", "BC");
|
||||||
|
sessionMac = new CBCBlockCipherMac(new AESEngine(), 128, null);
|
||||||
|
open = true;
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
|
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
|
||||||
}
|
}
|
||||||
|
@ -131,20 +143,7 @@ public class SecureChannelSession {
|
||||||
* @return true if response is correct, false otherwise
|
* @return true if response is correct, false otherwise
|
||||||
*/
|
*/
|
||||||
public boolean verifyMutuallyAuthenticateResponse(ResponseAPDU response) {
|
public boolean verifyMutuallyAuthenticateResponse(ResponseAPDU response) {
|
||||||
MessageDigest md;
|
return response.getNr() == SecureChannel.SC_SECRET_LENGTH;
|
||||||
|
|
||||||
try {
|
|
||||||
md = MessageDigest.getInstance("SHA256", "BC");
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] data = decryptAPDU(response.getData());
|
|
||||||
md.update(data, 0, SecureChannel.SC_SECRET_LENGTH);
|
|
||||||
byte[] digest = md.digest();
|
|
||||||
data = Arrays.copyOfRange(data, SecureChannel.SC_SECRET_LENGTH, data.length);
|
|
||||||
|
|
||||||
return Arrays.equals(data, digest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -204,7 +203,7 @@ public class SecureChannelSession {
|
||||||
* @throws CardException communication error
|
* @throws CardException communication error
|
||||||
*/
|
*/
|
||||||
public void autoUnpair(CardChannel apduChannel) throws CardException {
|
public void autoUnpair(CardChannel apduChannel) throws CardException {
|
||||||
ResponseAPDU resp = unpair(apduChannel, pairingIndex, new byte[] { pairingIndex });
|
ResponseAPDU resp = unpair(apduChannel, pairingIndex);
|
||||||
|
|
||||||
if (resp.getSW() != 0x9000) {
|
if (resp.getSW() != 0x9000) {
|
||||||
throw new CardException("Unpairing failed");
|
throw new CardException("Unpairing failed");
|
||||||
|
@ -221,6 +220,7 @@ public class SecureChannelSession {
|
||||||
* @throws CardException communication error
|
* @throws CardException communication error
|
||||||
*/
|
*/
|
||||||
public ResponseAPDU openSecureChannel(CardChannel apduChannel, byte index, byte[] data) throws CardException {
|
public ResponseAPDU openSecureChannel(CardChannel apduChannel, byte index, byte[] data) throws CardException {
|
||||||
|
open = false;
|
||||||
CommandAPDU openSecureChannel = new CommandAPDU(0x80, SecureChannel.INS_OPEN_SECURE_CHANNEL, index, 0, data);
|
CommandAPDU openSecureChannel = new CommandAPDU(0x80, SecureChannel.INS_OPEN_SECURE_CHANNEL, index, 0, data);
|
||||||
return apduChannel.transmit(openSecureChannel);
|
return apduChannel.transmit(openSecureChannel);
|
||||||
}
|
}
|
||||||
|
@ -233,19 +233,8 @@ public class SecureChannelSession {
|
||||||
* @throws CardException communication error
|
* @throws CardException communication error
|
||||||
*/
|
*/
|
||||||
public ResponseAPDU mutuallyAuthenticate(CardChannel apduChannel) throws CardException {
|
public ResponseAPDU mutuallyAuthenticate(CardChannel apduChannel) throws CardException {
|
||||||
MessageDigest md;
|
|
||||||
|
|
||||||
try {
|
|
||||||
md = MessageDigest.getInstance("SHA256", "BC");
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] data = new byte[SecureChannel.SC_SECRET_LENGTH];
|
byte[] data = new byte[SecureChannel.SC_SECRET_LENGTH];
|
||||||
random.nextBytes(data);
|
random.nextBytes(data);
|
||||||
byte[] digest = md.digest(data);
|
|
||||||
data = Arrays.copyOf(data, SecureChannel.SC_SECRET_LENGTH * 2);
|
|
||||||
System.arraycopy(digest, 0, data, SecureChannel.SC_SECRET_LENGTH, SecureChannel.SC_SECRET_LENGTH);
|
|
||||||
|
|
||||||
return mutuallyAuthenticate(apduChannel, data);
|
return mutuallyAuthenticate(apduChannel, data);
|
||||||
}
|
}
|
||||||
|
@ -259,8 +248,8 @@ public class SecureChannelSession {
|
||||||
* @throws CardException communication error
|
* @throws CardException communication error
|
||||||
*/
|
*/
|
||||||
public ResponseAPDU mutuallyAuthenticate(CardChannel apduChannel, byte[] data) throws CardException {
|
public ResponseAPDU mutuallyAuthenticate(CardChannel apduChannel, byte[] data) throws CardException {
|
||||||
CommandAPDU mutuallyAuthenticate = new CommandAPDU(0x80, SecureChannel.INS_MUTUALLY_AUTHENTICATE, 0, 0, encryptAPDU(data));
|
CommandAPDU mutuallyAuthenticate = protectedCommand(0x80, SecureChannel.INS_MUTUALLY_AUTHENTICATE, 0, 0, data);
|
||||||
return apduChannel.transmit(mutuallyAuthenticate);
|
return transmit(apduChannel, mutuallyAuthenticate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -274,7 +263,7 @@ public class SecureChannelSession {
|
||||||
*/
|
*/
|
||||||
public ResponseAPDU pair(CardChannel apduChannel, byte p1, byte[] data) throws CardException {
|
public ResponseAPDU pair(CardChannel apduChannel, byte p1, byte[] data) throws CardException {
|
||||||
CommandAPDU openSecureChannel = new CommandAPDU(0x80, SecureChannel.INS_PAIR, p1, 0, data);
|
CommandAPDU openSecureChannel = new CommandAPDU(0x80, SecureChannel.INS_PAIR, p1, 0, data);
|
||||||
return apduChannel.transmit(openSecureChannel);
|
return transmit(apduChannel, openSecureChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -282,13 +271,12 @@ public class SecureChannelSession {
|
||||||
*
|
*
|
||||||
* @param apduChannel the apdu channel
|
* @param apduChannel the apdu channel
|
||||||
* @param p1 the P1 parameter
|
* @param p1 the P1 parameter
|
||||||
* @param data the data
|
|
||||||
* @return the raw card response
|
* @return the raw card response
|
||||||
* @throws CardException communication error
|
* @throws CardException communication error
|
||||||
*/
|
*/
|
||||||
public ResponseAPDU unpair(CardChannel apduChannel, byte p1, byte[] data) throws CardException {
|
public ResponseAPDU unpair(CardChannel apduChannel, byte p1) throws CardException {
|
||||||
CommandAPDU openSecureChannel = new CommandAPDU(0x80, SecureChannel.INS_UNPAIR, p1, 0, encryptAPDU(data));
|
CommandAPDU openSecureChannel = protectedCommand(0x80, SecureChannel.INS_UNPAIR, p1, 0, new byte[0]);
|
||||||
return apduChannel.transmit(openSecureChannel);
|
return transmit(apduChannel, openSecureChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -299,27 +287,14 @@ public class SecureChannelSession {
|
||||||
* @param data the plaintext data
|
* @param data the plaintext data
|
||||||
* @return the encrypted data
|
* @return the encrypted data
|
||||||
*/
|
*/
|
||||||
public byte[] encryptAPDU(byte[] data) {
|
private byte[] encryptAPDU(byte[] data) {
|
||||||
assert data.length <= PAYLOAD_MAX_SIZE;
|
assert data.length <= PAYLOAD_MAX_SIZE;
|
||||||
|
|
||||||
if (sessionKey == null) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
int ivSize = sessionCipher.getBlockSize();
|
|
||||||
byte[] iv = new byte[ivSize];
|
|
||||||
random.nextBytes(iv);
|
|
||||||
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
|
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
|
||||||
|
|
||||||
sessionCipher.init(Cipher.ENCRYPT_MODE, sessionKey, ivParameterSpec);
|
sessionCipher.init(Cipher.ENCRYPT_MODE, sessionEncKey, ivParameterSpec);
|
||||||
byte[] encrypted = sessionCipher.doFinal(data);
|
return sessionCipher.doFinal(data);
|
||||||
|
|
||||||
byte[] result = new byte[ivSize + encrypted.length];
|
|
||||||
System.arraycopy(iv, 0, result, 0, ivSize);
|
|
||||||
System.arraycopy(encrypted, 0, result, ivSize, encrypted.length);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
|
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
|
||||||
}
|
}
|
||||||
|
@ -332,18 +307,75 @@ public class SecureChannelSession {
|
||||||
* @param data the ciphetext
|
* @param data the ciphetext
|
||||||
* @return the plaintext
|
* @return the plaintext
|
||||||
*/
|
*/
|
||||||
public byte[] decryptAPDU(byte[] data) {
|
private byte[] decryptAPDU(byte[] data) {
|
||||||
if (sessionKey == null) {
|
try {
|
||||||
return data;
|
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
|
||||||
|
sessionCipher.init(Cipher.DECRYPT_MODE, sessionEncKey, ivParameterSpec);
|
||||||
|
return sessionCipher.doFinal(data);
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CommandAPDU protectedCommand(int cla, int ins, int p1, int p2, byte[] data) {
|
||||||
|
byte[] finalData;
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
data = encryptAPDU(data);
|
||||||
|
byte[] meta = new byte[]{(byte) cla, (byte) ins, (byte) p1, (byte) p2, (byte) (data.length + SecureChannel.SC_BLOCK_SIZE), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||||
|
updateIV(meta, data);
|
||||||
|
|
||||||
|
finalData = Arrays.copyOf(iv, iv.length + data.length);
|
||||||
|
System.arraycopy(data, 0, finalData, iv.length, data.length);
|
||||||
|
} else {
|
||||||
|
finalData = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CommandAPDU(cla, ins, p1, p2, finalData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseAPDU transmit(CardChannel apduChannel, CommandAPDU apdu) throws CardException {
|
||||||
|
ResponseAPDU resp = apduChannel.transmit(apdu);
|
||||||
|
|
||||||
|
if (resp.getSW() == 0x6982) {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
byte[] data = resp.getData();
|
||||||
|
byte[] meta = new byte[]{(byte) data.length, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||||
|
byte[] mac = Arrays.copyOf(data, iv.length);
|
||||||
|
data = Arrays.copyOfRange(data, iv.length, data.length);
|
||||||
|
|
||||||
|
byte[] plainData = decryptAPDU(data);
|
||||||
|
|
||||||
|
updateIV(meta, data);
|
||||||
|
|
||||||
|
if (!Arrays.equals(iv, mac)) {
|
||||||
|
throw new CardException("Invalid MAC");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ResponseAPDU(plainData);
|
||||||
|
} else {
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOpen() {
|
||||||
|
open = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateIV(byte[] meta, byte[] data) {
|
||||||
try {
|
try {
|
||||||
int ivSize = sessionCipher.getBlockSize();
|
sessionMac.init(sessionMacKey);
|
||||||
IvParameterSpec ivParameterSpec = new IvParameterSpec(data, 0, ivSize);
|
sessionMac.update(meta, 0, meta.length);
|
||||||
sessionCipher.init(Cipher.DECRYPT_MODE, sessionKey, ivParameterSpec);
|
sessionMac.update(data, 0, data.length);
|
||||||
return sessionCipher.doFinal(data, ivSize, data.length - ivSize);
|
sessionMac.doFinal(iv, 0);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
|
||||||
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
|
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,10 @@ public class WalletAppletCommandSet {
|
||||||
* @throws CardException communication error
|
* @throws CardException communication error
|
||||||
*/
|
*/
|
||||||
public ResponseAPDU select() throws CardException {
|
public ResponseAPDU select() throws CardException {
|
||||||
|
if (secureChannel != null) {
|
||||||
|
secureChannel.reset();
|
||||||
|
}
|
||||||
|
|
||||||
CommandAPDU selectApplet = new CommandAPDU(ISO7816.CLA_ISO7816, ISO7816.INS_SELECT, 4, 0, APPLET_AID_BYTES);
|
CommandAPDU selectApplet = new CommandAPDU(ISO7816.CLA_ISO7816, ISO7816.INS_SELECT, 4, 0, APPLET_AID_BYTES);
|
||||||
return apduChannel.transmit(selectApplet);
|
return apduChannel.transmit(selectApplet);
|
||||||
}
|
}
|
||||||
|
@ -104,8 +108,8 @@ public class WalletAppletCommandSet {
|
||||||
/**
|
/**
|
||||||
* Sends a UNPAIR APDU. Calls the corresponding method of the SecureChannel class.
|
* Sends a UNPAIR APDU. Calls the corresponding method of the SecureChannel class.
|
||||||
*/
|
*/
|
||||||
public ResponseAPDU unpair(byte p1, byte[] data) throws CardException {
|
public ResponseAPDU unpair(byte p1) throws CardException {
|
||||||
return secureChannel.unpair(apduChannel, p1, data);
|
return secureChannel.unpair(apduChannel, p1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -117,8 +121,8 @@ public class WalletAppletCommandSet {
|
||||||
* @throws CardException communication error
|
* @throws CardException communication error
|
||||||
*/
|
*/
|
||||||
public ResponseAPDU getStatus(byte info) throws CardException {
|
public ResponseAPDU getStatus(byte info) throws CardException {
|
||||||
CommandAPDU getStatus = new CommandAPDU(0x80, WalletApplet.INS_GET_STATUS, info, 0, 256);
|
CommandAPDU getStatus = secureChannel.protectedCommand(0x80, WalletApplet.INS_GET_STATUS, info, 0, new byte[0]);
|
||||||
return apduChannel.transmit(getStatus);
|
return secureChannel.transmit(apduChannel, getStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -130,7 +134,7 @@ public class WalletAppletCommandSet {
|
||||||
*/
|
*/
|
||||||
public boolean getPublicKeyDerivationSupport() throws CardException {
|
public boolean getPublicKeyDerivationSupport() throws CardException {
|
||||||
ResponseAPDU resp = getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
|
ResponseAPDU resp = getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
|
||||||
byte[] data = secureChannel.decryptAPDU(resp.getData());
|
byte[] data = resp.getData();
|
||||||
return data[data.length - 1] == 1;
|
return data[data.length - 1] == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,8 +147,8 @@ public class WalletAppletCommandSet {
|
||||||
* @throws CardException communication error
|
* @throws CardException communication error
|
||||||
*/
|
*/
|
||||||
public ResponseAPDU verifyPIN(String pin) throws CardException {
|
public ResponseAPDU verifyPIN(String pin) throws CardException {
|
||||||
CommandAPDU verifyPIN = new CommandAPDU(0x80, WalletApplet.INS_VERIFY_PIN, 0, 0, secureChannel.encryptAPDU(pin.getBytes()));
|
CommandAPDU verifyPIN = secureChannel.protectedCommand(0x80, WalletApplet.INS_VERIFY_PIN, 0, 0, pin.getBytes());
|
||||||
return apduChannel.transmit(verifyPIN);
|
return secureChannel.transmit(apduChannel, verifyPIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -156,8 +160,8 @@ public class WalletAppletCommandSet {
|
||||||
* @throws CardException communication error
|
* @throws CardException communication error
|
||||||
*/
|
*/
|
||||||
public ResponseAPDU changePIN(String pin) throws CardException {
|
public ResponseAPDU changePIN(String pin) throws CardException {
|
||||||
CommandAPDU changePIN = new CommandAPDU(0x80, WalletApplet.INS_CHANGE_PIN, 0, 0, secureChannel.encryptAPDU(pin.getBytes()));
|
CommandAPDU changePIN = secureChannel.protectedCommand(0x80, WalletApplet.INS_CHANGE_PIN, 0, 0, pin.getBytes());
|
||||||
return apduChannel.transmit(changePIN);
|
return secureChannel.transmit(apduChannel, changePIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -168,8 +172,8 @@ public class WalletAppletCommandSet {
|
||||||
* @throws CardException communication error
|
* @throws CardException communication error
|
||||||
*/
|
*/
|
||||||
public ResponseAPDU unblockPIN(String puk, String newPin) throws CardException {
|
public ResponseAPDU unblockPIN(String puk, String newPin) throws CardException {
|
||||||
CommandAPDU unblockPIN = new CommandAPDU(0x80, WalletApplet.INS_UNBLOCK_PIN, 0, 0, secureChannel.encryptAPDU((puk + newPin).getBytes()));
|
CommandAPDU unblockPIN = secureChannel.protectedCommand(0x80, WalletApplet.INS_UNBLOCK_PIN, 0, 0, (puk + newPin).getBytes());
|
||||||
return apduChannel.transmit(unblockPIN);
|
return secureChannel.transmit(apduChannel, unblockPIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -335,8 +339,8 @@ public class WalletAppletCommandSet {
|
||||||
* @throws CardException communication error
|
* @throws CardException communication error
|
||||||
*/
|
*/
|
||||||
public ResponseAPDU loadKey(byte[] data, byte keyType) throws CardException {
|
public ResponseAPDU loadKey(byte[] data, byte keyType) throws CardException {
|
||||||
CommandAPDU loadKey = new CommandAPDU(0x80, WalletApplet.INS_LOAD_KEY, keyType, 0, secureChannel.encryptAPDU(data));
|
CommandAPDU loadKey = secureChannel.protectedCommand(0x80, WalletApplet.INS_LOAD_KEY, keyType, 0, data);
|
||||||
return apduChannel.transmit(loadKey);
|
return secureChannel.transmit(apduChannel, loadKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -347,8 +351,8 @@ public class WalletAppletCommandSet {
|
||||||
* @throws CardException communication error
|
* @throws CardException communication error
|
||||||
*/
|
*/
|
||||||
public ResponseAPDU generateMnemonic(int cs) throws CardException {
|
public ResponseAPDU generateMnemonic(int cs) throws CardException {
|
||||||
CommandAPDU generateMnemonic = new CommandAPDU(0x80, WalletApplet.INS_GENERATE_MNEMONIC, cs, 0, 256);
|
CommandAPDU generateMnemonic = secureChannel.protectedCommand(0x80, WalletApplet.INS_GENERATE_MNEMONIC, cs, 0, new byte[0]);
|
||||||
return apduChannel.transmit(generateMnemonic);
|
return secureChannel.transmit(apduChannel, generateMnemonic);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -366,8 +370,8 @@ public class WalletAppletCommandSet {
|
||||||
*/
|
*/
|
||||||
public ResponseAPDU sign(byte[] data, byte dataType, boolean isFirst, boolean isLast) throws CardException {
|
public ResponseAPDU sign(byte[] data, byte dataType, boolean isFirst, boolean isLast) throws CardException {
|
||||||
byte p2 = (byte) ((isFirst ? 0x01 : 0x00) | (isLast ? 0x80 : 0x00));
|
byte p2 = (byte) ((isFirst ? 0x01 : 0x00) | (isLast ? 0x80 : 0x00));
|
||||||
CommandAPDU sign = new CommandAPDU(0x80, WalletApplet.INS_SIGN, dataType, p2, secureChannel.encryptAPDU(data));
|
CommandAPDU sign = secureChannel.protectedCommand(0x80, WalletApplet.INS_SIGN, dataType, p2, data);
|
||||||
return apduChannel.transmit(sign);
|
return secureChannel.transmit(apduChannel, sign);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -398,8 +402,8 @@ public class WalletAppletCommandSet {
|
||||||
p1 |= reset ? 0 : WalletApplet.DERIVE_P1_APPEND_MASK;
|
p1 |= reset ? 0 : WalletApplet.DERIVE_P1_APPEND_MASK;
|
||||||
byte p2 = isPublicKey ? WalletApplet.DERIVE_P2_PUBLIC_KEY : WalletApplet.DERIVE_P2_KEY_PATH;
|
byte p2 = isPublicKey ? WalletApplet.DERIVE_P2_PUBLIC_KEY : WalletApplet.DERIVE_P2_KEY_PATH;
|
||||||
|
|
||||||
CommandAPDU deriveKey = new CommandAPDU(0x80, WalletApplet.INS_DERIVE_KEY, p1, p2, secureChannel.encryptAPDU(data));
|
CommandAPDU deriveKey = secureChannel.protectedCommand(0x80, WalletApplet.INS_DERIVE_KEY, p1, p2, data);
|
||||||
return apduChannel.transmit(deriveKey);
|
return secureChannel.transmit(apduChannel, deriveKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -410,8 +414,8 @@ public class WalletAppletCommandSet {
|
||||||
* @throws CardException communication error
|
* @throws CardException communication error
|
||||||
*/
|
*/
|
||||||
public ResponseAPDU setPinlessPath(byte [] data) throws CardException {
|
public ResponseAPDU setPinlessPath(byte [] data) throws CardException {
|
||||||
CommandAPDU setPinlessPath = new CommandAPDU(0x80, WalletApplet.INS_SET_PINLESS_PATH, 0x00, 0x00, secureChannel.encryptAPDU(data));
|
CommandAPDU setPinlessPath = secureChannel.protectedCommand(0x80, WalletApplet.INS_SET_PINLESS_PATH, 0x00, 0x00, data);
|
||||||
return apduChannel.transmit(setPinlessPath);
|
return secureChannel.transmit(apduChannel, setPinlessPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -422,7 +426,7 @@ public class WalletAppletCommandSet {
|
||||||
* @throws CardException communication error
|
* @throws CardException communication error
|
||||||
*/
|
*/
|
||||||
public ResponseAPDU exportKey(byte keyPathIndex) throws CardException {
|
public ResponseAPDU exportKey(byte keyPathIndex) throws CardException {
|
||||||
CommandAPDU exportKey = new CommandAPDU(0x80, WalletApplet.INS_EXPORT_KEY, keyPathIndex, 0x00, 256);
|
CommandAPDU exportKey = secureChannel.protectedCommand(0x80, WalletApplet.INS_EXPORT_KEY, keyPathIndex, 0x00, new byte[0]);
|
||||||
return apduChannel.transmit(exportKey);
|
return secureChannel.transmit(apduChannel, exportKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,14 +128,16 @@ public class WalletAppletTest {
|
||||||
// Good case
|
// Good case
|
||||||
response = cmdSet.openSecureChannel(secureChannel.getPairingIndex(), secureChannel.getPublicKey());
|
response = cmdSet.openSecureChannel(secureChannel.getPairingIndex(), secureChannel.getPublicKey());
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
assertEquals(SecureChannel.SC_SECRET_LENGTH, response.getData().length);
|
assertEquals(SecureChannel.SC_SECRET_LENGTH + SecureChannel.SC_BLOCK_SIZE, response.getData().length);
|
||||||
secureChannel.processOpenSecureChannelResponse(response);
|
secureChannel.processOpenSecureChannelResponse(response);
|
||||||
|
|
||||||
// Send command before MUTUALLY AUTHENTICATE
|
// Send command before MUTUALLY AUTHENTICATE
|
||||||
|
secureChannel.reset();
|
||||||
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
|
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
|
||||||
assertEquals(0x6985, response.getSW());
|
assertEquals(0x6985, response.getSW());
|
||||||
|
|
||||||
// Perform mutual authentication
|
// Perform mutual authentication
|
||||||
|
secureChannel.setOpen();
|
||||||
response = cmdSet.mutuallyAuthenticate();
|
response = cmdSet.mutuallyAuthenticate();
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
assertTrue(secureChannel.verifyMutuallyAuthenticateResponse(response));
|
assertTrue(secureChannel.verifyMutuallyAuthenticateResponse(response));
|
||||||
|
@ -157,17 +159,23 @@ public class WalletAppletTest {
|
||||||
secureChannel.processOpenSecureChannelResponse(response);
|
secureChannel.processOpenSecureChannelResponse(response);
|
||||||
|
|
||||||
// Wrong data format
|
// Wrong data format
|
||||||
response = cmdSet.mutuallyAuthenticate(new byte[63]);
|
response = cmdSet.mutuallyAuthenticate(new byte[31]);
|
||||||
assertEquals(0x6A80, response.getSW());
|
|
||||||
|
|
||||||
// Wrong authentication data
|
|
||||||
response = cmdSet.mutuallyAuthenticate(new byte[64]);
|
|
||||||
assertEquals(0x6982, response.getSW());
|
assertEquals(0x6982, response.getSW());
|
||||||
|
|
||||||
// Verify that after wrong authentication, the command does not work
|
// Verify that after wrong authentication, the command does not work
|
||||||
response = cmdSet.mutuallyAuthenticate();
|
response = cmdSet.mutuallyAuthenticate();
|
||||||
assertEquals(0x6985, response.getSW());
|
assertEquals(0x6985, response.getSW());
|
||||||
|
|
||||||
|
// Wrong authentication data
|
||||||
|
response = cmdSet.openSecureChannel(secureChannel.getPairingIndex(), secureChannel.getPublicKey());
|
||||||
|
assertEquals(0x9000, response.getSW());
|
||||||
|
secureChannel.processOpenSecureChannelResponse(response);
|
||||||
|
response = apduChannel.transmit(new CommandAPDU(0x80, SecureChannel.INS_MUTUALLY_AUTHENTICATE, 0, 0, new byte[48]));
|
||||||
|
assertEquals(0x6982, response.getSW());
|
||||||
|
secureChannel.reset();
|
||||||
|
response = cmdSet.mutuallyAuthenticate();
|
||||||
|
assertEquals(0x6985, response.getSW());
|
||||||
|
|
||||||
// Good case
|
// Good case
|
||||||
cmdSet.autoOpenSecureChannel();
|
cmdSet.autoOpenSecureChannel();
|
||||||
|
|
||||||
|
@ -235,7 +243,7 @@ public class WalletAppletTest {
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
|
|
||||||
for (byte i = 0; i < 4; i++) {
|
for (byte i = 0; i < 4; i++) {
|
||||||
response = cmdSet.unpair(i, new byte[] { i });
|
response = cmdSet.unpair(i);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -252,28 +260,22 @@ public class WalletAppletTest {
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
|
|
||||||
// Security condition violation: SecureChannel not open
|
// Security condition violation: SecureChannel not open
|
||||||
response = cmdSet.unpair(sparePairingIndex, new byte[] { sparePairingIndex });
|
response = cmdSet.unpair(sparePairingIndex);
|
||||||
assertEquals(0x6985, response.getSW());
|
assertEquals(0x6985, response.getSW());
|
||||||
|
|
||||||
// Not authenticated
|
// Not authenticated
|
||||||
cmdSet.autoOpenSecureChannel();
|
cmdSet.autoOpenSecureChannel();
|
||||||
response = cmdSet.unpair(sparePairingIndex, new byte[] { sparePairingIndex });
|
response = cmdSet.unpair(sparePairingIndex);
|
||||||
assertEquals(0x6985, response.getSW());
|
assertEquals(0x6985, response.getSW());
|
||||||
|
|
||||||
// Wrong P1
|
// Wrong P1
|
||||||
response = cmdSet.verifyPIN("000000");
|
response = cmdSet.verifyPIN("000000");
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.unpair((byte) 5, new byte[] { 5 });
|
response = cmdSet.unpair((byte) 5);
|
||||||
assertEquals(0x6A86, response.getSW());
|
assertEquals(0x6A86, response.getSW());
|
||||||
|
|
||||||
// Unmatching P1 and data
|
|
||||||
response = cmdSet.unpair(sparePairingIndex, new byte[] { (byte) (sparePairingIndex + 1) });
|
|
||||||
assertEquals(0x6985, response.getSW());
|
|
||||||
response = cmdSet.unpair((byte) (sparePairingIndex + 1), new byte[] { sparePairingIndex });
|
|
||||||
assertEquals(0x6985, response.getSW());
|
|
||||||
|
|
||||||
// Unpair spare keyset
|
// Unpair spare keyset
|
||||||
response = cmdSet.unpair(sparePairingIndex, new byte[] { sparePairingIndex });
|
response = cmdSet.unpair(sparePairingIndex);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
|
|
||||||
// Proof that unpaired is not usable
|
// Proof that unpaired is not usable
|
||||||
|
@ -293,27 +295,27 @@ public class WalletAppletTest {
|
||||||
// Additionally, support for public key derivation is hw dependent.
|
// Additionally, support for public key derivation is hw dependent.
|
||||||
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
|
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
byte[] data = secureChannel.decryptAPDU(response.getData());
|
byte[] data = response.getData();
|
||||||
assertTrue(Hex.toHexString(data).matches("a309c00103c10105c2010[0-1]c3010[0-1]"));
|
assertTrue(Hex.toHexString(data).matches("a309c00103c10105c2010[0-1]c3010[0-1]"));
|
||||||
|
|
||||||
response = cmdSet.verifyPIN("123456");
|
response = cmdSet.verifyPIN("123456");
|
||||||
assertEquals(0x63C2, response.getSW());
|
assertEquals(0x63C2, response.getSW());
|
||||||
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
|
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
data = secureChannel.decryptAPDU(response.getData());
|
data = response.getData();
|
||||||
assertTrue(Hex.toHexString(data).matches("a309c00102c10105c2010[0-1]c3010[0-1]"));
|
assertTrue(Hex.toHexString(data).matches("a309c00102c10105c2010[0-1]c3010[0-1]"));
|
||||||
|
|
||||||
response = cmdSet.verifyPIN("000000");
|
response = cmdSet.verifyPIN("000000");
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
|
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
data = secureChannel.decryptAPDU(response.getData());
|
data = response.getData();
|
||||||
assertTrue(Hex.toHexString(data).matches("a309c00103c10105c2010[0-1]c3010[0-1]"));
|
assertTrue(Hex.toHexString(data).matches("a309c00103c10105c2010[0-1]c3010[0-1]"));
|
||||||
|
|
||||||
// Check that key path is empty
|
// Check that key path is empty
|
||||||
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_KEY_PATH);
|
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_KEY_PATH);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
data = secureChannel.decryptAPDU(response.getData());
|
data = response.getData();
|
||||||
assertEquals(0, data.length);
|
assertEquals(0, data.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -524,23 +526,23 @@ public class WalletAppletTest {
|
||||||
// Good cases
|
// Good cases
|
||||||
response = cmdSet.generateMnemonic(4);
|
response = cmdSet.generateMnemonic(4);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
assertMnemonic(12, secureChannel.decryptAPDU(response.getData()));
|
assertMnemonic(12, response.getData());
|
||||||
|
|
||||||
response = cmdSet.generateMnemonic(5);
|
response = cmdSet.generateMnemonic(5);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
assertMnemonic(15, secureChannel.decryptAPDU(response.getData()));
|
assertMnemonic(15, response.getData());
|
||||||
|
|
||||||
response = cmdSet.generateMnemonic(6);
|
response = cmdSet.generateMnemonic(6);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
assertMnemonic(18, secureChannel.decryptAPDU(response.getData()));
|
assertMnemonic(18, response.getData());
|
||||||
|
|
||||||
response = cmdSet.generateMnemonic(7);
|
response = cmdSet.generateMnemonic(7);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
assertMnemonic(21, secureChannel.decryptAPDU(response.getData()));
|
assertMnemonic(21, response.getData());
|
||||||
|
|
||||||
response = cmdSet.generateMnemonic(8);
|
response = cmdSet.generateMnemonic(8);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
assertMnemonic(24, secureChannel.decryptAPDU(response.getData()));
|
assertMnemonic(24, response.getData());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -621,13 +623,13 @@ public class WalletAppletTest {
|
||||||
// Assisted derivation
|
// Assisted derivation
|
||||||
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, true, true, false);
|
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, true, true, false);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true);
|
response = cmdSet.deriveKey(derivePublicKey(response.getData()), false, true, true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
verifyKeyDerivation(keyPair, chainCode, new int[] { 1 });
|
verifyKeyDerivation(keyPair, chainCode, new int[] { 1 });
|
||||||
|
|
||||||
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, false, true, false);
|
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, false, true, false);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true);
|
response = cmdSet.deriveKey(derivePublicKey(response.getData()), false, true, true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
verifyKeyDerivation(keyPair, chainCode, new int[] { 1, 2 });
|
verifyKeyDerivation(keyPair, chainCode, new int[] { 1, 2 });
|
||||||
|
|
||||||
|
@ -645,7 +647,7 @@ public class WalletAppletTest {
|
||||||
// Try to sign and get key path before load public key, then resume loading public key
|
// Try to sign and get key path before load public key, then resume loading public key
|
||||||
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, false, true, false);
|
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, false, true, false);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
byte[] key = derivePublicKey(secureChannel.decryptAPDU(response.getData()));
|
byte[] key = derivePublicKey(response.getData());
|
||||||
response = cmdSet.sign(sha256("test".getBytes()), WalletApplet.SIGN_P1_PRECOMPUTED_HASH, true, true);
|
response = cmdSet.sign(sha256("test".getBytes()), WalletApplet.SIGN_P1_PRECOMPUTED_HASH, true, true);
|
||||||
assertEquals(0x6985, response.getSW());
|
assertEquals(0x6985, response.getSW());
|
||||||
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_KEY_PATH);
|
response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_KEY_PATH);
|
||||||
|
@ -696,7 +698,7 @@ public class WalletAppletTest {
|
||||||
// Correctly sign a precomputed hash
|
// Correctly sign a precomputed hash
|
||||||
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
|
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
byte[] sig = secureChannel.decryptAPDU(response.getData());
|
byte[] sig = response.getData();
|
||||||
byte[] keyData = extractPublicKeyFromSignature(sig);
|
byte[] keyData = extractPublicKeyFromSignature(sig);
|
||||||
sig = extractSignature(sig);
|
sig = extractSignature(sig);
|
||||||
assertEquals((SecureChannel.SC_KEY_LENGTH * 2 / 8) + 1, keyData.length);
|
assertEquals((SecureChannel.SC_KEY_LENGTH * 2 / 8) + 1, keyData.length);
|
||||||
|
@ -746,17 +748,17 @@ public class WalletAppletTest {
|
||||||
assertEquals(0x6985, response.getSW());
|
assertEquals(0x6985, response.getSW());
|
||||||
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, true, true, false);
|
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, true, true, false);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true);
|
response = cmdSet.deriveKey(derivePublicKey(response.getData()), false, true, true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, false, true, false);
|
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, false, true, false);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true);
|
response = cmdSet.deriveKey(derivePublicKey(response.getData()), false, true, true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
|
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
|
||||||
assertEquals(0x6985, response.getSW());
|
assertEquals(0x6985, response.getSW());
|
||||||
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, false, true, false);
|
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, false, true, false);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true);
|
response = cmdSet.deriveKey(derivePublicKey(response.getData()), false, true, true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
|
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
|
@ -772,11 +774,11 @@ public class WalletAppletTest {
|
||||||
assertEquals(0x6985, response.getSW());
|
assertEquals(0x6985, response.getSW());
|
||||||
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, true, true, false);
|
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, true, true, false);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true);
|
response = cmdSet.deriveKey(derivePublicKey(response.getData()), false, true, true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, false, true, false);
|
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, false, true, false);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true);
|
response = cmdSet.deriveKey(derivePublicKey(response.getData()), false, true, true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
|
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
|
@ -822,13 +824,13 @@ public class WalletAppletTest {
|
||||||
|
|
||||||
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, true, true, false);
|
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, true, true, false);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true);
|
response = cmdSet.deriveKey(derivePublicKey(response.getData()), false, true, true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER);
|
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER);
|
||||||
assertEquals(0x6985, response.getSW());
|
assertEquals(0x6985, response.getSW());
|
||||||
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, false, true, false);
|
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, false, true, false);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true);
|
response = cmdSet.deriveKey(derivePublicKey(response.getData()), false, true, true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
|
|
||||||
// Wrong P1
|
// Wrong P1
|
||||||
|
@ -840,7 +842,7 @@ public class WalletAppletTest {
|
||||||
// Correct
|
// Correct
|
||||||
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER);
|
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
byte[] keyTemplate = secureChannel.decryptAPDU(response.getData());
|
byte[] keyTemplate = response.getData();
|
||||||
verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 1, 1 });
|
verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 1, 1 });
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
|
@ -881,7 +883,7 @@ public class WalletAppletTest {
|
||||||
// Correctly sign 1 block (P2: 0x81)
|
// Correctly sign 1 block (P2: 0x81)
|
||||||
response = cmdSet.sign(smallData, WalletApplet.SIGN_P1_DATA,true, true);
|
response = cmdSet.sign(smallData, WalletApplet.SIGN_P1_DATA,true, true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
byte[] sig = extractSignature(secureChannel.decryptAPDU(response.getData()));
|
byte[] sig = extractSignature(response.getData());
|
||||||
signature.update(smallData);
|
signature.update(smallData);
|
||||||
assertTrue(signature.verify(sig));
|
assertTrue(signature.verify(sig));
|
||||||
|
|
||||||
|
@ -890,7 +892,7 @@ public class WalletAppletTest {
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.sign(smallData, WalletApplet.SIGN_P1_DATA,false, true);
|
response = cmdSet.sign(smallData, WalletApplet.SIGN_P1_DATA,false, true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
sig = extractSignature(secureChannel.decryptAPDU(response.getData()));
|
sig = extractSignature(response.getData());
|
||||||
signature.update(data);
|
signature.update(data);
|
||||||
signature.update(smallData);
|
signature.update(smallData);
|
||||||
assertTrue(signature.verify(sig));
|
assertTrue(signature.verify(sig));
|
||||||
|
@ -902,7 +904,7 @@ public class WalletAppletTest {
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.sign(smallData, WalletApplet.SIGN_P1_DATA,false, true);
|
response = cmdSet.sign(smallData, WalletApplet.SIGN_P1_DATA,false, true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
sig = extractSignature(secureChannel.decryptAPDU(response.getData()));
|
sig = extractSignature(response.getData());
|
||||||
signature.update(data);
|
signature.update(data);
|
||||||
signature.update(data);
|
signature.update(data);
|
||||||
signature.update(smallData);
|
signature.update(smallData);
|
||||||
|
@ -913,7 +915,7 @@ public class WalletAppletTest {
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.sign(smallData, WalletApplet.SIGN_P1_DATA,true, true);
|
response = cmdSet.sign(smallData, WalletApplet.SIGN_P1_DATA,true, true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
sig = extractSignature(secureChannel.decryptAPDU(response.getData()));
|
sig = extractSignature(response.getData());
|
||||||
signature.update(smallData);
|
signature.update(smallData);
|
||||||
assertTrue(signature.verify(sig));
|
assertTrue(signature.verify(sig));
|
||||||
|
|
||||||
|
@ -943,7 +945,7 @@ public class WalletAppletTest {
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
response = cmdSet.sign(smallData, WalletApplet.SIGN_P1_DATA,false, true);
|
response = cmdSet.sign(smallData, WalletApplet.SIGN_P1_DATA,false, true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
sig = extractSignature(secureChannel.decryptAPDU(response.getData()));
|
sig = extractSignature(response.getData());
|
||||||
signature.update(data);
|
signature.update(data);
|
||||||
signature.update(smallData);
|
signature.update(smallData);
|
||||||
assertTrue(signature.verify(sig));
|
assertTrue(signature.verify(sig));
|
||||||
|
@ -1074,7 +1076,7 @@ public class WalletAppletTest {
|
||||||
byte[] hash = Hash.sha3(new byte[8]);
|
byte[] hash = Hash.sha3(new byte[8]);
|
||||||
ResponseAPDU resp = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH, true, true);
|
ResponseAPDU resp = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH, true, true);
|
||||||
assertEquals(0x9000, resp.getSW());
|
assertEquals(0x9000, resp.getSW());
|
||||||
byte[] sig = secureChannel.decryptAPDU(resp.getData());
|
byte[] sig = resp.getData();
|
||||||
byte[] publicKey = extractPublicKeyFromSignature(sig);
|
byte[] publicKey = extractPublicKeyFromSignature(sig);
|
||||||
sig = extractSignature(sig);
|
sig = extractSignature(sig);
|
||||||
|
|
||||||
|
@ -1083,7 +1085,7 @@ public class WalletAppletTest {
|
||||||
|
|
||||||
resp = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_KEY_PATH);
|
resp = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_KEY_PATH);
|
||||||
assertEquals(0x9000, resp.getSW());
|
assertEquals(0x9000, resp.getSW());
|
||||||
byte[] rawPath = secureChannel.decryptAPDU(resp.getData());
|
byte[] rawPath = resp.getData();
|
||||||
|
|
||||||
assertEquals(path.length * 4, rawPath.length);
|
assertEquals(path.length * 4, rawPath.length);
|
||||||
|
|
||||||
|
@ -1172,7 +1174,7 @@ public class WalletAppletTest {
|
||||||
|
|
||||||
ResponseAPDU response = cmdSet.sign(messageHash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
|
ResponseAPDU response = cmdSet.sign(messageHash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
|
||||||
assertEquals(0x9000, response.getSW());
|
assertEquals(0x9000, response.getSW());
|
||||||
byte[] respData = secureChannel.decryptAPDU(response.getData());
|
byte[] respData = response.getData();
|
||||||
byte[] rawSig = extractSignature(respData);
|
byte[] rawSig = extractSignature(respData);
|
||||||
|
|
||||||
int rLen = rawSig[3];
|
int rLen = rawSig[3];
|
||||||
|
|
Loading…
Reference in New Issue