Merge pull request #5 from status-im/no-inst-params
Move installation parameters to INIT command
This commit is contained in:
commit
643f81b60c
|
@ -11,18 +11,22 @@ authentication.
|
|||
Before any application command is processed, a Secure Channel session must be established as specified in the
|
||||
[SECURE_CHANNEL.MD](SECURE_CHANNEL.MD) document.
|
||||
|
||||
## INITIALIZATION
|
||||
|
||||
After installation, the applet is not ready to operate and is in a pre-initializaed state. In this state the applet can
|
||||
only process the SELECT and INIT command. The INIT command is used to personalize the PIN, PUK and pairing secret, which
|
||||
must be generated off-card.
|
||||
|
||||
## PIN
|
||||
|
||||
During installation the user's PIN is set to 000000 (six times zero). The PIN length is fixed at 6 digits. After 3
|
||||
failed authentication attempts the PIN is blocked and authentication is not possible anymore. A blocked PIN can be
|
||||
replaced and unblocked using a PUK. The PUK is a 12-digit number, unique for each installation and is generated off-card
|
||||
and passed as an installation parameter to the applet according to the JavaCard specifications. After 5 failed attempts
|
||||
to unblock the applet using the PUK, the PUK is blocked, meaning the wallet is lost.
|
||||
The PIN length is fixed at 6 digits. After 3 failed authentication attempts the PIN is blocked and authentication is not
|
||||
possible anymore. A blocked PIN can be replaced and unblocked using a PUK. The PUK is a 12-digit number. After 5 failed
|
||||
attempts to unblock the applet using the PUK, the PUK is blocked, meaning the wallet is lost.
|
||||
|
||||
After authentication, the user remains authenticated until the application is either deselected or the card is reset.
|
||||
Authentication with PIN is a requirement for most commands to succeed.
|
||||
|
||||
The PIN can be changed by the user after authentication.
|
||||
The PIN and PUK can be changed by the user after authentication.
|
||||
|
||||
## Keys & Signature
|
||||
|
||||
|
@ -48,7 +52,7 @@ SW 0x6985 is returned. All tagged data structures are encoded in the [BER-TLV fo
|
|||
* P1 = 0x04
|
||||
* P2 = 0x00
|
||||
* Data = 53746174757357616C6C6574417070 (hex)
|
||||
* Response = Application Info Template
|
||||
* Response = Application Info Template or ECC public key.
|
||||
|
||||
Response Data format:
|
||||
- Tag 0xA4 = Application Info Template
|
||||
|
@ -67,6 +71,33 @@ application, formatted on two bytes. The first byte is the major version and the
|
|||
|
||||
The Key UID can be either empty (when no key is loaded on card) or the SHA-256 hash of the master public key.
|
||||
|
||||
When the applet is in pre-initializated state, it only returns the ECC public key, BER-TLV encoded with tag 0x80.
|
||||
|
||||
### INIT
|
||||
* CLA = 0x80
|
||||
* INS = 0xFE
|
||||
* P1 = 0x00
|
||||
* P2 = 0x00
|
||||
* Data = EC public key (LV encoded) | IV | encrypted payload
|
||||
* Response SW = 0x9000 on success, 0x6D00 if the applet is already initialized
|
||||
|
||||
This command is only available when the applet is in pre-initialized state and successful execution brings the applet in
|
||||
the initialized state. This command is needed to allow securely storing secrets on the applet at a different moment and
|
||||
place than installation is taking place. Currently these are the PIN, PUK and pairing password.
|
||||
|
||||
The client must take the public key received after the SELECT command, generate a random keypair and perform EC-DH to
|
||||
generate an AES key. It must then generate a random IV and encrypt the payload using AES-CBC with ISO/IEC 9797-1 Method
|
||||
2 padding.
|
||||
|
||||
They payload is the concatenation of the PIN (6 digits/bytes), PUK (12 digits/bytes) and pairing secret (32 bytes).
|
||||
|
||||
This scheme guarantees protection against passive MITM attacks. Since the applet has no "owner" before the execution of
|
||||
this command, protection against active MITM cannot be provided at this stage. However since the communication happens
|
||||
locally (either through NFC or contacted interface) the realization of such an attack at this point is unrealistic.
|
||||
|
||||
After successful execution, this command cannot be executed anymore. The regular SecureChannel (with pairing) is active
|
||||
and PIN and PUK are initialized.
|
||||
|
||||
### OPEN SECURE CHANNEL
|
||||
|
||||
The OPEN SECURE CHANNEL command is as specified in the [SECURE_CHANNEL.MD](SECURE_CHANNEL.MD).
|
||||
|
@ -133,15 +164,20 @@ always returns 0x63C0, even if the PIN is inserted correctly.
|
|||
|
||||
* CLA = 0x80
|
||||
* INS = 0x21
|
||||
* P1 = 0x00
|
||||
* P1 = PIN identifier
|
||||
* P2 = 0x00
|
||||
* Data = the new PIN
|
||||
* Response SW = 0x9000 on success, 0x6A80 if the PIN format is invalid
|
||||
* Response SW = 0x9000 on success, 0x6A80 if the PIN format is invalid, 0x6A86 if P1 is invalid
|
||||
* Preconditions: Secure Channel must be opened, user PIN must be verified
|
||||
|
||||
Used to change the user PIN. The new PIN must be composed of exactly 6 numeric digits. Should this be not the case,
|
||||
the code 0x6A80 is returned. If the conditions match, the user PIN is updated and authenticated for the rest of
|
||||
the session. The no-error SW 0x9000 is returned.
|
||||
Used to change a PIN or secret. In case of invalid format, the code 0x6A80 is returned. If the conditions match, the PIN
|
||||
or secret is updated. The no-error SW 0x9000 is returned.
|
||||
|
||||
P1:
|
||||
* 0x00: User PIN. Must be 6-digits. The updated PIN is authenticated for the rest of the session.
|
||||
* 0x01: Applet PUK. Must be 12-digits.
|
||||
* 0x02: Pairing secret. Must be 32-bytes long. Existing pairings are not broken, but new pairings will need to use the
|
||||
new secret.
|
||||
|
||||
### UNBLOCK PIN
|
||||
|
||||
|
|
11
README.md
11
README.md
|
@ -55,17 +55,6 @@ im.status.gradle.gpshell.kvn=0
|
|||
im.status.wallet.test.simulated=false
|
||||
```
|
||||
|
||||
## Alternative installation method
|
||||
This method does not require the JavaCard SDK but requires an already compiled CAP file. The cards generated this way
|
||||
have a random PUK and pairing code so they have better security. However applet installation/removal is not disabled,
|
||||
because the script is still meant to be used during the development phase.
|
||||
|
||||
1. Install GPShell and Python 3
|
||||
2. Put the wallet.cap file in the same directory as status_hw_perso.py
|
||||
3. Disconnect all card reader terminals from the system, except the one with the card where you want to install the applet
|
||||
4. Run the status_hw_perso.py script with no arguments.
|
||||
5. Take note of the pairing code and PUK output by the script
|
||||
|
||||
## Implementation notes
|
||||
|
||||
* The applet requires JavaCard 3.0.4 or later.
|
||||
|
|
|
@ -9,11 +9,13 @@ authentication for each APDU.
|
|||
A short description of establishing a session is as follows
|
||||
|
||||
1. The client selects the application on card. The application responds with a public EC key.
|
||||
2. The client sends an OPEN SECURE CHANNEL command with its public key. The EC-DH algorithm is used by both parties to
|
||||
generate a shared 256-bit secret (more details below).
|
||||
3. The generated secret is used as an AES key to encrypt all further communication. CBC mode is used with a random IV
|
||||
generated for each APDU and prepended to the APDU payload. Both command and responses are encrypted.
|
||||
4. The client sends a MUTUALLY AUTHENTICATE command to verify that the keys are matching and thus the secure channel is
|
||||
2. The client sends an OPEN SECURE CHANNEL command with its public key and pairing index. The EC-DH algorithm is used by
|
||||
both parties to generate a shared 256-bit secret (more details below).
|
||||
3. The generated secret is concatenated with the pairing key and random data and hashed with SHA-512.
|
||||
4. The first half of the generated value is used as an AES key to encrypt all further communication. CBC mode is used
|
||||
with a random IV generated for each APDU and prepended to the APDU payload. The second half is used to MAC generation
|
||||
and verification. Both command and responses are encrypted.
|
||||
5. The client sends a MUTUALLY AUTHENTICATE command to verify that the keys are matching and thus the secure channel is
|
||||
successfully established.
|
||||
|
||||
The EC keyset used by the card for the EC-DH algorithm is generated on-card on applet installation and is not used
|
||||
|
|
|
@ -67,7 +67,7 @@ task install(type: Exec) {
|
|||
send_apdu_nostop -sc 1 -APDU 80E400800E4F0C53746174757357616C6C6574
|
||||
install_for_load -pkgAID 53746174757357616C6C6574
|
||||
load -file build/javacard/im/status/wallet/javacard/wallet.cap
|
||||
send_apdu -sc 1 -APDU 80E60C005F0C53746174757357616C6C65740F53746174757357616C6C65744170700F53746174757357616C6C657441707001002EC92C313233343536373839303132e929d425d7f73c2a0a24ffefad87b65e9b2ee96603eab34d64088b5aae2a026f00
|
||||
install_for_install -AID 53746174757357616C6C6574417070 -pkgAID 53746174757357616C6C6574 -instAID 53746174757357616C6C6574417070
|
||||
install_for_install -AID 53746174757357616C6C65744e4643 -pkgAID 53746174757357616C6C6574 -instAID D2760000850101
|
||||
card_disconnect
|
||||
release_context
|
||||
|
|
|
@ -48,15 +48,13 @@ public class SecureChannel {
|
|||
private boolean mutuallyAuthenticated = false;
|
||||
|
||||
private Crypto crypto;
|
||||
private SECP256k1 secp256k1;
|
||||
|
||||
/**
|
||||
* Instantiates a Secure Channel. All memory allocations needed for the secure channel are performed here. The keypair
|
||||
* used for the EC-DH algorithm is also generated here.
|
||||
* Instantiates a Secure Channel. All memory allocations (except pairing secret) needed for the secure channel are
|
||||
* performed here. The keypair used for the EC-DH algorithm is also generated here.
|
||||
*/
|
||||
public SecureChannel(byte pairingLimit, byte[] aPairingSecret, short off, Crypto crypto, SECP256k1 secp256k1) {
|
||||
public SecureChannel(byte pairingLimit, Crypto crypto, SECP256k1 secp256k1) {
|
||||
this.crypto = crypto;
|
||||
this.secp256k1 = secp256k1;
|
||||
|
||||
scCipher = Cipher.getInstance(Cipher.ALG_AES_CBC_ISO9797_M2,false);
|
||||
|
||||
|
@ -76,11 +74,48 @@ public class SecureChannel {
|
|||
scKeypair.genKeyPair();
|
||||
|
||||
secret = JCSystem.makeTransientByteArray((short)(SC_SECRET_LENGTH * 2), JCSystem.CLEAR_ON_DESELECT);
|
||||
pairingSecret = new byte[SC_SECRET_LENGTH];
|
||||
pairingKeys = new byte[(short)(PAIRING_KEY_LENGTH * pairingLimit)];
|
||||
remainingSlots = pairingLimit;
|
||||
|
||||
Util.arrayCopyNonAtomic(aPairingSecret, off, pairingSecret, (short) 0, SC_SECRET_LENGTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the SecureChannel instance with the pairing secret.
|
||||
*
|
||||
* @param aPairingSecret the pairing secret
|
||||
* @param off the offset in the buffer
|
||||
*/
|
||||
public void initSecureChannel(byte[] aPairingSecret, short off) {
|
||||
if (pairingSecret != null) return;
|
||||
|
||||
pairingSecret = new byte[SC_SECRET_LENGTH];
|
||||
Util.arrayCopy(aPairingSecret, off, pairingSecret, (short) 0, SC_SECRET_LENGTH);
|
||||
scKeypair.genKeyPair();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the content of the APDU by generating an AES key using EC-DH. Only usable in pre-initialization state.
|
||||
* @param apduBuffer the APDU buffer
|
||||
*/
|
||||
public void oneShotDecrypt(byte[] apduBuffer) {
|
||||
if (pairingSecret != null) return;
|
||||
|
||||
crypto.ecdh.init(scKeypair.getPrivate());
|
||||
|
||||
short off = (short)(ISO7816.OFFSET_CDATA + 1);
|
||||
try {
|
||||
crypto.ecdh.generateSecret(apduBuffer, off, apduBuffer[ISO7816.OFFSET_CDATA], secret, (short) 0);
|
||||
off = (short)(off + apduBuffer[ISO7816.OFFSET_CDATA]);
|
||||
} catch(Exception e) {
|
||||
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
||||
return;
|
||||
}
|
||||
|
||||
scEncKey.setKey(secret, (short) 0);
|
||||
scCipher.init(scEncKey, Cipher.MODE_DECRYPT, apduBuffer, off, SC_BLOCK_SIZE);
|
||||
off = (short)(off + SC_BLOCK_SIZE);
|
||||
|
||||
apduBuffer[ISO7816.OFFSET_LC] = (byte) scCipher.doFinal(apduBuffer, off, (short)((short)(apduBuffer[ISO7816.OFFSET_LC] & 0xff) - off + ISO7816.OFFSET_CDATA), apduBuffer, ISO7816.OFFSET_CDATA);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -404,6 +439,15 @@ public class SecureChannel {
|
|||
mutuallyAuthenticated = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the pairing secret. Does not affect existing pairings.
|
||||
* @param aPairingSecret the buffer
|
||||
* @param off the offset
|
||||
*/
|
||||
public void updatePairingSecret(byte[] aPairingSecret, byte off) {
|
||||
Util.arrayCopy(aPairingSecret, off, pairingSecret, (short) 0, SC_SECRET_LENGTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset in the pairingKey byte array of the pairing key with the given index. Throws 0x6A86 if the index
|
||||
* is invalid
|
||||
|
|
|
@ -9,6 +9,7 @@ import javacard.security.*;
|
|||
public class WalletApplet extends Applet {
|
||||
static final short APPLICATION_VERSION = (short) 0x0102;
|
||||
|
||||
static final byte INS_INIT = (byte) 0xFE;
|
||||
static final byte INS_GET_STATUS = (byte) 0xF2;
|
||||
static final byte INS_VERIFY_PIN = (byte) 0x20;
|
||||
static final byte INS_CHANGE_PIN = (byte) 0x21;
|
||||
|
@ -37,6 +38,10 @@ public class WalletApplet extends Applet {
|
|||
static final byte GET_STATUS_P1_APPLICATION = 0x00;
|
||||
static final byte GET_STATUS_P1_KEY_PATH = 0x01;
|
||||
|
||||
static final byte CHANGE_PIN_P1_USER_PIN = 0x00;
|
||||
static final byte CHANGE_PIN_P1_PUK = 0x01;
|
||||
static final byte CHANGE_PIN_P1_PAIRING_SECRET = 0x02;
|
||||
|
||||
static final byte LOAD_KEY_P1_EC = 0x01;
|
||||
static final byte LOAD_KEY_P1_EXT_EC = 0x02;
|
||||
static final byte LOAD_KEY_P1_SEED = 0x03;
|
||||
|
@ -134,10 +139,11 @@ public class WalletApplet extends Applet {
|
|||
}
|
||||
|
||||
/**
|
||||
* Application constructor. All memory allocation is done here. The reason for this is two-fold: first the card might
|
||||
* not have Garbage Collection so dynamic allocation will eventually eat all memory. The second reason is to be sure
|
||||
* that if the application installs successfully, there is no risk of running out of memory because of other applets
|
||||
* allocating memory. The constructor also registers the applet with the JCRE so that it becomes selectable.
|
||||
* Application constructor. All memory allocation is done here and in the init function. The reason for this is
|
||||
* two-fold: first the card might not have Garbage Collection so dynamic allocation will eventually eat all memory.
|
||||
* The second reason is to be sure that if the application installs successfully, there is no risk of running out
|
||||
* of memory because of other applets allocating memory. The constructor also registers the applet with the JCRE so
|
||||
* that it becomes selectable.
|
||||
*
|
||||
* @param bArray installation parameters buffer
|
||||
* @param bOffset offset where the installation parameters begin
|
||||
|
@ -170,18 +176,7 @@ public class WalletApplet extends Applet {
|
|||
resetCurveParameters();
|
||||
|
||||
signature = Signature.getInstance(Signature.ALG_ECDSA_SHA_256, false);
|
||||
|
||||
short c9Off = (short)(bOffset + bArray[bOffset] + 1); // Skip AID
|
||||
c9Off += (short)(bArray[c9Off] + 2); // Skip Privileges and parameter length
|
||||
|
||||
secureChannel = new SecureChannel(PAIRING_MAX_CLIENT_COUNT, bArray, (short) (c9Off + PUK_LENGTH), crypto, secp256k1);
|
||||
|
||||
puk = new OwnerPIN(PUK_MAX_RETRIES, PUK_LENGTH);
|
||||
puk.update(bArray, c9Off, PUK_LENGTH);
|
||||
|
||||
Util.arrayFillNonAtomic(bArray, c9Off, PIN_LENGTH, (byte) 0x30);
|
||||
pin = new OwnerPIN(PIN_MAX_RETRIES, PIN_LENGTH);
|
||||
pin.update(bArray, c9Off, PIN_LENGTH);
|
||||
secureChannel = new SecureChannel(PAIRING_MAX_CLIENT_COUNT, crypto, secp256k1);
|
||||
|
||||
register(bArray, (short) (bOffset + 1), bArray[bOffset]);
|
||||
}
|
||||
|
@ -194,6 +189,12 @@ public class WalletApplet extends Applet {
|
|||
* @throws ISOException any processing error
|
||||
*/
|
||||
public void process(APDU apdu) throws ISOException {
|
||||
// If we have no PIN it means we still have to initialize the applet.
|
||||
if (pin == null) {
|
||||
processInit(apdu);
|
||||
return;
|
||||
}
|
||||
|
||||
// Since selection can happen not only by a SELECT command, we check for that separately.
|
||||
if (selectingApplet()) {
|
||||
selectApplet(apdu);
|
||||
|
@ -267,6 +268,45 @@ public class WalletApplet extends Applet {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the init command, this is invoked only if the applet has not yet been personalized with secrets.
|
||||
*
|
||||
* @param apdu the JCRE-owned APDU object.
|
||||
*/
|
||||
private void processInit(APDU apdu) {
|
||||
byte[] apduBuffer = apdu.getBuffer();
|
||||
apdu.setIncomingAndReceive();
|
||||
|
||||
if (selectingApplet()) {
|
||||
apduBuffer[0] = TLV_PUB_KEY;
|
||||
apduBuffer[1] = (byte) secureChannel.copyPublicKey(apduBuffer, (short) 2);
|
||||
apdu.setOutgoingAndSend((short) 0, (short)(apduBuffer[1] + 2));
|
||||
} else if (apduBuffer[ISO7816.OFFSET_INS] == INS_INIT) {
|
||||
secureChannel.oneShotDecrypt(apduBuffer);
|
||||
|
||||
if (apduBuffer[ISO7816.OFFSET_LC] != (byte)(PIN_LENGTH + PUK_LENGTH + SecureChannel.SC_SECRET_LENGTH)) {
|
||||
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
|
||||
}
|
||||
|
||||
if (!allDigits(apduBuffer, ISO7816.OFFSET_CDATA, (short)(PIN_LENGTH + PUK_LENGTH))) {
|
||||
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
||||
}
|
||||
|
||||
JCSystem.beginTransaction();
|
||||
secureChannel.initSecureChannel(apduBuffer, (short)(ISO7816.OFFSET_CDATA + PIN_LENGTH + PUK_LENGTH));
|
||||
|
||||
pin = new OwnerPIN(PIN_MAX_RETRIES, PIN_LENGTH);
|
||||
pin.update(apduBuffer, ISO7816.OFFSET_CDATA, PIN_LENGTH);
|
||||
|
||||
puk = new OwnerPIN(PUK_MAX_RETRIES, PUK_LENGTH);
|
||||
puk.update(apduBuffer, (short)(ISO7816.OFFSET_CDATA + PIN_LENGTH), PUK_LENGTH);
|
||||
|
||||
JCSystem.commitTransaction();
|
||||
} else {
|
||||
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldRespond(APDU apdu) {
|
||||
return secureChannel.isOpen() && (apdu.getCurrentState() != APDU.STATE_FULL_OUTGOING);
|
||||
}
|
||||
|
@ -418,9 +458,8 @@ public class WalletApplet extends Applet {
|
|||
}
|
||||
|
||||
/**
|
||||
* Processes the CHANGE PIN command. Requires a secure channel to be already open and the PIN to be verified. Since
|
||||
* the PIN is fixed to a 6-digits format, longer or shorter PINs or PINs containing non-numeric characters will be
|
||||
* refused.
|
||||
* Processes the CHANGE PIN command. Requires a secure channel to be already open and the user PIN to be verified. All
|
||||
* PINs have a fixed format which is verified by this method.
|
||||
*
|
||||
* @param apdu the JCRE-owned APDU object.
|
||||
*/
|
||||
|
@ -432,6 +471,28 @@ public class WalletApplet extends Applet {
|
|||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||
}
|
||||
|
||||
switch(apduBuffer[ISO7816.OFFSET_P1]) {
|
||||
case CHANGE_PIN_P1_USER_PIN:
|
||||
changeUserPIN(apduBuffer, len);
|
||||
break;
|
||||
case CHANGE_PIN_P1_PUK:
|
||||
changePUK(apduBuffer, len);
|
||||
break;
|
||||
case CHANGE_PIN_P1_PAIRING_SECRET:
|
||||
changePairingSecret(apduBuffer, len);
|
||||
break;
|
||||
default:
|
||||
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the user PIN. Called internally by CHANGE PIN
|
||||
* @param apduBuffer the APDU buffer
|
||||
* @param len the data length
|
||||
*/
|
||||
private void changeUserPIN(byte[] apduBuffer, byte len) {
|
||||
if (!(len == PIN_LENGTH && allDigits(apduBuffer, ISO7816.OFFSET_CDATA, len))) {
|
||||
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
||||
}
|
||||
|
@ -440,6 +501,32 @@ public class WalletApplet extends Applet {
|
|||
pin.check(apduBuffer, ISO7816.OFFSET_CDATA, len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the PUK. Called internally by CHANGE PIN
|
||||
* @param apduBuffer the APDU buffer
|
||||
* @param len the data length
|
||||
*/
|
||||
private void changePUK(byte[] apduBuffer, byte len) {
|
||||
if (!(len == PUK_LENGTH && allDigits(apduBuffer, ISO7816.OFFSET_CDATA, len))) {
|
||||
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
||||
}
|
||||
|
||||
puk.update(apduBuffer, ISO7816.OFFSET_CDATA, len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the pairing secret. Called internally by CHANGE PIN
|
||||
* @param apduBuffer the APDU buffer
|
||||
* @param len the data length
|
||||
*/
|
||||
private void changePairingSecret(byte[] apduBuffer, byte len) {
|
||||
if (len != SecureChannel.SC_SECRET_LENGTH) {
|
||||
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
||||
}
|
||||
|
||||
secureChannel.updatePairingSecret(apduBuffer, ISO7816.OFFSET_CDATA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the UNBLOCK PIN command. Requires a secure channel to be already open and the PIN to be blocked. The PUK
|
||||
* and the new PIN are sent in the same APDU with no separator. This is possible because the PUK is exactly 12 digits
|
||||
|
|
|
@ -7,6 +7,7 @@ import org.bouncycastle.jce.ECNamedCurveTable;
|
|||
import org.bouncycastle.jce.interfaces.ECPublicKey;
|
||||
import org.bouncycastle.jce.spec.ECParameterSpec;
|
||||
import org.bouncycastle.jce.spec.ECPublicKeySpec;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyAgreement;
|
||||
|
@ -392,6 +393,32 @@ public class SecureChannelSession {
|
|||
open = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the payload for the INIT command
|
||||
* @param initData the payload for the INIT command
|
||||
*
|
||||
* @return the encrypted buffer
|
||||
*/
|
||||
public byte[] oneShotEncrypt(byte[] initData) {
|
||||
try {
|
||||
iv = new byte[SecureChannel.SC_BLOCK_SIZE];
|
||||
random.nextBytes(iv);
|
||||
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
|
||||
sessionEncKey = new SecretKeySpec(secret, "AES");
|
||||
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding", "BC");
|
||||
sessionCipher.init(Cipher.ENCRYPT_MODE, sessionEncKey, ivParameterSpec);
|
||||
initData = sessionCipher.doFinal(initData);
|
||||
byte[] encrypted = new byte[1 + publicKey.length + iv.length + initData.length];
|
||||
encrypted[0] = (byte) publicKey.length;
|
||||
System.arraycopy(publicKey, 0, encrypted, 1, publicKey.length);
|
||||
System.arraycopy(iv, 0, encrypted, (1 + publicKey.length), iv.length);
|
||||
System.arraycopy(initData, 0, encrypted, (1 + publicKey.length + iv.length), initData.length);
|
||||
return encrypted;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the SecureChannel as open. Only to be used when writing tests for the SecureChannel, in normal operation this
|
||||
* would only make things wrong.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package im.status.wallet;
|
||||
|
||||
import com.licel.jcardsim.utils.ByteUtil;
|
||||
import javacard.framework.ISO7816;
|
||||
import org.bouncycastle.jce.interfaces.ECPrivateKey;
|
||||
import org.bouncycastle.jce.interfaces.ECPublicKey;
|
||||
|
@ -12,6 +13,7 @@ import javax.smartcardio.CommandAPDU;
|
|||
import javax.smartcardio.ResponseAPDU;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* This class is used to send APDU to the applet. Each method corresponds to an APDU as defined in the APPLICATION.md
|
||||
|
@ -168,12 +170,26 @@ public class WalletAppletCommandSet {
|
|||
* Sends a CHANGE PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
|
||||
* data.
|
||||
*
|
||||
* @param pinType the PIN type
|
||||
* @param pin the new PIN
|
||||
* @return the raw card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public ResponseAPDU changePIN(String pin) throws CardException {
|
||||
CommandAPDU changePIN = secureChannel.protectedCommand(0x80, WalletApplet.INS_CHANGE_PIN, 0, 0, pin.getBytes());
|
||||
public ResponseAPDU changePIN(int pinType, String pin) throws CardException {
|
||||
return changePIN(pinType, pin.getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a CHANGE PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
|
||||
* data.
|
||||
*
|
||||
* @param pinType the PIN type
|
||||
* @param pin the new PIN
|
||||
* @return the raw card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public ResponseAPDU changePIN(int pinType, byte[] pin) throws CardException {
|
||||
CommandAPDU changePIN = secureChannel.protectedCommand(0x80, WalletApplet.INS_CHANGE_PIN, pinType, 0, pin);
|
||||
return secureChannel.transmit(apduChannel, changePIN);
|
||||
}
|
||||
|
||||
|
@ -455,4 +471,21 @@ public class WalletAppletCommandSet {
|
|||
CommandAPDU exportKey = secureChannel.protectedCommand(0x80, WalletApplet.INS_EXPORT_KEY, keyPathIndex, p2, new byte[0]);
|
||||
return secureChannel.transmit(apduChannel, exportKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the INIT command to the card.
|
||||
*
|
||||
* @param pin the PIN
|
||||
* @param puk the PUK
|
||||
* @param sharedSecret the shared secret for pairing
|
||||
* @return the raw card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public ResponseAPDU init(String pin, String puk, byte[] sharedSecret) throws CardException {
|
||||
byte[] initData = Arrays.copyOf(pin.getBytes(), pin.length() + puk.length() + sharedSecret.length);
|
||||
System.arraycopy(puk.getBytes(), 0, initData, pin.length(), puk.length());
|
||||
System.arraycopy(sharedSecret, 0, initData, pin.length() + puk.length(), sharedSecret.length);
|
||||
CommandAPDU init = new CommandAPDU(0x80, WalletApplet.INS_INIT, 0, 0, secureChannel.oneShotEncrypt(initData));
|
||||
return apduChannel.transmit(init);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,8 +65,7 @@ public class WalletAppletTest {
|
|||
if (USE_SIMULATOR) {
|
||||
simulator = new CardSimulator();
|
||||
AID appletAID = AIDUtil.create(WalletAppletCommandSet.APPLET_AID);
|
||||
byte[] instParams = Hex.decode("0F53746174757357616C6C657441707001000C313233343536373839303132");
|
||||
simulator.installApplet(appletAID, WalletApplet.class, instParams, (short) 0, (byte) instParams.length);
|
||||
simulator.installApplet(appletAID, WalletApplet.class);
|
||||
cardTerminal = CardTerminalSimulator.terminal(simulator);
|
||||
} else {
|
||||
TerminalFactory tf = TerminalFactory.getDefault();
|
||||
|
@ -81,6 +80,18 @@ public class WalletAppletTest {
|
|||
|
||||
Card apduCard = cardTerminal.connect("*");
|
||||
apduChannel = apduCard.getBasicChannel();
|
||||
|
||||
initIfNeeded();
|
||||
}
|
||||
|
||||
private static void initIfNeeded() throws CardException {
|
||||
WalletAppletCommandSet cmdSet = new WalletAppletCommandSet(apduChannel);
|
||||
byte[] data = cmdSet.select().getData();
|
||||
if (data[0] == WalletApplet.TLV_APPLICATION_INFO_TEMPLATE) return;
|
||||
|
||||
SecureChannelSession secureChannel = new SecureChannelSession(Arrays.copyOfRange(data, 2, data.length));
|
||||
cmdSet.setSecureChannel(secureChannel);
|
||||
assertEquals(0x9000, cmdSet.init("000000", "123456789012", SHARED_SECRET).getSW());
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
|
@ -379,23 +390,54 @@ public class WalletAppletTest {
|
|||
@DisplayName("CHANGE PIN command")
|
||||
void changePinTest() throws CardException {
|
||||
// Security condition violation: SecureChannel not open
|
||||
ResponseAPDU response = cmdSet.changePIN("123456");
|
||||
ResponseAPDU response = cmdSet.changePIN(WalletApplet.CHANGE_PIN_P1_USER_PIN, "123456");
|
||||
assertEquals(0x6985, response.getSW());
|
||||
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
|
||||
// Security condition violation: PIN n ot verified
|
||||
response = cmdSet.changePIN("123456");
|
||||
response = cmdSet.changePIN(WalletApplet.CHANGE_PIN_P1_USER_PIN, "123456");
|
||||
assertEquals(0x6985, response.getSW());
|
||||
|
||||
// Change PIN correctly, check that after PIN change the PIN remains validated
|
||||
response = cmdSet.verifyPIN("000000");
|
||||
assertEquals(0x9000, response.getSW());
|
||||
|
||||
response = cmdSet.changePIN("123456");
|
||||
// Wrong P1
|
||||
response = cmdSet.changePIN(0x03, "123456");
|
||||
assertEquals(0x6a86, response.getSW());
|
||||
|
||||
// Test wrong PIN formats (non-digits, too short, too long)
|
||||
response = cmdSet.changePIN(WalletApplet.CHANGE_PIN_P1_USER_PIN, "654a21");
|
||||
assertEquals(0x6A80, response.getSW());
|
||||
|
||||
response = cmdSet.changePIN(WalletApplet.CHANGE_PIN_P1_USER_PIN, "54321");
|
||||
assertEquals(0x6A80, response.getSW());
|
||||
|
||||
response = cmdSet.changePIN(WalletApplet.CHANGE_PIN_P1_USER_PIN, "7654321");
|
||||
assertEquals(0x6A80, response.getSW());
|
||||
|
||||
// Test wrong PUK formats
|
||||
response = cmdSet.changePIN(WalletApplet.CHANGE_PIN_P1_PUK, "210987654a21");
|
||||
assertEquals(0x6A80, response.getSW());
|
||||
|
||||
response = cmdSet.changePIN(WalletApplet.CHANGE_PIN_P1_PUK, "10987654321");
|
||||
assertEquals(0x6A80, response.getSW());
|
||||
|
||||
response = cmdSet.changePIN(WalletApplet.CHANGE_PIN_P1_PUK, "3210987654321");
|
||||
assertEquals(0x6A80, response.getSW());
|
||||
|
||||
// Test wrong pairing secret format (too long, too short)
|
||||
response = cmdSet.changePIN(WalletApplet.CHANGE_PIN_P1_PAIRING_SECRET, "abcdefghilmnopqrstuvz123456789012");
|
||||
assertEquals(0x6A80, response.getSW());
|
||||
|
||||
response = cmdSet.changePIN(WalletApplet.CHANGE_PIN_P1_PAIRING_SECRET, "abcdefghilmnopqrstuvz1234567890");
|
||||
assertEquals(0x6A80, response.getSW());
|
||||
|
||||
// Change PIN correctly, check that after PIN change the PIN remains validated
|
||||
response = cmdSet.changePIN(WalletApplet.CHANGE_PIN_P1_USER_PIN, "123456");
|
||||
assertEquals(0x9000, response.getSW());
|
||||
|
||||
response = cmdSet.changePIN("654321");
|
||||
response = cmdSet.changePIN(WalletApplet.CHANGE_PIN_P1_USER_PIN, "654321");
|
||||
assertEquals(0x9000, response.getSW());
|
||||
|
||||
// Reset card and verify that the new PIN has really been set
|
||||
|
@ -404,18 +446,46 @@ public class WalletAppletTest {
|
|||
response = cmdSet.verifyPIN("654321");
|
||||
assertEquals(0x9000, response.getSW());
|
||||
|
||||
// Test wrong PIN formats (non-digits, too short, too long)
|
||||
response = cmdSet.changePIN("654a21");
|
||||
assertEquals(0x6A80, response.getSW());
|
||||
// Change PUK
|
||||
response = cmdSet.changePIN(WalletApplet.CHANGE_PIN_P1_PUK, "210987654321");
|
||||
assertEquals(0x9000, response.getSW());
|
||||
|
||||
response = cmdSet.changePIN("54321");
|
||||
assertEquals(0x6A80, response.getSW());
|
||||
resetAndSelectAndOpenSC();
|
||||
|
||||
response = cmdSet.changePIN("7654321");
|
||||
assertEquals(0x6A80, response.getSW());
|
||||
response = cmdSet.verifyPIN("000000");
|
||||
assertEquals(0x63C2, response.getSW());
|
||||
response = cmdSet.verifyPIN("000000");
|
||||
assertEquals(0x63C1, response.getSW());
|
||||
response = cmdSet.verifyPIN("000000");
|
||||
assertEquals(0x63C0, response.getSW());
|
||||
|
||||
// Reset the PIN to make further tests possible
|
||||
response = cmdSet.changePIN("000000");
|
||||
// Reset the PIN with the new PUK
|
||||
response = cmdSet.unblockPIN("210987654321", "000000");
|
||||
assertEquals(0x9000, response.getSW());
|
||||
|
||||
response = cmdSet.verifyPIN("000000");
|
||||
assertEquals(0x9000, response.getSW());
|
||||
|
||||
// Reset PUK
|
||||
response = cmdSet.changePIN(WalletApplet.CHANGE_PIN_P1_PUK, "123456789012");
|
||||
assertEquals(0x9000, response.getSW());
|
||||
|
||||
// Change the pairing secret
|
||||
response = cmdSet.changePIN(WalletApplet.CHANGE_PIN_P1_PAIRING_SECRET, "abcdefghilmnopqrstuvz12345678901");
|
||||
assertEquals(0x9000, response.getSW());
|
||||
cmdSet.autoUnpair();
|
||||
reset();
|
||||
response = cmdSet.select();
|
||||
assertEquals(0x9000, response.getSW());
|
||||
cmdSet.autoPair("abcdefghilmnopqrstuvz12345678901".getBytes());
|
||||
|
||||
// Reset pairing secret
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
|
||||
response = cmdSet.verifyPIN("000000");
|
||||
assertEquals(0x9000, response.getSW());
|
||||
|
||||
response = cmdSet.changePIN(WalletApplet.CHANGE_PIN_P1_PAIRING_SECRET, SHARED_SECRET);
|
||||
assertEquals(0x9000, response.getSW());
|
||||
}
|
||||
|
||||
|
@ -464,7 +534,7 @@ public class WalletAppletTest {
|
|||
assertEquals(0x9000, response.getSW());
|
||||
|
||||
// Reset the PIN to make further tests possible
|
||||
response = cmdSet.changePIN("000000");
|
||||
response = cmdSet.changePIN(WalletApplet.CHANGE_PIN_P1_USER_PIN, "000000");
|
||||
assertEquals(0x9000, response.getSW());
|
||||
}
|
||||
|
||||
|
@ -1061,7 +1131,7 @@ public class WalletAppletTest {
|
|||
// Signing session can be resumed if other commands are sent
|
||||
response = cmdSet.sign(data, WalletApplet.SIGN_P1_DATA,true, false);
|
||||
assertEquals(0x9000, response.getSW());
|
||||
response = cmdSet.changePIN("000000");
|
||||
response = cmdSet.changePIN(WalletApplet.CHANGE_PIN_P1_USER_PIN, "000000");
|
||||
assertEquals(0x9000, response.getSW());
|
||||
response = cmdSet.sign(smallData, WalletApplet.SIGN_P1_DATA,false, true);
|
||||
assertEquals(0x9000, response.getSW());
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
import secrets
|
||||
import hmac
|
||||
import hashlib
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
|
||||
gpshell_template = """
|
||||
mode_211
|
||||
enable_trace
|
||||
establish_context
|
||||
card_connect
|
||||
select -AID A000000151000000
|
||||
open_sc -security 1 -keyind 0 -keyver 0 -mac_key 404142434445464748494a4b4c4d4e4f -enc_key 404142434445464748494a4b4c4d4e4f -kek_key 404142434445464748494a4b4c4d4e4f
|
||||
send_apdu_nostop -sc 1 -APDU 80E400800E4F0C53746174757357616C6C6574
|
||||
install_for_load -pkgAID 53746174757357616C6C6574
|
||||
load -file wallet.cap
|
||||
send_apdu -sc 1 -APDU 80E60C005F0C53746174757357616C6C65740F53746174757357616C6C65744170700F53746174757357616C6C657441707001002EC92C{:s}{:s}00
|
||||
card_disconnect
|
||||
release_context
|
||||
"""
|
||||
|
||||
def pbkdf2(digestmod, password: 'bytes', salt, count, dk_length) -> 'bytes':
|
||||
def pbkdf2_function(pw, salt, count, i):
|
||||
# in the first iteration, the hmac message is the salt
|
||||
# concatinated with the block number in the form of \x00\x00\x00\x01
|
||||
r = u = hmac.new(pw, salt + struct.pack(">i", i), digestmod).digest()
|
||||
for i in range(2, count + 1):
|
||||
# in subsequent iterations, the hmac message is the
|
||||
# previous hmac digest. The key is always the users password
|
||||
# see the hmac specification for notes on padding and stretching
|
||||
u = hmac.new(pw, u, digestmod).digest()
|
||||
# this is the exclusive or of the two byte-strings
|
||||
r = bytes(i ^ j for i, j in zip(r, u))
|
||||
return r
|
||||
dk, h_length = b'', digestmod().digest_size
|
||||
# we generate as many blocks as are required to
|
||||
# concatinate to the desired key size:
|
||||
blocks = (dk_length // h_length) + (1 if dk_length % h_length else 0)
|
||||
for i in range(1, blocks + 1):
|
||||
dk += pbkdf2_function(password, salt, count, i)
|
||||
# The length of the key wil be dk_length to the nearest
|
||||
# hash block size, i.e. larger than or equal to it. We
|
||||
# slice it to the desired length befor returning it.
|
||||
return dk[:dk_length]
|
||||
|
||||
def run():
|
||||
puk = '{:012d}'.format(secrets.randbelow(999999999999))
|
||||
pairing = secrets.token_urlsafe(12)
|
||||
pairing_key = pbkdf2(hashlib.sha256, pairing.encode('utf-8'), 'Status Hardware Wallet Lite'.encode('utf-8'), 50000, 32).hex()
|
||||
perso_script = gpshell_template.format(puk.encode('utf-8').hex(), pairing_key)
|
||||
subprocess.run("gpshell", shell=True, check=True, input=perso_script.encode('utf-8'))
|
||||
|
||||
print('\n**************************************\nPairing password: {:s}\nPUK: {:s}'.format(pairing, puk))
|
||||
if __name__ == '__main__':
|
||||
run()
|
Loading…
Reference in New Issue