implement PAIR/UNPAIR
This commit is contained in:
parent
2d63b34afa
commit
e11d817c64
|
@ -26,11 +26,11 @@ opened.
|
|||
|
||||
* CLA = 0x80
|
||||
* INS = 0x10
|
||||
* P1 = the pairing index (1-127)
|
||||
* P1 = the pairing index
|
||||
* P2 = 0x00
|
||||
* Data = An EC-256 public key on the SECP256k1 curve encoded as an uncompressed point.
|
||||
* Response Data = A 256-bit salt
|
||||
* Response SW = 0x9000
|
||||
* Response SW = 0x9000 on success, 0x6A86 if P1 is invalid
|
||||
|
||||
This APDU is sent to establish a Secure Channel session. A session is aborted when the application is deselected,
|
||||
either directly or because of a card reset/tear. This APDU and its response are not encrypted.
|
||||
|
@ -54,7 +54,8 @@ TODO: define a second step where client and card mutually verify that they have
|
|||
* Data = see below
|
||||
* Response Data = see below
|
||||
* Response SW = 0x9000 on success, 0x6A80 if the data are in the wrong format, 0x6982 if client cryptogram verification
|
||||
fails, 0x6A84 if all available pairing slot are taken
|
||||
fails, 0x6A84 if all available pairing slot are taken, 0x6A86 if P1 is invalid or is 0x01 but the first phase was not
|
||||
completed
|
||||
|
||||
P1:
|
||||
* 0x00: First step
|
||||
|
@ -82,6 +83,9 @@ available pairing slot and will be further used to derive session keys. The card
|
|||
the client must send in all OPEN SECURE CHANNEL commands) and the salt used to generate the key, so that the client can
|
||||
generate and store the same key.
|
||||
|
||||
The shared secret is a 256-bit value which must be be known to both parts being paired. The exact means of how this
|
||||
happens depend on the specific applet.
|
||||
|
||||
### UNPAIR
|
||||
|
||||
* CLA = 0x80
|
||||
|
@ -89,12 +93,14 @@ generate and store the same key.
|
|||
* P1 = the index to unpair
|
||||
* P2 = 0x00
|
||||
* Data = the same index as in P1
|
||||
* Response SW = 0x9000 on success, 0x6985 if security conditions are not met
|
||||
* Response SW = 0x9000 on success, 0x6985 if security conditions are not met, 0x6A86 if the index is higher than the
|
||||
highest possible pairing index.
|
||||
|
||||
This APDU is sent to unpair a client. An existing secure channel session must be open. The application implementing this
|
||||
protocol may apply additional restrictions, such as the verification of a user PIN. The reason to repeat P1 in the data
|
||||
field is to verify that the client indeed performed authentication and has the correct session keys. On success the
|
||||
pairing slot at the given index will be freed and will be made available to pair other clients.
|
||||
pairing slot at the given index will be freed and will be made available to pair other clients. If the index is already
|
||||
free nothing will happen.
|
||||
|
||||
### Encrypted APDUs
|
||||
|
||||
|
|
|
@ -20,6 +20,8 @@ public class Crypto {
|
|||
|
||||
// The below two objects are meant to be access from the entire applet
|
||||
static RandomData random;
|
||||
static KeyAgreement ecdh;
|
||||
|
||||
static MessageDigest sha256;
|
||||
|
||||
private static MessageDigest sha512;
|
||||
|
@ -34,6 +36,7 @@ public class Crypto {
|
|||
static void init() {
|
||||
random = RandomData.getInstance(RandomData.ALG_SECURE_RANDOM);
|
||||
sha256 = MessageDigest.getInstance(MessageDigest.ALG_SHA_256, false);
|
||||
ecdh = KeyAgreement.getInstance(KeyAgreement.ALG_EC_SVDP_DH_PLAIN, false);
|
||||
|
||||
short blockSize;
|
||||
|
||||
|
|
|
@ -53,7 +53,6 @@ public class SECP256k1 {
|
|||
private static final byte ALG_EC_SVDP_DH_PLAIN_XY = 6; // constant from JavaCard 3.0.5
|
||||
|
||||
private static KeyAgreement ecPointMultiplier;
|
||||
private static KeyAgreement ecPointMultiplierX;
|
||||
|
||||
/**
|
||||
* Allocates objects needed by this class. Must be invoked during the applet installation exactly 1 time.
|
||||
|
@ -64,8 +63,6 @@ public class SECP256k1 {
|
|||
} catch(CryptoException e) {
|
||||
ecPointMultiplier = null;
|
||||
}
|
||||
|
||||
ecPointMultiplierX = KeyAgreement.getInstance(KeyAgreement.ALG_EC_SVDP_DH_PLAIN, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -106,8 +103,8 @@ public class SECP256k1 {
|
|||
* @return the length of X
|
||||
*/
|
||||
static short derivePublicX(ECPrivateKey privateKey, byte[] xOut, short xOff) {
|
||||
ecPointMultiplierX.init(privateKey);
|
||||
return ecPointMultiplierX.generateSecret(SECP256K1_G, (short) 0, (short) SECP256K1_G.length, xOut, xOff);
|
||||
Crypto.ecdh.init(privateKey);
|
||||
return Crypto.ecdh.generateSecret(SECP256K1_G, (short) 0, (short) SECP256K1_G.length, xOut, xOff);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package im.status.wallet;
|
||||
|
||||
import javacard.framework.APDU;
|
||||
import javacard.framework.ISO7816;
|
||||
import javacard.framework.JCSystem;
|
||||
import javacard.framework.*;
|
||||
import javacard.security.*;
|
||||
import javacardx.crypto.Cipher;
|
||||
|
||||
|
@ -12,22 +10,36 @@ import javacardx.crypto.Cipher;
|
|||
public class SecureChannel {
|
||||
public static final short SC_KEY_LENGTH = 256;
|
||||
public static final short SC_SECRET_LENGTH = 32;
|
||||
public static final short PAIRING_KEY_LENGTH = SC_SECRET_LENGTH + 1;
|
||||
public static final short SC_BLOCK_SIZE = 16;
|
||||
public static final short SC_OUT_OFFSET = ISO7816.OFFSET_CDATA + (SC_BLOCK_SIZE * 2);
|
||||
|
||||
public static final byte INS_OPEN_SECURE_CHANNEL = 0x10;
|
||||
public static final byte INS_PAIR = 0x11;
|
||||
public static final byte INS_UNPAIR = 0x12;
|
||||
|
||||
public static final byte PAIR_P1_FIRST_STEP = 0x00;
|
||||
public static final byte PAIR_P1_LAST_STEP = 0x01;
|
||||
|
||||
private KeyAgreement scAgreement;
|
||||
private AESKey scKey;
|
||||
private Cipher scCipher;
|
||||
private KeyPair scKeypair;
|
||||
private byte[] secret;
|
||||
private byte[] pairingSecret;
|
||||
|
||||
/*
|
||||
* To avoid overhead, the pairing keys are stored in a plain byte array as sequences of 33-bytes elements. The first
|
||||
* byte is 0 if the slot is free and 1 if used. The following 32 bytes are the actual key data.
|
||||
*/
|
||||
private byte[] pairingKeys;
|
||||
|
||||
private short preassignedPairingOffset = -1;
|
||||
|
||||
/**
|
||||
* Instantiates a Secure Channel. All memory allocations needed for the secure channel are peformed here. The keypair
|
||||
* 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.
|
||||
*/
|
||||
public SecureChannel() {
|
||||
public SecureChannel(byte pairingLimit, byte[] aPairingSecret, short off) {
|
||||
scCipher = Cipher.getInstance(Cipher.ALG_AES_CBC_ISO9797_M2,false);
|
||||
|
||||
scKey = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES_TRANSIENT_DESELECT, KeyBuilder.LENGTH_AES_256, false);
|
||||
|
@ -37,28 +49,150 @@ public class SecureChannel {
|
|||
SECP256k1.setCurveParameters((ECKey) scKeypair.getPublic());
|
||||
scKeypair.genKeyPair();
|
||||
|
||||
scAgreement = KeyAgreement.getInstance(KeyAgreement.ALG_EC_SVDP_DH, false);
|
||||
scAgreement.init(scKeypair.getPrivate());
|
||||
|
||||
secret = JCSystem.makeTransientByteArray(SC_SECRET_LENGTH, JCSystem.CLEAR_ON_DESELECT);
|
||||
pairingSecret = new byte[SC_SECRET_LENGTH];
|
||||
pairingKeys = new byte[(short)(PAIRING_KEY_LENGTH * pairingLimit)];
|
||||
|
||||
Util.arrayCopyNonAtomic(aPairingSecret, off, pairingSecret, (short) 0, SC_SECRET_LENGTH);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Processes the OPEN SECURE CHANNEL command.
|
||||
*
|
||||
* @param apdu the JCRE-owned APDU object.
|
||||
*/
|
||||
public void openSecureChannel(APDU apdu) {
|
||||
preassignedPairingOffset = -1;
|
||||
|
||||
apdu.setIncomingAndReceive();
|
||||
byte[] apduBuffer = apdu.getBuffer();
|
||||
short len = scAgreement.generateSecret(apduBuffer, ISO7816.OFFSET_CDATA, apduBuffer[ISO7816.OFFSET_LC], secret, (short) 0);
|
||||
|
||||
short pairingKeyOff = checkPairingIndexAndGetOffset(apduBuffer[ISO7816.OFFSET_P1]);
|
||||
|
||||
if (pairingKeys[pairingKeyOff] != 1) {
|
||||
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
|
||||
} else {
|
||||
pairingKeyOff++;
|
||||
}
|
||||
|
||||
Crypto.ecdh.init(scKeypair.getPrivate());
|
||||
short len = Crypto.ecdh.generateSecret(apduBuffer, ISO7816.OFFSET_CDATA, apduBuffer[ISO7816.OFFSET_LC], secret, (short) 0);
|
||||
Crypto.random.generateData(apduBuffer, (short) 0, SC_SECRET_LENGTH);
|
||||
Crypto.sha256.update(secret, (short) 0, len);
|
||||
Crypto.sha256.update(pairingSecret, pairingKeyOff, SC_SECRET_LENGTH);
|
||||
Crypto.sha256.doFinal(apduBuffer, (short) 0, SC_SECRET_LENGTH, secret, (short) 0);
|
||||
scKey.setKey(secret, (short) 0);
|
||||
apdu.setOutgoingAndSend((short) 0, SC_SECRET_LENGTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the PAIR command.
|
||||
*
|
||||
* @param apdu the JCRE-owned APDU object.
|
||||
*/
|
||||
public void pair(APDU apdu) {
|
||||
apdu.setIncomingAndReceive();
|
||||
byte[] apduBuffer = apdu.getBuffer();
|
||||
|
||||
if (apduBuffer[ISO7816.OFFSET_LC] != SC_SECRET_LENGTH) {
|
||||
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
||||
}
|
||||
|
||||
short len;
|
||||
|
||||
if (apduBuffer[ISO7816.OFFSET_P1] == PAIR_P1_FIRST_STEP) {
|
||||
len = pairStep1(apduBuffer);
|
||||
} else if ((apduBuffer[ISO7816.OFFSET_P1] == PAIR_P1_LAST_STEP) && (preassignedPairingOffset != -1)) {
|
||||
len = pairStep2(apduBuffer);
|
||||
} else {
|
||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||
return;
|
||||
}
|
||||
|
||||
apdu.setOutgoingAndSend((short) 0, len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the first step of pairing. In this step the card solves the challenge sent by the card, thus authenticating
|
||||
* itself to the client. At the same time, it creates a challenge for the client. This can only fail if the card has
|
||||
* already paired with the maximum allowed amount of clients.
|
||||
*
|
||||
* @param apduBuffer the APDU buffer
|
||||
* @return the length of the reply
|
||||
*/
|
||||
private short pairStep1(byte[] apduBuffer) {
|
||||
preassignedPairingOffset = -1;
|
||||
|
||||
for (short i = 0; i < (short) pairingKeys.length; i += PAIRING_KEY_LENGTH) {
|
||||
if (pairingKeys[i] == 0) {
|
||||
preassignedPairingOffset = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (preassignedPairingOffset == -1) {
|
||||
ISOException.throwIt(ISO7816.SW_FILE_FULL);
|
||||
}
|
||||
|
||||
Crypto.sha256.update(pairingSecret, (short) 0, SC_SECRET_LENGTH);
|
||||
Crypto.sha256.doFinal(apduBuffer, ISO7816.OFFSET_CDATA, SC_SECRET_LENGTH, apduBuffer, (short) 0);
|
||||
Crypto.random.generateData(secret, (short) 0, SC_SECRET_LENGTH);
|
||||
Util.arrayCopyNonAtomic(secret, (short) 0, apduBuffer, SC_SECRET_LENGTH, SC_SECRET_LENGTH);
|
||||
|
||||
return (SC_SECRET_LENGTH * 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the last step of pairing. In this step the card verifies that the client has correctly solved its
|
||||
* challenge, authenticating it. It then proceeds to generate the pairing key and returns to the client the data
|
||||
* necessary to further establish a secure channel session.
|
||||
*
|
||||
* @param apduBuffer the APDU buffer
|
||||
* @return the length of the reply
|
||||
*/
|
||||
private short pairStep2(byte[] apduBuffer) {
|
||||
Crypto.sha256.update(pairingSecret, (short) 0, SC_SECRET_LENGTH);
|
||||
Crypto.sha256.doFinal(secret, (short) 0, SC_SECRET_LENGTH, secret, (short) 0);
|
||||
|
||||
if (Util.arrayCompare(apduBuffer, ISO7816.OFFSET_CDATA, secret, (short) 0, SC_SECRET_LENGTH) != 0) {
|
||||
preassignedPairingOffset = -1;
|
||||
ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);
|
||||
}
|
||||
|
||||
Crypto.random.generateData(apduBuffer, (short) 1, SC_SECRET_LENGTH);
|
||||
Crypto.sha256.update(pairingSecret, (short) 0, SC_SECRET_LENGTH);
|
||||
Crypto.sha256.doFinal(apduBuffer, (short) 1, SC_SECRET_LENGTH, pairingKeys, (short) (preassignedPairingOffset + 1));
|
||||
pairingKeys[preassignedPairingOffset] = 1;
|
||||
apduBuffer[0] = (byte) (preassignedPairingOffset / PAIRING_KEY_LENGTH);
|
||||
|
||||
preassignedPairingOffset = -1;
|
||||
|
||||
return (SC_SECRET_LENGTH + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the UNPAIR command. For security reasons the key is not only marked as free but also zero-ed out.
|
||||
*
|
||||
* @param apdu the JCRE-owned APDU object.
|
||||
*/
|
||||
public void unpair(APDU apdu) {
|
||||
if (!isOpen()) {
|
||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||
}
|
||||
|
||||
apdu.setIncomingAndReceive();
|
||||
byte[] apduBuffer = apdu.getBuffer();
|
||||
short v = decryptAPDU(apduBuffer);
|
||||
|
||||
if ((v != 1) || (apduBuffer[ISO7816.OFFSET_CDATA] != apduBuffer[ISO7816.OFFSET_P1])) {
|
||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||
}
|
||||
|
||||
v = checkPairingIndexAndGetOffset(apduBuffer[ISO7816.OFFSET_P1]);
|
||||
Util.arrayFillNonAtomic(pairingKeys, v, PAIRING_KEY_LENGTH, (byte) 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the given APDU buffer. The plaintext is written in-place starting at the ISO7816.OFFSET_CDATA offset. The
|
||||
* IV and padding are stripped. The LC byte is overwritten with the plaintext length.
|
||||
|
@ -111,4 +245,21 @@ public class SecureChannel {
|
|||
public boolean isOpen() {
|
||||
return scKey.isInitialized();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset in the pairingKey byte array of the pairing key with the given index. Throws 0x6A86 if the index
|
||||
* is invalid
|
||||
*
|
||||
* @param idx the index
|
||||
* @return the offset
|
||||
*/
|
||||
private short checkPairingIndexAndGetOffset(byte idx) {
|
||||
short off = (short) (idx * PAIRING_KEY_LENGTH);
|
||||
|
||||
if (off >= ((short) pairingKeys.length)) {
|
||||
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
|
||||
}
|
||||
|
||||
return off;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,8 @@ public class WalletApplet extends Applet {
|
|||
static final byte PUK_MAX_RETRIES = 5;
|
||||
static final byte PIN_LENGTH = 6;
|
||||
static final byte PIN_MAX_RETRIES = 3;
|
||||
static final short KEY_PATH_MAX_DEPTH = 10;
|
||||
static final byte KEY_PATH_MAX_DEPTH = 10;
|
||||
static final byte PAIRING_MAX_CLIENT_COUNT = 5;
|
||||
|
||||
static final short EC_KEY_SIZE = 256;
|
||||
static final short CHAIN_CODE_SIZE = 32;
|
||||
|
@ -118,25 +119,13 @@ public class WalletApplet extends Applet {
|
|||
* @param bLength length of the installation parameters
|
||||
*/
|
||||
public WalletApplet(byte[] bArray, short bOffset, byte bLength) {
|
||||
SECP256k1.init();
|
||||
Crypto.init();
|
||||
|
||||
short c9Off = (short)(bOffset + bArray[bOffset] + 1); // Skip AID
|
||||
c9Off += (short)(bArray[c9Off] + 2); // Skip Privileges and parameter length
|
||||
|
||||
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();
|
||||
SECP256k1.init();
|
||||
|
||||
masterPublic = (ECPublicKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PUBLIC, EC_KEY_SIZE, false);
|
||||
masterPrivate = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, EC_KEY_SIZE, false);
|
||||
masterChainCode = new byte[32];
|
||||
chainCode = new byte[32];
|
||||
masterChainCode = new byte[CHAIN_CODE_SIZE];
|
||||
chainCode = new byte[CHAIN_CODE_SIZE];
|
||||
keyPath = new byte[KEY_PATH_MAX_DEPTH * 4];
|
||||
pinlessPath = new byte[KEY_PATH_MAX_DEPTH * 4];
|
||||
|
||||
|
@ -151,6 +140,20 @@ public class WalletApplet extends Applet {
|
|||
|
||||
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
|
||||
|
||||
Crypto.sha256.doFinal(bArray, c9Off, PUK_LENGTH, chainCode, (short) 0);
|
||||
secureChannel = new SecureChannel(PAIRING_MAX_CLIENT_COUNT, chainCode, (short) 0);
|
||||
Util.arrayFillNonAtomic(chainCode, (short) 0, CHAIN_CODE_SIZE, (byte) 0);
|
||||
|
||||
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);
|
||||
|
||||
register(bArray, (short) (bOffset + 1), bArray[bOffset]);
|
||||
}
|
||||
|
||||
|
@ -174,6 +177,12 @@ public class WalletApplet extends Applet {
|
|||
case SecureChannel.INS_OPEN_SECURE_CHANNEL:
|
||||
secureChannel.openSecureChannel(apdu);
|
||||
break;
|
||||
case SecureChannel.INS_PAIR:
|
||||
secureChannel.pair(apdu);
|
||||
break;
|
||||
case SecureChannel.INS_UNPAIR:
|
||||
unpair(apdu);
|
||||
break;
|
||||
case INS_GET_STATUS:
|
||||
getStatus(apdu);
|
||||
break;
|
||||
|
@ -210,6 +219,21 @@ public class WalletApplet extends Applet {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the PIN is validated and if it is call the unpair method of the secure channel. If the PIN is not
|
||||
* validated the 0x6985 exception is thrown.
|
||||
*
|
||||
* @param apdu the JCRE-owned APDU object.
|
||||
*/
|
||||
private void unpair(APDU apdu) {
|
||||
if (pin.isValidated()) {
|
||||
secureChannel.unpair(apdu);
|
||||
} else {
|
||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked on applet (re-)selection. Aborts any in-progress signing session and sets PIN and PUK to not verified.
|
||||
* Responds with a SECP256k1 public key which the client must use to establish a secure channel.
|
||||
|
|
|
@ -29,8 +29,7 @@ public class SecureChannelSession {
|
|||
|
||||
/**
|
||||
* Constructs a SecureChannel session on the client. The client should generate a fresh key pair for each session.
|
||||
* The public key of the card is used as input for the EC-DH algorithm. The output is hashed with SHA1 and is stored
|
||||
* as the secret.
|
||||
* The public key of the card is used as input for the EC-DH algorithm. The output is stored as the secret.
|
||||
*
|
||||
* @param keyData the public key returned by the applet as response to the SELECT command
|
||||
*/
|
||||
|
@ -51,7 +50,7 @@ public class SecureChannelSession {
|
|||
ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA", "BC").generatePublic(cardKeySpec);
|
||||
|
||||
keyAgreement.doPhase(cardKey, true);
|
||||
secret = MessageDigest.getInstance("SHA1", "BC").digest(keyAgreement.generateSecret());
|
||||
secret = keyAgreement.generateSecret();
|
||||
|
||||
} catch(Exception e) {
|
||||
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
|
||||
|
|
Loading…
Reference in New Issue