mirror of
https://github.com/status-im/status-keycard.git
synced 2025-02-03 01:13:29 +00:00
implement secure channel key exchange (open secure channel)
This commit is contained in:
parent
5ddffcc10c
commit
5a70ed2113
@ -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
5
install_applet.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
pushd scripts
|
||||
gpshell <statuswallet_install.gpshell
|
||||
popd
|
61
src/main/java/im/status/wallet/SecureChannel.java
Normal file
61
src/main/java/im/status/wallet/SecureChannel.java
Normal 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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
62
src/test/java/im/status/wallet/SecureChannelSession.java
Normal file
62
src/test/java/im/status/wallet/SecureChannelSession.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user