implement secure channel key exchange (open secure channel)

This commit is contained in:
Michele Balistreri 2017-09-26 13:05:59 +03:00
parent 5ddffcc10c
commit 5a70ed2113
6 changed files with 196 additions and 9 deletions

View File

@ -11,6 +11,8 @@ buildscript {
}
javacard {
sdkVersion = "2.2.2"
cap {
aid = '0x53:0x74:0x61:0x74:0x75:0x73:0x57:0x61:0x6c:0x6c:0x65:0x74'
packageName = 'im.status.wallet'
@ -27,7 +29,7 @@ repositories {
}
dependencies {
testCompile("com.licel:jcardsim:2.2.2")
testCompile("org.bouncycastle:bcprov-jdk15on:1.58")
testCompile("org.junit.jupiter:junit-jupiter-api:5.0.0")
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.0.0")
}

5
install_applet.sh Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
pushd scripts
gpshell <statuswallet_install.gpshell
popd

View File

@ -0,0 +1,61 @@
package im.status.wallet;
import javacard.framework.APDU;
import javacard.framework.ISO7816;
import javacard.framework.JCSystem;
import javacard.security.*;
import javacardx.crypto.Cipher;
public class SecureChannel {
public static final short SC_KEY_SIZE = 256;
public static final short SC_SECRET_LENGTH = 32;
public static final byte INS_OPEN_SECURE_CHANNEL = 0x10;
private KeyAgreement scAgreement;
private AESKey scKey;
private Cipher scCipher;
private KeyPair scKeypair;
private MessageDigest scMd;
private RandomData scRandom;
private byte[] secret;
public SecureChannel() {
scRandom = RandomData.getInstance(RandomData.ALG_SECURE_RANDOM);
scMd = MessageDigest.getInstance(MessageDigest.ALG_SHA_256, false);
scCipher = Cipher.getInstance(Cipher.ALG_AES_BLOCK_128_CBC_NOPAD, false);
scKey = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES_TRANSIENT_DESELECT, KeyBuilder.LENGTH_AES_256, false);
scKeypair = new KeyPair(KeyPair.ALG_EC_FP, SC_KEY_SIZE);
ECCurves.setSECP256K1CurveParameters((ECKey) scKeypair.getPrivate());
ECCurves.setSECP256K1CurveParameters((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);
}
public void openSecureChannel(APDU apdu) {
apdu.setIncomingAndReceive();
byte[] apduBuffer = apdu.getBuffer();
short len = scAgreement.generateSecret(apduBuffer, ISO7816.OFFSET_CDATA, apduBuffer[ISO7816.OFFSET_LC], secret, (short) 0);
scRandom.generateData(apduBuffer, (short) 0, SC_SECRET_LENGTH);
scMd.update(secret, (short) 0, len);
scMd.doFinal(apduBuffer, (short) 0, SC_SECRET_LENGTH, secret, (short) 0);
scKey.setKey(secret, (short) 0);
apdu.setOutgoingAndSend((short) 0, SC_SECRET_LENGTH);
}
public short copyPublicKey(byte[] buf, byte off) {
ECPublicKey pk = (ECPublicKey) scKeypair.getPublic();
return pk.getW(buf, off);
}
public boolean isOpen() {
return scKey.isInitialized();
}
}

View File

@ -1,6 +1,8 @@
package im.status.wallet;
import javacard.framework.*;
import javacard.security.ECKey;
import javacard.security.KeyPair;
public class WalletApplet extends Applet {
static final byte INS_VERIFY_PIN = (byte) 0x20;
@ -12,8 +14,12 @@ public class WalletApplet extends Applet {
static final byte PIN_LENGTH = 6;
static final byte PIN_MAX_RETRIES = 3;
static final short TMP_BUFFER_LENGTH = 32;
public static final short EC_KEY_SIZE = 256;
private OwnerPIN ownerPIN;
private SecureChannel secureChannel;
private KeyPair keypair;
private byte[] tmp;
public static void install(byte[] bArray, short bOffset, byte bLength) {
@ -27,18 +33,26 @@ public class WalletApplet extends Applet {
ownerPIN = new OwnerPIN(PIN_MAX_RETRIES, PIN_LENGTH);
ownerPIN.update(tmp, (short) 0, PIN_LENGTH);
secureChannel = new SecureChannel();
keypair = new KeyPair(KeyPair.ALG_EC_FP, EC_KEY_SIZE);
ECCurves.setSECP256K1CurveParameters((ECKey) keypair.getPrivate());
ECCurves.setSECP256K1CurveParameters((ECKey) keypair.getPublic());
register(bArray, (short) (bOffset + 1), bArray[0]);
}
public void process(APDU apdu) throws ISOException {
if (selectingApplet()) {
apdu.setIncomingAndReceive();
selectApplet(apdu);
return;
}
byte[] apduBuffer = apdu.getBuffer();
switch(apduBuffer[ISO7816.OFFSET_INS]) {
case SecureChannel.INS_OPEN_SECURE_CHANNEL:
secureChannel.openSecureChannel(apdu);
break;
case INS_VERIFY_PIN:
verifyPIN(apdu);
break;
@ -60,7 +74,19 @@ public class WalletApplet extends Applet {
}
}
private void selectApplet(APDU apdu) {
apdu.setIncomingAndReceive();
short keyLength = secureChannel.copyPublicKey(apdu.getBuffer(), ISO7816.OFFSET_CDATA);
apdu.setOutgoingAndSend(ISO7816.OFFSET_CDATA, keyLength);
}
private void verifyPIN(APDU apdu) {
apdu.setIncomingAndReceive();
if (!secureChannel.isOpen()) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
byte[] apduBuffer = apdu.getBuffer();
if (!ownerPIN.check(apduBuffer, ISO7816.OFFSET_CDATA, apduBuffer[ISO7816.OFFSET_LC])) {
@ -69,14 +95,34 @@ public class WalletApplet extends Applet {
}
private void changePIN(APDU apdu) {
apdu.setIncomingAndReceive();
if (!(secureChannel.isOpen() && ownerPIN.isValidated())) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
}
private void unblockPIN(APDU apdu) {
apdu.setIncomingAndReceive();
if (!(secureChannel.isOpen() && ownerPIN.getTriesRemaining() == 0)) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
}
private void loadKey(APDU apdu) {
apdu.setIncomingAndReceive();
if (!(secureChannel.isOpen() && ownerPIN.isValidated())) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
}
private void sign(APDU apdu) {
apdu.setIncomingAndReceive();
if (!(secureChannel.isOpen() && ownerPIN.isValidated() && keypair.getPrivate().isInitialized())) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
}
}

View File

@ -0,0 +1,62 @@
package im.status.wallet;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.interfaces.ECPublicKey;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.jce.spec.ECPublicKeySpec;
import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.spec.SecretKeySpec;
import javax.smartcardio.CardChannel;
import javax.smartcardio.CardException;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
import java.security.*;
public class SecureChannelSession {
private byte[] secret;
private byte[] publicKey;
private Cipher sessionCipher;
private SecretKeySpec sessionKey;
public SecureChannelSession(byte[] keyData) {
try {
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
KeyPairGenerator g = KeyPairGenerator.getInstance("ECDH", "BC");
g.initialize(ecSpec, new SecureRandom());
KeyPair keyPair = g.generateKeyPair();
publicKey = ((ECPublicKey) keyPair.getPublic()).getQ().getEncoded(false);
KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH", "BC");
keyAgreement.init(keyPair.getPrivate());
ECPublicKeySpec cardKeySpec = new ECPublicKeySpec(ecSpec.getCurve().decodePoint(keyData), ecSpec);
ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA", "BC").generatePublic(cardKeySpec);
keyAgreement.doPhase(cardKey, true);
secret = MessageDigest.getInstance("SHA1", "BC").digest(keyAgreement.generateSecret());
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
}
public ResponseAPDU openSecureChannel(CardChannel apduChannel) throws CardException {
CommandAPDU openSecureChannel = new CommandAPDU(0x80, SecureChannel.INS_OPEN_SECURE_CHANNEL, 0, 0, publicKey);
ResponseAPDU response = apduChannel.transmit(openSecureChannel);
byte[] salt = response.getData();
try {
MessageDigest md = MessageDigest.getInstance("SHA256", "BC");
md.update(secret);
sessionKey = new SecretKeySpec(md.digest(salt), "AES");
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding", "BC");
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
return response;
}
}

View File

@ -1,22 +1,23 @@
package im.status.wallet;
import org.bouncycastle.util.encoders.Hex;
import org.junit.jupiter.api.*;
import javax.smartcardio.*;
import java.security.Security;
import static org.junit.jupiter.api.Assertions.assertEquals;
@DisplayName("Test the Wallet Applet")
public class WalletAppletTest {
private static final String APPLET_AID = "53746174757357616C6C6574417070";
private static final byte[] APPLET_AID_BYTES = Hex.decode(APPLET_AID);
private static CardTerminal cardTerminal;
private static CardChannel apduChannel;
private SecureChannelSession secureChannel;
@BeforeAll
static void initAll() throws CardException {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
TerminalFactory tf = TerminalFactory.getDefault();
for (CardTerminal t : tf.terminals().list()) {
@ -32,7 +33,8 @@ public class WalletAppletTest {
@BeforeEach
void init() throws CardException {
WalletAppletCommandSet.select(apduChannel);
byte[] keyData = WalletAppletCommandSet.select(apduChannel).getData();
secureChannel = new SecureChannelSession(keyData);
}
@AfterEach
@ -46,10 +48,19 @@ public class WalletAppletTest {
@Test
@DisplayName("SELECT command")
void selectTest() throws CardException {
//TODO: as soon as secure channel is implemented, check that a public key is returned.
ResponseAPDU response = WalletAppletCommandSet.select(apduChannel);
assertEquals(0x9000, response.getSW());
byte[] data = response.getData();
assertEquals(0x04, data[0]);
assertEquals((SecureChannel.SC_KEY_SIZE * 2 / 8) + 1, data.length);
}
@Test
@DisplayName("OPEN SECURE CHANNEL command")
void openSecureChannelTest() throws CardException {
ResponseAPDU response = secureChannel.openSecureChannel(apduChannel);
assertEquals(0x9000, response.getSW());
assertEquals(SecureChannel.SC_SECRET_LENGTH, response.getData().length);
}
@Test