diff --git a/APPLICATION.MD b/APPLICATION.MD index 63d215d..3abb5d2 100644 --- a/APPLICATION.MD +++ b/APPLICATION.MD @@ -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 diff --git a/README.md b/README.md index 450bf78..94baa76 100644 --- a/README.md +++ b/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. diff --git a/SECURE_CHANNEL.MD b/SECURE_CHANNEL.MD index 06873d4..bca9e0d 100644 --- a/SECURE_CHANNEL.MD +++ b/SECURE_CHANNEL.MD @@ -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 diff --git a/build.gradle b/build.gradle index c78ad6f..77608cf 100644 --- a/build.gradle +++ b/build.gradle @@ -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 diff --git a/src/main/java/im/status/wallet/SecureChannel.java b/src/main/java/im/status/wallet/SecureChannel.java index 29c8d7b..760394d 100644 --- a/src/main/java/im/status/wallet/SecureChannel.java +++ b/src/main/java/im/status/wallet/SecureChannel.java @@ -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 SecureChannel(byte pairingLimit, byte[] aPairingSecret, short off, Crypto 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 void reset() { 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 diff --git a/src/main/java/im/status/wallet/WalletApplet.java b/src/main/java/im/status/wallet/WalletApplet.java index 6ba6755..8af3984 100644 --- a/src/main/java/im/status/wallet/WalletApplet.java +++ b/src/main/java/im/status/wallet/WalletApplet.java @@ -9,6 +9,7 @@ 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 static void install(byte[] bArray, short bOffset, byte bLength) { } /** - * 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 WalletApplet(byte[] bArray, short bOffset, byte bLength) { 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 WalletApplet(byte[] bArray, short bOffset, byte bLength) { * @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 void process(APDU apdu) throws ISOException { } } + /** + * 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 @@ private void verifyPIN(APDU apdu) { } /** - * 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 @@ private void changePIN(APDU apdu) { 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 @@ private void changePIN(APDU apdu) { 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 diff --git a/src/test/java/im/status/wallet/SecureChannelSession.java b/src/test/java/im/status/wallet/SecureChannelSession.java index c9c208c..4f58112 100644 --- a/src/test/java/im/status/wallet/SecureChannelSession.java +++ b/src/test/java/im/status/wallet/SecureChannelSession.java @@ -7,6 +7,7 @@ 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 void reset() { 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. diff --git a/src/test/java/im/status/wallet/WalletAppletCommandSet.java b/src/test/java/im/status/wallet/WalletAppletCommandSet.java index 07ea236..d389064 100644 --- a/src/test/java/im/status/wallet/WalletAppletCommandSet.java +++ b/src/test/java/im/status/wallet/WalletAppletCommandSet.java @@ -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.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 ResponseAPDU verifyPIN(String pin) throws CardException { * 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 ResponseAPDU exportKey(byte keyPathIndex, boolean publicOnly) throws Card 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); + } } diff --git a/src/test/java/im/status/wallet/WalletAppletTest.java b/src/test/java/im/status/wallet/WalletAppletTest.java index f77a795..27fcfd0 100644 --- a/src/test/java/im/status/wallet/WalletAppletTest.java +++ b/src/test/java/im/status/wallet/WalletAppletTest.java @@ -65,8 +65,7 @@ static void initAll() throws CardException { 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 @@ static void initAll() throws CardException { 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 @@ void verifyPinTest() throws CardException { @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 @@ void changePinTest() throws CardException { 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 @@ void unblockPinTest() throws CardException { 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 @@ void signDataTest() throws Exception { // 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()); diff --git a/status_hw_perso.py b/status_hw_perso.py deleted file mode 100644 index 46c9b30..0000000 --- a/status_hw_perso.py +++ /dev/null @@ -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() \ No newline at end of file