diff --git a/lib/src/main/java/im/status/keycard/applet/CashCommandSet.java b/lib/src/main/java/im/status/keycard/applet/CashCommandSet.java index eb9c4ba..b362773 100644 --- a/lib/src/main/java/im/status/keycard/applet/CashCommandSet.java +++ b/lib/src/main/java/im/status/keycard/applet/CashCommandSet.java @@ -32,6 +32,18 @@ public class CashCommandSet { return apduChannel.send(selectApplet); } + /** + * Sends an IDENTIFY CARD APDU. The challenge is sent as APDU data as-is. It must be 32 bytes long + * + * @param challenge the data of the APDU + * @return the raw card response + * @throws IOException communication error + */ + public APDUResponse identifyCard(byte[] challenge) throws IOException { + APDUCommand identifyCard = new APDUCommand(0x80, KeycardCommandSet.INS_IDENTIFY_CARD, 0, 0, challenge); + return apduChannel.send(identifyCard); + } + /** * Sends a SIGN APDU. * diff --git a/lib/src/main/java/im/status/keycard/applet/Certificate.java b/lib/src/main/java/im/status/keycard/applet/Certificate.java new file mode 100644 index 0000000..601bbd3 --- /dev/null +++ b/lib/src/main/java/im/status/keycard/applet/Certificate.java @@ -0,0 +1,141 @@ +package im.status.keycard.applet; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.spec.ECGenParameterSpec; + +import org.bouncycastle.jce.ECNamedCurveTable; +import org.bouncycastle.jce.interfaces.ECPublicKey; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.jce.interfaces.ECPrivateKey; +import org.bouncycastle.jce.spec.ECPublicKeySpec; +import org.bouncycastle.util.Arrays; + +public class Certificate extends RecoverableSignature { + public static final byte TLV_CERT = (byte) 0x8A; + + private byte[] identPriv; + private byte[] identPub; + + public Certificate(byte[] publicKey, boolean compressed, byte[] r, byte[] s, int recId) { + super(publicKey, compressed,r, s, recId); + } + + public static KeyPair generateIdentKeyPair() { + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ECDSA", "BC"); + ECGenParameterSpec spec = new ECGenParameterSpec("secp256k1"); + keyPairGenerator.initialize(spec, new SecureRandom()); + return keyPairGenerator.generateKeyPair(); + } catch(Exception e) { + throw new RuntimeException("Is BouncyCastle in the classpath?"); + } + } + + public static Certificate createCertificate(KeyPair caPair, KeyPair identKeys) { + try { + byte[] pub = ((ECPublicKey) identKeys.getPublic()).getQ().getEncoded(true); + + MessageDigest md = MessageDigest.getInstance("SHA256", "BC"); + byte[] hash = md.digest(pub); + + Signature signer = Signature.getInstance("NONEwithECDSA", "BC"); + signer.initSign(caPair.getPrivate()); + signer.update(hash); + byte[] sig = signer.sign(); + + TinyBERTLV tlv = new TinyBERTLV(sig); + tlv.enterConstructed(TLV_ECDSA_TEMPLATE); + byte[] r = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT)); + byte[] s = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT)); + Certificate cert = new Certificate(((ECPublicKey)caPair.getPublic()).getQ().getEncoded(true), true, r, s, -1); + cert.calculateRecID(hash); + cert.identPriv = toUInt(((ECPrivateKey) identKeys.getPrivate()).getD().toByteArray()); + cert.identPub = pub; + + return cert; + } catch(IllegalArgumentException e) { + throw e; + } catch(Exception e) { + throw new RuntimeException("Is BouncyCastle in the classpath?"); + } + } + + public static Certificate generateNewCertificate(KeyPair caPair) { + return createCertificate(caPair, generateIdentKeyPair()); + } + + public static Certificate fromTLV(byte[] certData) { + try { + byte[] pub = Arrays.copyOfRange(certData, 0, 33); + byte[] r = Arrays.copyOfRange(certData, 33, 65); + byte[] s = Arrays.copyOfRange(certData, 65, 97); + int recId = certData[97]; + + MessageDigest md = MessageDigest.getInstance("SHA256", "BC"); + byte[] hash = md.digest(pub); + byte[] caPub = recoverFromSignature(recId, hash, r, s, true); + + Certificate cert = new Certificate(caPub, true, r, s, recId); + cert.identPub = pub; + + return cert; + } catch(IllegalArgumentException e) { + throw e; + } catch(Exception e) { + throw new RuntimeException("Is BouncyCastle in the classpath?"); + } + } + + public static byte[] verifyIdentity(byte[] hash, byte[] tlvData) { + try { + TinyBERTLV tlv = new TinyBERTLV(tlvData); + tlv.enterConstructed(TLV_SIGNATURE_TEMPLATE); + byte[] certData = tlv.readPrimitive(TLV_CERT); + Certificate cert = fromTLV(certData); + byte[] signature = tlv.peekUnread(); + Signature verifier = Signature.getInstance("NONEWithECDSA", "BC"); + + ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1"); + ECPublicKeySpec cardKeySpec = new ECPublicKeySpec(ecSpec.getCurve().decodePoint(cert.identPub), ecSpec); + ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA", "BC").generatePublic(cardKeySpec); + + verifier.initVerify(cardKey); + verifier.update(hash); + + if (!verifier.verify(signature)) { + return null; + } + + return cert.getPublicKey(); + } catch(Exception e) { + throw new RuntimeException("Is BouncyCastle in the classpath?"); + } + } + + public byte[] toStoreData() { + if (identPriv == null) { + throw new IllegalStateException("The private key must be set."); + } + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + try { + os.write(this.identPub); + os.write(this.getR()); + os.write(this.getS()); + os.write(this.getRecId()); + os.write(this.identPriv); + } catch(IOException e) { + throw new RuntimeException(e); + } + + return os.toByteArray(); + } +} diff --git a/lib/src/main/java/im/status/keycard/applet/IdentCommandSet.java b/lib/src/main/java/im/status/keycard/applet/IdentCommandSet.java new file mode 100644 index 0000000..e5656a1 --- /dev/null +++ b/lib/src/main/java/im/status/keycard/applet/IdentCommandSet.java @@ -0,0 +1,46 @@ +package im.status.keycard.applet; + +import im.status.keycard.io.APDUCommand; +import im.status.keycard.io.APDUResponse; +import im.status.keycard.io.CardChannel; + +import java.io.IOException; + +/** + * Command set for the Ident applet. + */ +public class IdentCommandSet { + private final CardChannel apduChannel; + + /** + * Creates a IdentCommandSet using the given APDU Channel + * @param apduChannel APDU channel + */ + public IdentCommandSet(CardChannel apduChannel) { + this.apduChannel = apduChannel; + } + + /** + * Selects a Cash instance. The applet is assumed to have been installed with its default AID. The returned data is + * a public key which must be used to initialize the secure channel. + * + * @return the raw card response + * @throws IOException communication error + */ + public APDUResponse select() throws IOException { + APDUCommand selectApplet = new APDUCommand(0x00, 0xA4, 4, 0, Identifiers.IDENT_INSTANCE_AID); + return apduChannel.send(selectApplet); + } + + /** + * Sends a STORE DATA APDU. + * + * @param data the data to sign + * @return the raw card response + * @throws IOException communication error + */ + public APDUResponse storeData(byte[] data) throws IOException { + APDUCommand sign = new APDUCommand(0x80, KeycardCommandSet.INS_STORE_DATA, 0x00, 0x00, data); + return apduChannel.send(sign); + } +} diff --git a/lib/src/main/java/im/status/keycard/applet/Identifiers.java b/lib/src/main/java/im/status/keycard/applet/Identifiers.java index 76b646e..3d43d45 100644 --- a/lib/src/main/java/im/status/keycard/applet/Identifiers.java +++ b/lib/src/main/java/im/status/keycard/applet/Identifiers.java @@ -16,6 +16,9 @@ public class Identifiers { public static final byte[] CASH_AID = Hex.decode("A000000804000103"); public static final byte[] CASH_INSTANCE_AID = Hex.decode("A00000080400010301"); + public static final byte[] IDENT_AID = Hex.decode("A000000804000104"); + public static final byte[] IDENT_INSTANCE_AID = Hex.decode("A00000080400010401"); + /** * Gets the instance AID of the default instance of the Keycard applet. * diff --git a/lib/src/main/java/im/status/keycard/applet/KeycardCommandSet.java b/lib/src/main/java/im/status/keycard/applet/KeycardCommandSet.java index 2b35f2b..c0d3047 100644 --- a/lib/src/main/java/im/status/keycard/applet/KeycardCommandSet.java +++ b/lib/src/main/java/im/status/keycard/applet/KeycardCommandSet.java @@ -20,6 +20,7 @@ public class KeycardCommandSet { static final byte INS_INIT = (byte) 0xFE; static final byte INS_GET_STATUS = (byte) 0xF2; static final byte INS_SET_NDEF = (byte) 0xF3; + static final byte INS_IDENTIFY_CARD = (byte) 0x14; static final byte INS_VERIFY_PIN = (byte) 0x20; static final byte INS_CHANGE_PIN = (byte) 0x21; static final byte INS_UNBLOCK_PIN = (byte) 0x22; @@ -269,6 +270,18 @@ public class KeycardCommandSet { secureChannel.unpairOthers(apduChannel); } + /** + * Sends an IDENTIFY CARD APDU. The challenge is sent as APDU data as-is. It must be 32 bytes long + * + * @param challenge the data of the APDU + * @return the raw card response + * @throws IOException communication error + */ + public APDUResponse identifyCard(byte[] challenge) throws IOException { + APDUCommand identifyCard = secureChannel.protectedCommand(0x80, INS_IDENTIFY_CARD, 0, 0, challenge); + return secureChannel.transmit(apduChannel, identifyCard); + } + /** * Sends a GET STATUS APDU. The info byte is the P1 parameter of the command, valid constants are defined in the applet * class itself. diff --git a/lib/src/main/java/im/status/keycard/applet/RecoverableSignature.java b/lib/src/main/java/im/status/keycard/applet/RecoverableSignature.java index 6b99c3b..38fea51 100644 --- a/lib/src/main/java/im/status/keycard/applet/RecoverableSignature.java +++ b/lib/src/main/java/im/status/keycard/applet/RecoverableSignature.java @@ -20,6 +20,7 @@ public class RecoverableSignature { private int recId; private byte[] r; private byte[] s; + private boolean compressed; public static final byte TLV_SIGNATURE_TEMPLATE = (byte) 0xA0; public static final byte TLV_ECDSA_TEMPLATE = (byte) 0x30; @@ -41,15 +42,28 @@ public class RecoverableSignature { public RecoverableSignature(byte[] hash, byte[] tlvData) { TinyBERTLV tlv = new TinyBERTLV(tlvData); tlv.enterConstructed(TLV_SIGNATURE_TEMPLATE); - publicKey = tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY); + this.publicKey = tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY); tlv.enterConstructed(TLV_ECDSA_TEMPLATE); - r = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT)); - s = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT)); + this.r = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT)); + this.s = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT)); + this.compressed = false; + calculateRecID(hash); + } + + public RecoverableSignature(byte[] publicKey, boolean compressed, byte[] r, byte[] s, int recId) { + this.publicKey = publicKey; + this.r = r; + this.s = s; + this.compressed = compressed; + this.recId = recId; + } + + void calculateRecID(byte[] hash) { recId = -1; for (int i = 0; i < 4; i++) { - byte[] candidate = recoverFromSignature(i, new BigInteger(1, hash), new BigInteger(1, r), new BigInteger(1, s)); + byte[] candidate = recoverFromSignature(i, hash, r, s, compressed); if (Arrays.equals(candidate, publicKey)) { recId = i; @@ -62,7 +76,7 @@ public class RecoverableSignature { } } - private byte[] toUInt(byte[] signedInt) { + static byte[] toUInt(byte[] signedInt) { if (signedInt[0] == 0) { return Arrays.copyOfRange(signedInt, 1, signedInt.length); } else { @@ -114,7 +128,15 @@ public class RecoverableSignature { return Ethereum.toEthereumAddress(publicKey); } - private static byte[] recoverFromSignature(int recId, BigInteger e, BigInteger r, BigInteger s) { + static byte[] recoverFromSignature(int recId, byte[] hash, byte[] r, byte[] s, boolean compressed) { + BigInteger h = new BigInteger(1, hash); + BigInteger br = new BigInteger(1, r); + BigInteger bs = new BigInteger(1, s); + + return recoverFromSignature(recId, h, br, bs, compressed); + } + + static byte[] recoverFromSignature(int recId, BigInteger e, BigInteger r, BigInteger s, boolean compressed) { BigInteger n = CURVE.getN(); BigInteger i = BigInteger.valueOf((long) recId / 2); BigInteger x = r.add(i.multiply(n)); @@ -135,7 +157,7 @@ public class RecoverableSignature { BigInteger srInv = rInv.multiply(s).mod(n); BigInteger eInvrInv = rInv.multiply(eInv).mod(n); ECPoint q = ECAlgorithms.sumOfTwoMultiplies(CURVE.getG(), eInvrInv, R, srInv); - return q.getEncoded(false); + return q.getEncoded(compressed); } private static ECPoint decompressKey(BigInteger xBN, boolean yBit) { diff --git a/lib/src/main/java/im/status/keycard/applet/TinyBERTLV.java b/lib/src/main/java/im/status/keycard/applet/TinyBERTLV.java index 41e5183..66a480f 100644 --- a/lib/src/main/java/im/status/keycard/applet/TinyBERTLV.java +++ b/lib/src/main/java/im/status/keycard/applet/TinyBERTLV.java @@ -119,6 +119,15 @@ public class TinyBERTLV { return TinyBERTLV.readVal(val, 0, val.length); } + /** + * Returns all unread bytes in the TLV. + * + * @return all unread bytes + */ + byte[] peekUnread() { + return Arrays.copyOfRange(buffer, pos, buffer.length); + } + /** * Low-level method to unread the last read tag. Only valid if the previous call was readTag(). Does nothing if the * end of the TLV has been reached. diff --git a/lib/src/main/java/im/status/keycard/globalplatform/GlobalPlatformCommandSet.java b/lib/src/main/java/im/status/keycard/globalplatform/GlobalPlatformCommandSet.java index 67f8a88..3ac7b61 100644 --- a/lib/src/main/java/im/status/keycard/globalplatform/GlobalPlatformCommandSet.java +++ b/lib/src/main/java/im/status/keycard/globalplatform/GlobalPlatformCommandSet.java @@ -250,6 +250,16 @@ public class GlobalPlatformCommandSet { return delete(Identifiers.NDEF_INSTANCE_AID); } + /** + * Deletes the Ident applet instance. + * + * @return the card response + * @throws IOException communication error + */ + public APDUResponse deleteIdentInstance() throws IOException { + return delete(Identifiers.IDENT_INSTANCE_AID); + } + /** * Deletes the Keycard package. * @@ -271,6 +281,7 @@ public class GlobalPlatformCommandSet { deleteNDEFInstance().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND); deleteKeycardInstance().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND); deleteCashInstance().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND); + deleteIdentInstance().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND); deleteKeycardPackage().checkSW(APDUResponse.SW_OK, APDUResponse.SW_REFERENCED_DATA_NOT_FOUND); } @@ -448,4 +459,14 @@ public class GlobalPlatformCommandSet { public APDUResponse installCashApplet() throws IOException { return installCashApplet(new byte[0]); } + + /** + * Installs the Ident applet. + * + * @return the card response + * @throws IOException communication error. + */ + public APDUResponse installIdentApplet() throws IOException { + return installForInstall(Identifiers.PACKAGE_AID, Identifiers.IDENT_AID, Identifiers.IDENT_INSTANCE_AID, new byte[0]); + } }