implement PAIR/UNPAIR

This commit is contained in:
Michele Balistreri 2017-11-14 18:04:22 +03:00
parent 2d63b34afa
commit e11d817c64
6 changed files with 219 additions and 39 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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