From afb6687075c547d862a1d7e7dfc49ef5ff9c78a8 Mon Sep 17 00:00:00 2001 From: GBangad Date: Mon, 13 Jan 2025 09:44:10 +0800 Subject: [PATCH 1/5] Added Handling for mastercard attestation and assertion --- .../java/com/thales/attest/Attestation.java | 26 +- .../java/com/thales/attest/MainActivity.kt | 2 +- .../thales/attest/MastercardAttestation.java | 242 ++++++++++++++++++ app/src/main/java/com/thales/attest/Util.java | 86 +++++++ 4 files changed, 331 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/com/thales/attest/MastercardAttestation.java diff --git a/app/src/main/java/com/thales/attest/Attestation.java b/app/src/main/java/com/thales/attest/Attestation.java index 2c9e5b2..43dbb6a 100644 --- a/app/src/main/java/com/thales/attest/Attestation.java +++ b/app/src/main/java/com/thales/attest/Attestation.java @@ -3,6 +3,8 @@ import static com.thales.attest.Util.bytesToHex; import android.content.Context; +import android.util.Base64; +import android.util.Log; import androidx.annotation.NonNull; import androidx.biometric.BiometricPrompt; @@ -105,30 +107,6 @@ public static byte[] createCredentialPublicKeyCbor(PublicKey rsaPublicKey) throw return cborBytes; } - // Function to construct Attested Credential Data - public static byte[] constructAttestedCredentialData(PublicKey publicKey, byte[] credentialPublicKey) throws Exception { - // Compute the credentialId as the SHA-256 hash of the encoded publicKey - byte[] credentialId = Util.sha256(publicKey.getEncoded()); - - // AAGUID is set to 16 bytes of 0 - byte[] aaguid = new byte[16]; - - // Credential ID length (2 bytes) - short credentialIdLength = (short) credentialId.length; - ByteBuffer credentialIdLengthBuffer = ByteBuffer.allocate(2); - credentialIdLengthBuffer.putShort(credentialIdLength); - byte[] credentialIdLengthBytes = credentialIdLengthBuffer.array(); - - // Construct the Attested Credential Data - ByteBuffer buffer = ByteBuffer.allocate(16 + 2 + credentialId.length + credentialPublicKey.length); - buffer.put(aaguid); // AAGUID (16 bytes) - buffer.put(credentialIdLengthBytes); // Credential ID length (2 bytes) - buffer.put(credentialId); // Credential ID (32 bytes, derived from SHA-256 of the publicKey) - buffer.put(credentialPublicKey); // Credential Public Key (encoded form of the publicKey) - - return buffer.array(); - } - public static byte[] constructAuthenticatorData(Context context, byte[] credentialData) throws Exception { // 1. Retrieve the package name from the context String packageName = context.getPackageName(); diff --git a/app/src/main/java/com/thales/attest/MainActivity.kt b/app/src/main/java/com/thales/attest/MainActivity.kt index d2cd16d..01ff283 100644 --- a/app/src/main/java/com/thales/attest/MainActivity.kt +++ b/app/src/main/java/com/thales/attest/MainActivity.kt @@ -36,7 +36,7 @@ class MainActivity : FragmentActivity() { @Composable fun Greeting(name: String, modifier: Modifier = Modifier, context: FragmentActivity) { Text( - text = "Hello $name! " + Attestation.test(context), + text = "Hello $name! " + MastercardAttestation.test(context), modifier = modifier ) } diff --git a/app/src/main/java/com/thales/attest/MastercardAttestation.java b/app/src/main/java/com/thales/attest/MastercardAttestation.java new file mode 100644 index 0000000..1c13685 --- /dev/null +++ b/app/src/main/java/com/thales/attest/MastercardAttestation.java @@ -0,0 +1,242 @@ +package com.thales.attest; + +import android.content.Context; +import android.security.keystore.KeyProperties; +import android.util.Log; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.cbor.CBORFactory; + +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.cert.Certificate; +import java.security.spec.ECPublicKeySpec; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class MastercardAttestation { + + public static String TAG = "att2_Mastercard"; + + private static String CLIENT_DATA = "{\"appInstanceID\":\"05c666b6-c833-46c2-a4ab-6321fc3cfe8c\",\"timeStamp\":\"2024-11-22T12:50:30Z\"}"; + + + public static void test(Context context) throws Exception { + Util.getAppSigningKey(context); + PublicKey key = (PublicKey) Util.generateECDSAKeyPair(false); + byte[] credentialPublicKeyCbor = createCredentialPublicKeyCbor(key); + Log.d(TAG, "credPubKey: " + Util.bytesToHex(credentialPublicKeyCbor) ); + + Util.logLongString(TAG,"credPubKey Hex( "+Util.bytesToHex(credentialPublicKeyCbor)+" )"); + + byte[] atData = Util.constructAttestedCredentialData(key, credentialPublicKeyCbor); + Log.d(TAG, "atData: " + Util.bytesToHex(atData) ); + + Util.logLongString(TAG,"atData Hex( "+Util.bytesToHex(atData)+" )"); + + byte[] authData = constructAuthenticatorData(context, atData); + Log.d(TAG, "authData: " + Util.bytesToHex(authData) ); + + Util.logLongString(TAG,"authData Hex( "+Util.bytesToHex(authData)+" )"); + + byte[] clientDataHash = Util.sha256(CLIENT_DATA.getBytes(StandardCharsets.UTF_8)); + byte[] attest = constructWebAuthnCbor(Util.KEY_ALIAS_MASTERCARD, authData, clientDataHash); + String attestStr = Util.bytesToHex(attest); + Util.logLongString("attest", attestStr); + + Util.logLongString(TAG, "attest Hex( "+attestStr+" )"); + + byte[] assertion = constructAssertionData(context); + byte[] assertionArray = constructAssertionWebAuthnCbor(Util.KEY_ALIAS_MASTERCARD,assertion,clientDataHash); + String assertionStr = Util.bytesToHex(assertionArray); + Util.logLongString("assertionObject", assertionStr); + + } + + public static byte[] createCredentialPublicKeyCbor(PublicKey ecdsaPublicKey) throws Exception { + // Extract ECDSA public key components: modulus (n) and exponent (e) + ECPublicKeySpec ecdsaKeySpec = KeyFactory.getInstance(KeyProperties.KEY_ALGORITHM_EC).getKeySpec(ecdsaPublicKey, ECPublicKeySpec.class); + BigInteger modulus = ecdsaKeySpec.getW().getAffineX(); + BigInteger exponent = ecdsaKeySpec.getW().getAffineY(); + + // Convert modulus and exponent to byte arrays + byte[] nBytes = modulus.toByteArray(); + byte[] eBytes = exponent.toByteArray(); + + // Parse JSON into a Map + Map data = new LinkedHashMap<>(); + data.put(1, 2); + data.put(3, -7); + data.put(-1, 1); + data.put(-2, nBytes); + data.put(-3, eBytes); + + // Create an ObjectMapper for CBOR + ObjectMapper cborMapper = new ObjectMapper(new CBORFactory()); + + // Convert the updated Map to CBOR + byte[] cborBytes = cborMapper.writeValueAsBytes(data); + + return cborBytes; + } + + public static byte[] constructAssertionData(Context context) { + + byte[] rpIdHash; + byte flag = 0x5; + byte[] signCount; + signCount = ByteBuffer.allocate(4).putInt(1).array(); + String packageName = context.getPackageName(); + try { + rpIdHash = Util.sha256(packageName.getBytes()); + } catch (Exception e) { + throw new RuntimeException(e); + } + + // RpIdHash 16 bytes + flags 1 byte + SignCount 4 bytes + ByteBuffer byteBuffer = ByteBuffer.allocate(rpIdHash.length + 1 + 4); + byteBuffer.put(rpIdHash); + byteBuffer.put(flag); + byteBuffer.put(signCount); + + return byteBuffer.array(); + + } + + + // Function to construct Attested Credential Data + + + public static byte[] constructAuthenticatorData(Context context, byte[] credentialData) throws Exception { + // 1. Retrieve the package name from the context + String packageName = context.getPackageName(); + + // 2. Compute rpIdHash (SHA-256 of the package name) + byte[] rpIdHash = Util.sha256(packageName.getBytes()); + + // 3. Flags (set to 0x45) + byte flags = 0x45; + + // 4. Sign count (set to 0x00000000) + byte[] signCount = ByteBuffer.allocate(4).putInt(0).array(); + + // 5. Concatenate all parts to form authenticatorData + ByteBuffer buffer = ByteBuffer.allocate( + rpIdHash.length + 1 + signCount.length + credentialData.length + ); + buffer.put(rpIdHash); // rpIdHash (32 bytes) + buffer.put(flags); // Flags (1 byte) + buffer.put(signCount); // Sign Count (4 bytes) + buffer.put(credentialData); // Attested Credential Data (variable length) + + return buffer.array(); + } + + + public static byte[] constructAssertionWebAuthnCbor(String alias, byte[] authenticatorData, byte[] clientDataHash) throws Exception { + // Load the Android Keystore + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + + // Retrieve the private key and public key using the alias + PrivateKey privateKey = (PrivateKey) Util.generateECDSAKeyPair(true); + + // Retrieve the certificate chain + Certificate[] certificateChain = keyStore.getCertificateChain(alias); + if (certificateChain == null || certificateChain.length == 0) { + throw new IllegalStateException("Certificate chain is empty for alias: " + alias); + } + + // Perform ECDSA signature using SHA-256 with PSS padding + Signature signature = Signature.getInstance("SHA256withECDSA"); + signature.initSign(privateKey); + signature.update(clientDataHash); + byte[] signedData = signature.sign(); + + // CBOR factory and mapper + CBORFactory cborFactory = new CBORFactory(); + ObjectMapper cborMapper = new ObjectMapper(cborFactory); + + + // Create WebAuthn object + Map webAuthnObject = new LinkedHashMap<>(); + + webAuthnObject.put("signature", signedData); + webAuthnObject.put("authenticatorData", authenticatorData); + + // Serialize to CBOR + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (JsonGenerator generator = cborFactory.createGenerator(outputStream)) { + cborMapper.writeValue(generator, webAuthnObject); + } + + return outputStream.toByteArray(); + } + + public static byte[] constructWebAuthnCbor(String alias, byte[] authenticatorData, byte[] clientDataHash) throws Exception { + // Load the Android Keystore + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + + // Retrieve the private key and public key using the alias + PrivateKey privateKey = (PrivateKey) Util.generateECDSAKeyPair(true); + + // Retrieve the certificate chain + Certificate[] certificateChain = keyStore.getCertificateChain(alias); + if (certificateChain == null || certificateChain.length == 0) { + throw new IllegalStateException("Certificate chain is empty for alias: " + alias); + } + + // Perform ECDSA signature using SHA-256 with PSS padding + Signature signature = Signature.getInstance("SHA256withECDSA"); + signature.initSign(privateKey); + signature.update(authenticatorData); + signature.update(clientDataHash); + byte[] signedData = signature.sign(); + + Util.logLongString(TAG, "Signed Data Hex( "+Util.bytesToHex(signedData)+" )"); + // Convert x5c (certificate chain) to a list of DER-encoded certificates + List x5cList = new ArrayList<>(); + for (Certificate cert : certificateChain) { + String certificate = Util.prepareDeviceCertificate(cert.getEncoded()); + Util.logLongString("x5c:",certificate); + x5cList.add(cert.getEncoded()); + } + + // CBOR factory and mapper + CBORFactory cborFactory = new CBORFactory(); + ObjectMapper cborMapper = new ObjectMapper(cborFactory); + + // Create attestation statement + Map attStmt = new LinkedHashMap<>(); + attStmt.put("alg", -7); // ECDSA PSS (ECDSA-PSS using SHA-256, alg value -37 in COSE) + attStmt.put("sig", signedData); + attStmt.put("x5c", x5cList); + + // Create WebAuthn object + Map webAuthnObject = new LinkedHashMap<>(); + webAuthnObject.put("fmt", "android-key"); + webAuthnObject.put("attStmt", attStmt); + webAuthnObject.put("authData", authenticatorData); + + // Serialize to CBOR + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (JsonGenerator generator = cborFactory.createGenerator(outputStream)) { + cborMapper.writeValue(generator, webAuthnObject); + } + + return outputStream.toByteArray(); + } + + +} diff --git a/app/src/main/java/com/thales/attest/Util.java b/app/src/main/java/com/thales/attest/Util.java index 21f3457..9a0b9c1 100644 --- a/app/src/main/java/com/thales/attest/Util.java +++ b/app/src/main/java/com/thales/attest/Util.java @@ -1,5 +1,8 @@ package com.thales.attest; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; import android.util.Base64; @@ -7,6 +10,7 @@ import java.io.IOException; import java.math.BigInteger; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.Key; import java.security.KeyPair; @@ -15,7 +19,10 @@ import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; import java.security.cert.CertificateException; +import java.security.spec.ECGenParameterSpec; import java.security.spec.RSAKeyGenParameterSpec; import java.util.Arrays; @@ -24,6 +31,8 @@ public class Util { public static final String ANDROID_KEYSTORE = "AndroidKeyStore"; public static final String KEY_ALIAS = "alias2"; + public static final String KEY_ALIAS_MASTERCARD = "alias3"; + private static String CLIENT_DATA = "{\"appInstanceID\":\"05c666b6-c833-46c2-a4ab-6321fc3cfe8c\",\"timeStamp\":\"2024-11-22T12:50:30Z\"}"; public static byte[] toUnsignedByteArray(BigInteger bigInt) { @@ -68,6 +77,22 @@ public static boolean checkKeyExists(String alias) throws CertificateException, return keyStore.containsAlias(alias); } + public static String getAppSigningKey(Context context) { + try { + // Get the PackageManager and package info + PackageManager packageManager = context.getPackageManager(); + String packageName = context.getPackageName(); + PackageInfo packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES); + + // Extract the signing certificate + return bytesToHex(sha256(packageInfo.signatures[0].toByteArray())); + + } catch (Exception e) { + Log.e("AppSignatureUtils", "Error retrieving app signing key", e); + return null; + } + } + public static Key getKey(boolean isPrivate) throws Exception { if (!checkKeyExists(KEY_ALIAS)) { KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEYSTORE); @@ -91,9 +116,70 @@ public static Key getKey(boolean isPrivate) throws Exception { return keyStore.getCertificate(KEY_ALIAS).getPublicKey(); } } + + public static Key generateECDSAKeyPair(boolean isPrivate) throws Exception { + if (!checkKeyExists(KEY_ALIAS_MASTERCARD)) { + // Create the KeyPairGenerator instance for ECDSA using the Keystore provider + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, ANDROID_KEYSTORE); + + // Define the KeyGenParameterSpec for the key pair + KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(KEY_ALIAS_MASTERCARD, KeyProperties.PURPOSE_SIGN) + .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1")) + .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512) + // Only permit the private key to be used if the user authenticated + .setAttestationChallenge(sha256(CLIENT_DATA.getBytes(StandardCharsets.UTF_8))) + .build(); + + // Initialize the key generator with the specified parameters + keyPairGenerator.initialize(keyGenParameterSpec); + + // Generate the key pair + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + + // Extract public and private keys + PublicKey publicKey = keyPair.getPublic(); + + // Display the public key in Base64 format for example + String publicKeyBase64 = Base64.encodeToString(publicKey.getEncoded(), Base64.DEFAULT); + Log.d("Util", "Public Key (Base64): " + publicKeyBase64); + return publicKey; + } else { + KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE); + keyStore.load(null); + if (isPrivate) { + return keyStore.getKey(KEY_ALIAS_MASTERCARD, null); + } + return keyStore.getCertificate(KEY_ALIAS_MASTERCARD).getPublicKey(); + } + } + public static String prepareDeviceCertificate(byte[] devicePublicKey) { return "-----BEGIN CERTIFICATE-----\n" + Base64.encodeToString(devicePublicKey,Base64.DEFAULT) + "\n-----END CERTIFICATE-----"; } + + // Function to construct Attested Credential Data + public static byte[] constructAttestedCredentialData(PublicKey publicKey, byte[] credentialPublicKey) throws Exception { + // Compute the credentialId as the SHA-256 hash of the encoded publicKey + byte[] credentialId = Util.sha256(publicKey.getEncoded()); + + // AAGUID is set to 16 bytes of 0 + byte[] aaguid = new byte[16]; + + // Credential ID length (2 bytes) + short credentialIdLength = (short) credentialId.length; + ByteBuffer credentialIdLengthBuffer = ByteBuffer.allocate(2); + credentialIdLengthBuffer.putShort(credentialIdLength); + byte[] credentialIdLengthBytes = credentialIdLengthBuffer.array(); + + // Construct the Attested Credential Data + ByteBuffer buffer = ByteBuffer.allocate(16 + 2 + credentialId.length + credentialPublicKey.length); + buffer.put(aaguid); // AAGUID (16 bytes) + buffer.put(credentialIdLengthBytes); // Credential ID length (2 bytes) + buffer.put(credentialId); // Credential ID (32 bytes, derived from SHA-256 of the publicKey) + buffer.put(credentialPublicKey); // Credential Public Key (encoded form of the publicKey) + + return buffer.array(); + } } From cc602dc44c34683a2dda89a300f0a1cbef2de280 Mon Sep 17 00:00:00 2001 From: GBangad Date: Tue, 14 Jan 2025 16:33:14 +0800 Subject: [PATCH 2/5] Updated master card attesation as per VISA latest --- .../thales/attest/MastercardAttestation.java | 99 ++++++++++++++++--- 1 file changed, 84 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/thales/attest/MastercardAttestation.java b/app/src/main/java/com/thales/attest/MastercardAttestation.java index 1c13685..25c330f 100644 --- a/app/src/main/java/com/thales/attest/MastercardAttestation.java +++ b/app/src/main/java/com/thales/attest/MastercardAttestation.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.cbor.CBORFactory; +import com.fasterxml.jackson.dataformat.cbor.CBORGenerator; import java.io.ByteArrayOutputStream; import java.math.BigInteger; @@ -81,11 +82,27 @@ public static byte[] createCredentialPublicKeyCbor(PublicKey ecdsaPublicKey) thr data.put(-2, nBytes); data.put(-3, eBytes); - // Create an ObjectMapper for CBOR - ObjectMapper cborMapper = new ObjectMapper(new CBORFactory()); + CBORFactory cborFactory = new CBORFactory(); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + // Create CBOR generator with fixed size start + try (CBORGenerator cborGenerator = cborFactory.createGenerator(byteArrayOutputStream)) { + // Write start object with size (fixed length) + cborGenerator.writeStartObject(data.size()); // Pass the fixed length (map size) + + // Write key-value pairs to the CBOR object + for (Map.Entry entry : data.entrySet()) { + cborGenerator.writeFieldId(entry.getKey()); + cborGenerator.writeObject(entry.getValue()); + } + + // End the object + cborGenerator.writeEndObject(); + } // Convert the updated Map to CBOR - byte[] cborBytes = cborMapper.writeValueAsBytes(data); + byte[] cborBytes = byteArrayOutputStream.toByteArray(); + return cborBytes; } @@ -165,7 +182,6 @@ public static byte[] constructAssertionWebAuthnCbor(String alias, byte[] authent // CBOR factory and mapper CBORFactory cborFactory = new CBORFactory(); - ObjectMapper cborMapper = new ObjectMapper(cborFactory); // Create WebAuthn object @@ -174,13 +190,30 @@ public static byte[] constructAssertionWebAuthnCbor(String alias, byte[] authent webAuthnObject.put("signature", signedData); webAuthnObject.put("authenticatorData", authenticatorData); - // Serialize to CBOR - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - try (JsonGenerator generator = cborFactory.createGenerator(outputStream)) { - cborMapper.writeValue(generator, webAuthnObject); + // Create CBOR factory and object mapper + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + // Create CBOR generator + try (CBORGenerator cborGenerator = cborFactory.createGenerator(byteArrayOutputStream)) { + // Write start object without specifying size + cborGenerator.writeStartObject(webAuthnObject.size()); + + // Write simple key-value pairs + for (Map.Entry entry : webAuthnObject.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + cborGenerator.writeFieldName(key); + // Handle normal key-value pairs + cborGenerator.writeObject(value); + } + + // End the main object + cborGenerator.writeEndObject(); } - return outputStream.toByteArray(); + // Output the CBOR encoded byte array + return byteArrayOutputStream.toByteArray(); } public static byte[] constructWebAuthnCbor(String alias, byte[] authenticatorData, byte[] clientDataHash) throws Exception { @@ -215,7 +248,6 @@ public static byte[] constructWebAuthnCbor(String alias, byte[] authenticatorDat // CBOR factory and mapper CBORFactory cborFactory = new CBORFactory(); - ObjectMapper cborMapper = new ObjectMapper(cborFactory); // Create attestation statement Map attStmt = new LinkedHashMap<>(); @@ -229,13 +261,50 @@ public static byte[] constructWebAuthnCbor(String alias, byte[] authenticatorDat webAuthnObject.put("attStmt", attStmt); webAuthnObject.put("authData", authenticatorData); - // Serialize to CBOR - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - try (JsonGenerator generator = cborFactory.createGenerator(outputStream)) { - cborMapper.writeValue(generator, webAuthnObject); + // Create CBOR factory and object mapper + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + // Create CBOR generator + try (CBORGenerator cborGenerator = cborFactory.createGenerator(byteArrayOutputStream)) { + // Write start object without specifying size + cborGenerator.writeStartObject(webAuthnObject.size()); + + // Write simple key-value pairs + for (Map.Entry entry : webAuthnObject.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + cborGenerator.writeFieldName(key); + if (value instanceof Map) { + cborGenerator.writeStartObject(attStmt.size()); + for (Map.Entry subEntry : ((Map) value).entrySet()) { + cborGenerator.writeFieldName(subEntry.getKey()); + Object subValue = subEntry.getValue(); + if (subValue instanceof List) { + List subValueList = (List) subValue; + // Handle nested array + cborGenerator.writeStartArray(new ArrayList(), subValueList.size()); + for (byte[] x5cVal : subValueList) { + cborGenerator.writeBinary(x5cVal); + } + cborGenerator.writeEndArray(); + } else { + cborGenerator.writeObject(subValue); + } + } + cborGenerator.writeEndObject(); + } else { + // Handle normal key-value pairs + cborGenerator.writeObject(value); + } + } + + // End the main object + cborGenerator.writeEndObject(); } - return outputStream.toByteArray(); + // Output the CBOR encoded byte array + return byteArrayOutputStream.toByteArray(); } From 8fd3b9840a5d30af6263f7b3f8234d5ad0f3803d Mon Sep 17 00:00:00 2001 From: GBangad Date: Tue, 18 Feb 2025 16:44:41 +0800 Subject: [PATCH 3/5] Updated authenticator data in assertion signature --- .../main/java/com/thales/attest/MastercardAttestation.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/thales/attest/MastercardAttestation.java b/app/src/main/java/com/thales/attest/MastercardAttestation.java index 25c330f..e577e3f 100644 --- a/app/src/main/java/com/thales/attest/MastercardAttestation.java +++ b/app/src/main/java/com/thales/attest/MastercardAttestation.java @@ -4,8 +4,6 @@ import android.security.keystore.KeyProperties; import android.util.Log; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.cbor.CBORFactory; import com.fasterxml.jackson.dataformat.cbor.CBORGenerator; @@ -29,7 +27,7 @@ public class MastercardAttestation { public static String TAG = "att2_Mastercard"; - private static String CLIENT_DATA = "{\"appInstanceID\":\"05c666b6-c833-46c2-a4ab-6321fc3cfe8c\",\"timeStamp\":\"2024-11-22T12:50:30Z\"}"; + private static final String CLIENT_DATA = "{\"appInstanceID\":\"05c666b6-c833-46c2-a4ab-6321fc3cfe8c\",\"timeStamp\":\"2024-11-22T12:50:30Z\"}"; public static void test(Context context) throws Exception { @@ -177,6 +175,7 @@ public static byte[] constructAssertionWebAuthnCbor(String alias, byte[] authent // Perform ECDSA signature using SHA-256 with PSS padding Signature signature = Signature.getInstance("SHA256withECDSA"); signature.initSign(privateKey); + signature.update(authenticatorData); signature.update(clientDataHash); byte[] signedData = signature.sign(); From 77fb507564d9f5ad555e8c3346a9616a72a4b4d6 Mon Sep 17 00:00:00 2001 From: GBangad Date: Tue, 15 Apr 2025 11:02:35 +0800 Subject: [PATCH 4/5] fixed compilation issue after rebase --- app/src/main/java/com/thales/attest/Attestation.java | 2 +- gradle.properties | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/com/thales/attest/Attestation.java b/app/src/main/java/com/thales/attest/Attestation.java index 43dbb6a..a6ae3a7 100644 --- a/app/src/main/java/com/thales/attest/Attestation.java +++ b/app/src/main/java/com/thales/attest/Attestation.java @@ -49,7 +49,7 @@ public static void test(FragmentActivity context) throws Exception { byte[] credentialPublicKeyCbor = createCredentialPublicKeyCbor(key); Util.logString(TAG, "credPubKey: " + bytesToHex(credentialPublicKeyCbor) ); - byte[] atData = constructAttestedCredentialData(key, credentialPublicKeyCbor); + byte[] atData = Util.constructAttestedCredentialData(key, credentialPublicKeyCbor); Util.logString(TAG, "atData: " + bytesToHex(atData) ); authData = constructAuthenticatorData(context, atData); diff --git a/gradle.properties b/gradle.properties index 86967bc..132244e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,5 +21,3 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true - -org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk17/Contents/Home From ae3bd31d862cd6aa60c5797575c6ebb409ca99fc Mon Sep 17 00:00:00 2001 From: GBangad Date: Tue, 15 Apr 2025 11:04:26 +0800 Subject: [PATCH 5/5] additional fixes for crypto object visa and mastercard --- .../java/com/thales/attest/Attestation.java | 25 ++-- .../java/com/thales/attest/MainActivity.kt | 4 +- .../thales/attest/MastercardAttestation.java | 113 ++++++++++++++---- app/src/main/java/com/thales/attest/Util.java | 12 +- 4 files changed, 116 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/thales/attest/Attestation.java b/app/src/main/java/com/thales/attest/Attestation.java index a6ae3a7..7154481 100644 --- a/app/src/main/java/com/thales/attest/Attestation.java +++ b/app/src/main/java/com/thales/attest/Attestation.java @@ -3,8 +3,6 @@ import static com.thales.attest.Util.bytesToHex; import android.content.Context; -import android.util.Base64; -import android.util.Log; import androidx.annotation.NonNull; import androidx.biometric.BiometricPrompt; @@ -18,10 +16,8 @@ import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.KeyStore; -import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Signature; @@ -36,15 +32,16 @@ public class Attestation { public static String TAG = "att2"; - private static String CLIENT_DATA = "{\"appInstanceID\":\"05c666b6-c833-46c2-a4ab-6321fc3cfe8c\",\"timeStamp\":\"2024-11-22T12:50:30Z\"}"; + public static final String CLIENT_DATA = "{\"appInstanceID\":\"05c666b6c83346c2a4ab6322\",\"timeStamp\":\"2024-11-22T12:50:30Z\"}"; private static byte[] authData; private static byte[] clientDataHash; - private static Signature signature; - public static void test(FragmentActivity context) throws Exception { + System.out.println("VISA----PackageName---"+Util.bytesToHex(context.getPackageName().getBytes())); + + System.out.println("VISA----Challenge---"+Util.bytesToHex(Util.sha256(CLIENT_DATA.getBytes()))); PublicKey key = (PublicKey) Util.getKey(false); byte[] credentialPublicKeyCbor = createCredentialPublicKeyCbor(key); Util.logString(TAG, "credPubKey: " + bytesToHex(credentialPublicKeyCbor) ); @@ -59,11 +56,6 @@ public static void test(FragmentActivity context) throws Exception { Util.logString(TAG, "clientDataHash: " + bytesToHex(clientDataHash)); authenticateAndSign(context); - -// constructWebAuthnCbor(authData, clientDataHash); -// byte[] attest = constructWebAuthnCbor(Util.KEY_ALIAS, authData, clientDataHash); -// String attestStr = bytesToHex(attest); -// Util.logLongString("attest", attestStr); } public static byte[] createCredentialPublicKeyCbor(PublicKey rsaPublicKey) throws Exception { @@ -134,6 +126,15 @@ public static byte[] constructAuthenticatorData(Context context, byte[] credenti public static void authenticateAndSign(FragmentActivity context) { Executor executor = ContextCompat.getMainExecutor(context); + Signature signature; + try { + signature = Signature.getInstance("SHA256withRSA/PSS"); + signature.initSign((PrivateKey) Util.getKey(true)); + } catch (Exception e) { + throw new RuntimeException(e); + } + + try { signature = Signature.getInstance("SHA256withRSA/PSS"); signature.initSign((PrivateKey) Util.getKey(true)); diff --git a/app/src/main/java/com/thales/attest/MainActivity.kt b/app/src/main/java/com/thales/attest/MainActivity.kt index 01ff283..098d75e 100644 --- a/app/src/main/java/com/thales/attest/MainActivity.kt +++ b/app/src/main/java/com/thales/attest/MainActivity.kt @@ -30,13 +30,15 @@ class MainActivity : FragmentActivity() { } } } + Attestation.test(this@MainActivity) } + } @Composable fun Greeting(name: String, modifier: Modifier = Modifier, context: FragmentActivity) { Text( - text = "Hello $name! " + MastercardAttestation.test(context), + text = "Hello $name! ", modifier = modifier ) } diff --git a/app/src/main/java/com/thales/attest/MastercardAttestation.java b/app/src/main/java/com/thales/attest/MastercardAttestation.java index e577e3f..69f2e37 100644 --- a/app/src/main/java/com/thales/attest/MastercardAttestation.java +++ b/app/src/main/java/com/thales/attest/MastercardAttestation.java @@ -4,6 +4,11 @@ import android.security.keystore.KeyProperties; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.biometric.BiometricPrompt; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.FragmentActivity; + import com.fasterxml.jackson.dataformat.cbor.CBORFactory; import com.fasterxml.jackson.dataformat.cbor.CBORGenerator; @@ -22,15 +27,20 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.Executor; public class MastercardAttestation { public static String TAG = "att2_Mastercard"; - private static final String CLIENT_DATA = "{\"appInstanceID\":\"05c666b6-c833-46c2-a4ab-6321fc3cfe8c\",\"timeStamp\":\"2024-11-22T12:50:30Z\"}"; + private static byte[] authData; + + private static byte[] clientDataHash; + public static final String CLIENT_DATA = "{\"appInstanceID\":\"05c666b6-c833-46c2-a4ab-6321fc3cfe8c\",\"timeStamp\":\"2024-11-22T12:50:30Z\"}"; - public static void test(Context context) throws Exception { + + public static void test(FragmentActivity context) throws Exception { Util.getAppSigningKey(context); PublicKey key = (PublicKey) Util.generateECDSAKeyPair(false); byte[] credentialPublicKeyCbor = createCredentialPublicKeyCbor(key); @@ -43,25 +53,89 @@ public static void test(Context context) throws Exception { Util.logLongString(TAG,"atData Hex( "+Util.bytesToHex(atData)+" )"); - byte[] authData = constructAuthenticatorData(context, atData); + authData = constructAuthenticatorData(context, atData); Log.d(TAG, "authData: " + Util.bytesToHex(authData) ); Util.logLongString(TAG,"authData Hex( "+Util.bytesToHex(authData)+" )"); - byte[] clientDataHash = Util.sha256(CLIENT_DATA.getBytes(StandardCharsets.UTF_8)); - byte[] attest = constructWebAuthnCbor(Util.KEY_ALIAS_MASTERCARD, authData, clientDataHash); - String attestStr = Util.bytesToHex(attest); - Util.logLongString("attest", attestStr); + clientDataHash = Util.sha256(CLIENT_DATA.getBytes(StandardCharsets.UTF_8)); + Util.logLongString(TAG,"ClientData Hex( "+Util.bytesToHex(clientDataHash)+" )"); + authenticateAndSign(context,true); +// byte[] assertion = constructAssertionData(context); +// byte[] assertionArray = constructAssertionWebAuthnCbor(Util.KEY_ALIAS_MASTERCARD,assertion,clientDataHash); +// String assertionStr = Util.bytesToHex(assertionArray); +// Util.logLongString("assertionObject", assertionStr); - Util.logLongString(TAG, "attest Hex( "+attestStr+" )"); + } + + public static void authenticateAndSign(FragmentActivity context, boolean isAttestation) { + Executor executor = ContextCompat.getMainExecutor(context); + Signature signature; + try { + signature = Signature.getInstance("SHA256withECDSA"); + signature.initSign((PrivateKey) Util.generateECDSAKeyPair(true)); + } catch (Exception e) { + throw new RuntimeException(e); + } - byte[] assertion = constructAssertionData(context); - byte[] assertionArray = constructAssertionWebAuthnCbor(Util.KEY_ALIAS_MASTERCARD,assertion,clientDataHash); - String assertionStr = Util.bytesToHex(assertionArray); - Util.logLongString("assertionObject", assertionStr); + // Attach the Signature to a CryptoObject + BiometricPrompt.CryptoObject cryptoObject = new BiometricPrompt.CryptoObject(signature); + + // Create the BiometricPrompt + BiometricPrompt biometricPrompt = new BiometricPrompt( + context, + executor, + new BiometricPrompt.AuthenticationCallback() { + @Override + public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { + if(isAttestation) { + try { + // Perform attestation after authentication + byte[] attest = constructWebAuthnCbor(result); + String attestStr = Util.bytesToHex(attest); + Util.logLongString("attest", attestStr); + } catch (Exception e) { + e.printStackTrace(); + } + authenticateAndSign(context,false); + } else { + try { + // Perform assertion after authentication + byte[] assertion = constructAssertionData(context); + byte[] assertionArray = constructAssertionWebAuthnCbor(Util.KEY_ALIAS_MASTERCARD, assertion, clientDataHash); + String assertionStr = Util.bytesToHex(assertionArray); + Util.logLongString("assertionObject", assertionStr); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + @Override + public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { + System.err.println("Authentication error: " + errString); + } + + @Override + public void onAuthenticationFailed() { + System.err.println("Authentication failed."); + } + }); + + // Create the PromptInfo + BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder() + .setTitle("Biometric Authentication Required") + .setSubtitle("Authenticate to use your private key") + .setNegativeButtonText("Cancel") // You can add a fallback button here + .build(); + + // Start the authentication process + biometricPrompt.authenticate(promptInfo, cryptoObject); } + + public static byte[] createCredentialPublicKeyCbor(PublicKey ecdsaPublicKey) throws Exception { // Extract ECDSA public key components: modulus (n) and exponent (e) ECPublicKeySpec ecdsaKeySpec = KeyFactory.getInstance(KeyProperties.KEY_ALGORITHM_EC).getKeySpec(ecdsaPublicKey, ECPublicKeySpec.class); @@ -215,7 +289,7 @@ public static byte[] constructAssertionWebAuthnCbor(String alias, byte[] authent return byteArrayOutputStream.toByteArray(); } - public static byte[] constructWebAuthnCbor(String alias, byte[] authenticatorData, byte[] clientDataHash) throws Exception { + public static byte[] constructWebAuthnCbor(BiometricPrompt.AuthenticationResult result) throws Exception { // Load the Android Keystore KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); @@ -224,15 +298,14 @@ public static byte[] constructWebAuthnCbor(String alias, byte[] authenticatorDat PrivateKey privateKey = (PrivateKey) Util.generateECDSAKeyPair(true); // Retrieve the certificate chain - Certificate[] certificateChain = keyStore.getCertificateChain(alias); + Certificate[] certificateChain = keyStore.getCertificateChain(Util.KEY_ALIAS_MASTERCARD); if (certificateChain == null || certificateChain.length == 0) { - throw new IllegalStateException("Certificate chain is empty for alias: " + alias); + throw new IllegalStateException("Certificate chain is empty for alias: " + Util.KEY_ALIAS_MASTERCARD); } // Perform ECDSA signature using SHA-256 with PSS padding - Signature signature = Signature.getInstance("SHA256withECDSA"); - signature.initSign(privateKey); - signature.update(authenticatorData); + Signature signature = result.getCryptoObject().getSignature(); + signature.update(authData); signature.update(clientDataHash); byte[] signedData = signature.sign(); @@ -241,7 +314,7 @@ public static byte[] constructWebAuthnCbor(String alias, byte[] authenticatorDat List x5cList = new ArrayList<>(); for (Certificate cert : certificateChain) { String certificate = Util.prepareDeviceCertificate(cert.getEncoded()); - Util.logLongString("x5c:",certificate); + System.out.println(certificate); x5cList.add(cert.getEncoded()); } @@ -258,7 +331,7 @@ public static byte[] constructWebAuthnCbor(String alias, byte[] authenticatorDat Map webAuthnObject = new LinkedHashMap<>(); webAuthnObject.put("fmt", "android-key"); webAuthnObject.put("attStmt", attStmt); - webAuthnObject.put("authData", authenticatorData); + webAuthnObject.put("authData", authData); // Create CBOR factory and object mapper ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); diff --git a/app/src/main/java/com/thales/attest/Util.java b/app/src/main/java/com/thales/attest/Util.java index 9a0b9c1..4fc860e 100644 --- a/app/src/main/java/com/thales/attest/Util.java +++ b/app/src/main/java/com/thales/attest/Util.java @@ -1,5 +1,7 @@ package com.thales.attest; +import static com.thales.attest.Attestation.TAG; + import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -19,7 +21,6 @@ import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.CertificateException; import java.security.spec.ECGenParameterSpec; @@ -33,8 +34,6 @@ public class Util { public static final String KEY_ALIAS_MASTERCARD = "alias3"; - private static String CLIENT_DATA = "{\"appInstanceID\":\"05c666b6-c833-46c2-a4ab-6321fc3cfe8c\",\"timeStamp\":\"2024-11-22T12:50:30Z\"}"; - public static byte[] toUnsignedByteArray(BigInteger bigInt) { byte[] byteArray = bigInt.toByteArray(); if (byteArray[0] == 0x00 && byteArray.length > 1) { @@ -100,7 +99,8 @@ public static Key getKey(boolean isPrivate) throws Exception { .setDigests(KeyProperties.DIGEST_SHA256) .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PSS) .setAlgorithmParameterSpec(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4)) - .setAttestationChallenge(sha256(CLIENT_DATA.getBytes(StandardCharsets.UTF_8))) + .setAttestationChallenge(sha256(Attestation.CLIENT_DATA.getBytes(StandardCharsets.UTF_8))) + .setUserAuthenticationValidityDurationSeconds(0) .setUserAuthenticationRequired(true) ; @@ -127,7 +127,7 @@ public static Key generateECDSAKeyPair(boolean isPrivate) throws Exception { .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1")) .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512) // Only permit the private key to be used if the user authenticated - .setAttestationChallenge(sha256(CLIENT_DATA.getBytes(StandardCharsets.UTF_8))) + .setAttestationChallenge(sha256(MastercardAttestation.CLIENT_DATA.getBytes(StandardCharsets.UTF_8))) .build(); // Initialize the key generator with the specified parameters @@ -164,6 +164,8 @@ public static byte[] constructAttestedCredentialData(PublicKey publicKey, byte[] // Compute the credentialId as the SHA-256 hash of the encoded publicKey byte[] credentialId = Util.sha256(publicKey.getEncoded()); + Util.logString(TAG, "credentialId: " + bytesToHex(credentialId) ); + // AAGUID is set to 16 bytes of 0 byte[] aaguid = new byte[16];