Merge pull request #5 from status-im/no-inst-params

Move installation parameters to INIT command
This commit is contained in:
Bitgamma 2018-10-10 16:30:48 +03:00 committed by GitHub
commit 643f81b60c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 363 additions and 131 deletions

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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);
}
}

View File

@ -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());

View File

@ -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()