Add a method for signing the auth token hash.

This commit is contained in:
Lemmo Lavonen 2021-10-19 00:58:53 +03:00
parent 850ab8fc66
commit 62888a7299

View File

@ -53,6 +53,12 @@ public class Comms {
private static final byte[] verifyPIN2 = Hex.decode("0c2000851d871101"); private static final byte[] verifyPIN2 = Hex.decode("0c2000851d871101");
private static final byte[] MSESetEnv = Hex.decode("0c2241A41d871101");
private static final byte[] Env = Hex.decode("8004FF200800840181");
private static final byte[] InternalAuthenticate = Hex.decode("0c8800001d871101");
private static final byte[] IASECCFID = {0x3f, 0x00}; private static final byte[] IASECCFID = {0x3f, 0x00};
private static final byte[] personalDF = {0x50, 0x00}; private static final byte[] personalDF = {0x50, 0x00};
private static final byte[] AWP = {(byte) 0xad, (byte) 0xf1}; private static final byte[] AWP = {(byte) 0xad, (byte) 0xf1};
@ -75,7 +81,7 @@ public class Comms {
idCard.connect(); idCard.connect();
this.idCard = idCard; this.idCard = idCard;
byte[][] keys = PACE(CAN); byte[][] keys = PACE(CAN.getBytes(StandardCharsets.UTF_8));
keyEnc = keys[0]; keyEnc = keys[0];
keyMAC = keys[1]; keyMAC = keys[1];
} }
@ -132,40 +138,51 @@ public class Comms {
* @param CAN the card access number provided by the user * @param CAN the card access number provided by the user
* @return the decrypted nonce * @return the decrypted nonce
*/ */
private byte[] decryptNonce(byte[] encryptedNonce, String CAN) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { private byte[] decryptNonce(byte[] encryptedNonce, byte[] CAN) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
byte[] decryptionKey = createKey(CAN.getBytes(StandardCharsets.UTF_8), (byte) 3); byte[] decryptionKey = createKey(CAN, (byte) 3);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptionKey, "AES"), new IvParameterSpec(new byte[16])); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptionKey, "AES"), new IvParameterSpec(new byte[16]));
return cipher.doFinal(encryptedNonce); return cipher.doFinal(encryptedNonce);
} }
/**
* Communicates with the card and logs the response
*
* @param APDU The command
* @param log Information for logging
* @return The response
*/
private byte[] getResponse(byte[] APDU, String log) throws IOException {
byte[] response = idCard.transceive(APDU);
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
throw new RuntimeException(String.format("%s failed.", log));
}
Log.i(log, Hex.toHexString(response));
return response;
}
/** /**
* Attempts to use the PACE protocol to create a secure channel with an Estonian ID-card * Attempts to use the PACE protocol to create a secure channel with an Estonian ID-card
* *
* @param CAN the card access number * @param CAN the card access number
*/ */
private byte[][] PACE(String CAN) throws IOException, NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException { private byte[][] PACE(byte[] CAN) throws IOException, NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
// select the IAS-ECC application on the chip // select the IAS-ECC application on the chip
byte[] response = idCard.transceive(selectMaster); getResponse(selectMaster, "Select the master application");
Log.i("Select the master application", Hex.toHexString(response));
// initiate PACE // initiate PACE
response = idCard.transceive(MSESetAT); getResponse(MSESetAT, "Set authentication template");
Log.i("Authentication template", Hex.toHexString(response));
// get nonce // get nonce
response = idCard.transceive(GAGetNonce); byte[] response = getResponse(GAGetNonce, "Get nonce");
Log.i("Get nonce", Hex.toHexString(response));
byte[] decryptedNonce = decryptNonce(Arrays.copyOfRange(response, 4, response.length - 2), CAN); byte[] decryptedNonce = decryptNonce(Arrays.copyOfRange(response, 4, response.length - 2), CAN);
// generate an EC keypair and exchange public keys with the chip // generate an EC keypair and exchange public keys with the chip
ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("secp256r1"); ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("secp256r1");
BigInteger privateKey = new BigInteger(255, new SecureRandom()).add(BigInteger.ONE); // should be in [1, spec.getN()-1], but this is good enough for this application BigInteger privateKey = new BigInteger(255, new SecureRandom()).add(BigInteger.ONE); // should be in [1, spec.getN()-1], but this is good enough for this application
ECPoint publicKey = spec.getG().multiply(privateKey).normalize(); ECPoint publicKey = spec.getG().multiply(privateKey).normalize();
byte[] APDU = createAPDU(GAMapNonceIncomplete, publicKey.getEncoded(false), 66); response = getResponse(createAPDU(GAMapNonceIncomplete, publicKey.getEncoded(false), 66), "Map nonce");
response = idCard.transceive(APDU);
Log.i("Map nonce", Hex.toHexString(response));
ECPoint cardPublicKey = spec.getCurve().decodePoint(Arrays.copyOfRange(response, 4, 69)); ECPoint cardPublicKey = spec.getCurve().decodePoint(Arrays.copyOfRange(response, 4, 69));
// calculate the new base point, use it to generate a new keypair, and exchange public keys // calculate the new base point, use it to generate a new keypair, and exchange public keys
@ -173,28 +190,18 @@ public class Comms {
ECPoint mappedECBasePoint = spec.getG().multiply(new BigInteger(1, decryptedNonce)).add(sharedSecret).normalize(); ECPoint mappedECBasePoint = spec.getG().multiply(new BigInteger(1, decryptedNonce)).add(sharedSecret).normalize();
privateKey = new BigInteger(255, new SecureRandom()).add(BigInteger.ONE); privateKey = new BigInteger(255, new SecureRandom()).add(BigInteger.ONE);
publicKey = mappedECBasePoint.multiply(privateKey).normalize(); publicKey = mappedECBasePoint.multiply(privateKey).normalize();
APDU = createAPDU(GAKeyAgreementIncomplete, publicKey.getEncoded(false), 66); response = getResponse(createAPDU(GAKeyAgreementIncomplete, publicKey.getEncoded(false), 66), "Key agreement");
response = idCard.transceive(APDU);
Log.i("Key agreement", Hex.toHexString(response));
cardPublicKey = spec.getCurve().decodePoint(Arrays.copyOfRange(response, 4, 69)); cardPublicKey = spec.getCurve().decodePoint(Arrays.copyOfRange(response, 4, 69));
// generate the session keys and exchange MACs to verify them // generate the session keys and exchange MACs to verify them
sharedSecret = cardPublicKey.multiply(privateKey).normalize(); byte[] secret = cardPublicKey.multiply(privateKey).normalize().getAffineXCoord().getEncoded();
byte[] encodedSecret = sharedSecret.getAffineXCoord().getEncoded(); byte[] keyEnc = createKey(secret, (byte) 1);
byte[] keyEnc = createKey(encodedSecret, (byte) 1); byte[] keyMAC = createKey(secret, (byte) 2);
byte[] keyMAC = createKey(encodedSecret, (byte) 2); byte[] MAC = getMAC(createAPDU(dataForMACIncomplete, cardPublicKey.getEncoded(false), 65), keyMAC);
APDU = createAPDU(dataForMACIncomplete, cardPublicKey.getEncoded(false), 65); response = getResponse(createAPDU(GAMutualAuthenticationIncomplete, MAC, 9), "Mutual authentication");
byte[] MAC = getMAC(APDU, keyMAC);
APDU = createAPDU(GAMutualAuthenticationIncomplete, MAC, 9);
response = idCard.transceive(APDU);
Log.i("Mutual authentication", Hex.toHexString(response));
// if the chip-side verification fails, crash and burn // verify chip's MAC and return session keys
if (response.length == 2) throw new RuntimeException("Invalid CAN."); MAC = getMAC(createAPDU(dataForMACIncomplete, publicKey.getEncoded(false), 65), keyMAC);
// otherwise verify chip's MAC and return session keys
APDU = createAPDU(dataForMACIncomplete, publicKey.getEncoded(false), 65);
MAC = getMAC(APDU, keyMAC);
if (!Hex.toHexString(response, 4, 8).equals(Hex.toHexString(MAC))) { if (!Hex.toHexString(response, 4, 8).equals(Hex.toHexString(MAC))) {
throw new RuntimeException("Could not verify chip's MAC."); // *Should* never happen. throw new RuntimeException("Could not verify chip's MAC."); // *Should* never happen.
} }
@ -211,10 +218,8 @@ public class Comms {
*/ */
private byte[] readFile(byte[] FID, String info) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException { private byte[] readFile(byte[] FID, String info) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
selectFile(FID, info); selectFile(FID, info);
byte[] APDU = createSecureAPDU(new byte[0], readFile); byte[] response = getResponse(new byte[0], readFile, "Read binary");
byte[] response = idCard.transceive(APDU); if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
Log.i("Read binary", Hex.toHexString(response));
if (response[response.length - 2] != -112 || response[response.length - 1] != 0) {
throw new RuntimeException(String.format("Could not read %s", info)); throw new RuntimeException(String.format("Could not read %s", info));
} }
return encryptDecryptData(Arrays.copyOfRange(response, 3, 19), Cipher.DECRYPT_MODE); return encryptDecryptData(Arrays.copyOfRange(response, 3, 19), Cipher.DECRYPT_MODE);
@ -254,15 +259,15 @@ public class Comms {
byte[] macData = new byte[data.length > 0 ? 48 + length : 48]; byte[] macData = new byte[data.length > 0 ? 48 + length : 48];
macData[15] = ssc; // first block contains the ssc macData[15] = ssc; // first block contains the ssc
System.arraycopy(incomplete, 0, macData, 16, 4); // second block has the command System.arraycopy(incomplete, 0, macData, 16, 4); // second block has the command
macData[20] = -128; // elements are terminated by 0x80 and zero-padded to the next block macData[20] = (byte) 0x80; // elements are terminated by 0x80 and zero-padded to the next block
System.arraycopy(incomplete, 5, macData, 32, 3); // third block contains appropriately encapsulated data/Le System.arraycopy(incomplete, 5, macData, 32, 3); // third block contains appropriately encapsulated data/Le
if (data.length > 0) { // if the APDU has data, add padding and encrypt it if (data.length > 0) { // if the APDU has data, add padding and encrypt it
byte[] paddedData = Arrays.copyOf(data, length); byte[] paddedData = Arrays.copyOf(data, length);
paddedData[data.length] = -128; paddedData[data.length] = (byte) 0x80;
encryptedData = encryptDecryptData(paddedData, Cipher.ENCRYPT_MODE); encryptedData = encryptDecryptData(paddedData, Cipher.ENCRYPT_MODE);
System.arraycopy(encryptedData, 0, macData, 35, encryptedData.length); System.arraycopy(encryptedData, 0, macData, 35, encryptedData.length);
} }
macData[35 + encryptedData.length] = -128; macData[35 + encryptedData.length] = (byte) 0x80;
byte[] MAC = getMAC(macData, keyMAC); byte[] MAC = getMAC(macData, keyMAC);
// construct the APDU using the encrypted data and the MAC // construct the APDU using the encrypted data and the MAC
@ -282,10 +287,8 @@ public class Comms {
* *
*/ */
private void selectFile(byte[] FID, String info) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException { private void selectFile(byte[] FID, String info) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
byte[] APDU = createSecureAPDU(FID, selectFile); byte[] response = getResponse(FID, selectFile, String.format("Select %s", info));
byte[] response = idCard.transceive(APDU); if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
Log.i(String.format("Select %s", info), Hex.toHexString(response));
if (response[response.length - 2] != -112 || response[response.length - 1] != 0) {
throw new RuntimeException(String.format("Could not select %s", info)); throw new RuntimeException(String.format("Could not select %s", info));
} }
} }
@ -330,7 +333,7 @@ public class Comms {
* @param PIN user-provided PIN * @param PIN user-provided PIN
* @param oneOrTwo true for PIN1, false for PIN2 * @param oneOrTwo true for PIN1, false for PIN2
*/ */
public void verifyPIN(byte[] PIN, boolean oneOrTwo) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException { private void verifyPIN(byte[] PIN, boolean oneOrTwo) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException {
selectFile(IASECCFID, "the master application"); selectFile(IASECCFID, "the master application");
if (!oneOrTwo) { if (!oneOrTwo) {
@ -338,19 +341,15 @@ public class Comms {
} }
// pad the PIN and use the chip for verification // pad the PIN and use the chip for verification
byte[] paddedPIN1 = Hex.decode("ffffffffffffffffffffffff"); byte[] paddedPIN = Hex.decode("ffffffffffffffffffffffff");
System.arraycopy(PIN, 0, paddedPIN1, 0, PIN.length); System.arraycopy(PIN, 0, paddedPIN, 0, PIN.length);
byte[] APDU = createSecureAPDU(paddedPIN1, oneOrTwo ? verifyPIN1 : verifyPIN2); byte[] response = getResponse(paddedPIN, oneOrTwo ? verifyPIN1 : verifyPIN2, "PIN verification");
byte[] response = idCard.transceive(APDU);
Log.i(String.format("PIN%d verification", oneOrTwo ? 1 : 2), Hex.toHexString(response));
byte sw1 = response[response.length - 2]; if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
byte sw2 = response[response.length - 1]; if (response[response.length - 2] == 0x69 && response[response.length - 1] == (byte) 0x83) {
if (sw1 != -112 || sw2 != 0) {
if (sw1 == 105 && sw2 == -125) {
throw new RuntimeException("Invalid PIN. Authentication method blocked."); throw new RuntimeException("Invalid PIN. Authentication method blocked.");
} else { } else {
throw new RuntimeException(String.format("Invalid PIN1. %d attempt%s left.", sw2 + 64, sw2 == -63 ? "" : "s")); throw new RuntimeException(String.format("Invalid PIN. Attempts left: %d.", response[response.length - 1] + 64));
} }
} }
} }
@ -377,13 +376,8 @@ public class Comms {
// Set the P1/P2 values to incrementally read the certificate // Set the P1/P2 values to incrementally read the certificate
readCert[2] = (byte) (certificate.length / 256); readCert[2] = (byte) (certificate.length / 256);
readCert[3] = (byte) (certificate.length % 256); readCert[3] = (byte) (certificate.length % 256);
byte[] APDU = createSecureAPDU(new byte[0], readCert); byte[] response = getResponse(new byte[0], readCert, "Read the certificate");
byte[] response = idCard.transceive(APDU); if (response[response.length - 2] == 0x6b && response[response.length - 1] == 0x00) {
Log.i("Read the certificate", Hex.toHexString(response));
byte sw1 = response[response.length - 2];
byte sw2 = response[response.length - 1];
if (sw1 == 107 && sw2 == 0) {
throw new RuntimeException("Wrong read parameters."); throw new RuntimeException("Wrong read parameters.");
} }
@ -395,17 +389,50 @@ public class Comms {
certificate = Arrays.copyOf(certificate, certificate.length + indexOfTerminator); certificate = Arrays.copyOf(certificate, certificate.length + indexOfTerminator);
System.arraycopy(decrypted, 0, certificate, certificate.length - indexOfTerminator, indexOfTerminator); System.arraycopy(decrypted, 0, certificate, certificate.length - indexOfTerminator, indexOfTerminator);
if (sw1 == -112 && sw2 == 0) { if (response[response.length - 2] == (byte) 0x90 && response[response.length - 1] == 0x00) {
break; break;
} }
} }
// For debugging, ascertain that the byte array corresponds to a valid certificate
// CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
// X509Certificate x509Certificate = (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(certificate));
// Log.i("Certificate serial number", String.valueOf(x509Certificate.getSerialNumber()));
return certificate; return certificate;
} }
/**
* Signs the authentication token hash
*
* @param PIN1 PIN1
* @param token the token hash to be signed
* @return authentication token hash signature
*/
public byte[] authenticate(String PIN1, byte[] token) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
verifyPIN(PIN1.getBytes(StandardCharsets.UTF_8), true);
selectFile(AWP, "the AWP application");
byte[] response = getResponse(Env, MSESetEnv, "Set environment");
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
throw new RuntimeException("Setting the environment failed.");
}
InternalAuthenticate[4] = (byte) (0x1d + 16 * (token.length / 16));
InternalAuthenticate[6] = (byte) (0x11 + 16 * (token.length / 16));
response = getResponse(token, InternalAuthenticate, "Internal Authenticate");
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
throw new RuntimeException("Signing the token failed.");
}
byte[] signature = encryptDecryptData(Arrays.copyOfRange(response, 3, 115), Cipher.DECRYPT_MODE);
int indexOfTerminator = Hex.toHexString(signature).lastIndexOf("80") / 2;
return Arrays.copyOf(signature, indexOfTerminator);
}
private byte[] getResponse(byte[] data, byte[] command, String log) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
byte[] response = idCard.transceive(createSecureAPDU(data, command));
Log.i(log, Hex.toHexString(response));
return response;
}
} }