diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-04-04 20:26:58 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-04-04 20:26:58 +0000 |
commit | 0571ed03e9c95c42b8f922934cf1e873b32bdd93 (patch) | |
tree | 7e9d8c10acdcdb6b1e01426d609c4db191ed21ec | |
parent | 357c1efb44968cdcb78785bc79cf7795fd47dc33 (diff) | |
parent | ab31729dff8b1a44f193c27d2411ffcd2b1c56bb (diff) | |
download | android-key-attestation-android14-tests-release.tar.gz |
Snap for 11672127 from ab31729dff8b1a44f193c27d2411ffcd2b1c56bb to android14-tests-releaseandroid-vts-14.0_r4android-cts-14.0_r4android14-tests-release
Change-Id: I78eb63183df955bcb443a7fc46ede588d2bd9a7d
-rw-r--r-- | .github/workflows/bazel.yml | 35 | ||||
-rw-r--r-- | .github/workflows/gradle.yml | 31 | ||||
-rw-r--r-- | Android.bp | 4 | ||||
-rw-r--r-- | METADATA | 15 | ||||
-rw-r--r-- | WORKSPACE | 5 | ||||
-rw-r--r-- | server/build.gradle | 2 | ||||
-rw-r--r-- | server/src/main/java/com/android/example/KeyAttestationExample.java | 53 | ||||
-rw-r--r-- | server/src/main/java/com/google/android/attestation/ASN1Parsing.java | 2 | ||||
-rw-r--r-- | server/src/main/java/com/google/android/attestation/AuthorizationList.java | 76 | ||||
-rw-r--r-- | server/src/main/java/com/google/android/attestation/BUILD | 2 | ||||
-rw-r--r-- | server/src/main/java/com/google/android/attestation/Constants.java | 55 | ||||
-rw-r--r-- | server/src/main/java/com/google/android/attestation/ParsedAttestationRecord.java | 27 | ||||
-rw-r--r-- | server/src/test/java/com/google/android/attestation/AuthorizationListTest.java | 33 | ||||
-rw-r--r-- | server/src/test/java/com/google/android/attestation/ParsedAttestationRecordTest.java | 37 |
14 files changed, 289 insertions, 88 deletions
diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml new file mode 100644 index 0000000..abe6fd2 --- /dev/null +++ b/.github/workflows/bazel.yml @@ -0,0 +1,35 @@ +name: Java CI with Bazel + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + + - name: Mount bazel cache + uses: actions/cache@v1 + with: + path: "/home/runner/.cache/bazel" + key: bazel + + - name: Install bazelisk + run: | + curl -LO "https://github.com/bazelbuild/bazelisk/releases/download/v1.1.0/bazelisk-linux-amd64" + mkdir -p "${GITHUB_WORKSPACE}/bin/" + mv bazelisk-linux-amd64 "${GITHUB_WORKSPACE}/bin/bazel" + chmod +x "${GITHUB_WORKSPACE}/bin/bazel" + + - name: Build + run: | + "${GITHUB_WORKSPACE}/bin/bazel" build //... + + - name: Test + run: | + "${GITHUB_WORKSPACE}/bin/bazel" test //... diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000..f38668f --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,31 @@ +# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle + +name: Java CI with Gradle + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + - name: Build with Gradle + uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 + with: + arguments: build + build-root-directory: server/ @@ -45,8 +45,10 @@ java_library_static { sdk_version: "current", static_libs: [ "bouncycastle-unbundled", + "error_prone_annotations", + "guava", ], libs: [ - "wycheproof-gson", + "gson", ], } diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..e1c123d --- /dev/null +++ b/METADATA @@ -0,0 +1,15 @@ +name: "android-key-attestation" +description: "Android Key Attestation validation library" +third_party { + url { + type: GIT + value: "https://github.com/google/android-key-attestation" + } + version: "e3a09702acdc332ef5a6496c5b78a2ca6d2713f8" + license_type: NOTICE + last_upgrade_date { + year: 2023 + month: 4 + day: 18 + } +} @@ -34,6 +34,11 @@ maven_install( # Gson used for decoding certificate status list "com.google.code.gson:gson:2.8.5", + "com.google.guava:guava:27.0.1-android", + "com.google.errorprone:error_prone_annotations:2.3.1", + + "com.squareup.okhttp3:okhttp:4.10.0", + # Test libraries "junit:junit:4.12", "com.google.truth:truth:1.0", diff --git a/server/build.gradle b/server/build.gradle index d5cb1cc..fff1b6a 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -27,6 +27,8 @@ repositories { dependencies { // Bouncy Castle Cryptography APIs used for certificate verification compile 'org.bouncycastle:bcpkix-jdk15on:1.61' + compile 'com.google.guava:guava:27.0.1-android' + compile 'com.google.errorprone:error_prone_annotations:2.3.1' // Gson used for decoding certificate status list compile 'com.google.code.gson:gson:2.8.5' // JUnit, Truth and Truth8 used for testing diff --git a/server/src/main/java/com/android/example/KeyAttestationExample.java b/server/src/main/java/com/android/example/KeyAttestationExample.java index a503dc7..5167c12 100644 --- a/server/src/main/java/com/android/example/KeyAttestationExample.java +++ b/server/src/main/java/com/android/example/KeyAttestationExample.java @@ -15,7 +15,7 @@ package com.android.example; -import static com.google.android.attestation.Constants.GOOGLE_ROOT_CERTIFICATE; +import static com.google.android.attestation.Constants.GOOGLE_ROOT_CA_PUB_KEY; import static com.google.android.attestation.ParsedAttestationRecord.createParsedAttestationRecord; import static java.nio.charset.StandardCharsets.UTF_8; @@ -25,6 +25,8 @@ import com.google.android.attestation.CertificateRevocationStatus; import com.google.android.attestation.AuthorizationList; import com.google.android.attestation.ParsedAttestationRecord; import com.google.android.attestation.RootOfTrust; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.file.Files; @@ -78,7 +80,7 @@ public class KeyAttestationExample { public static void main(String[] args) throws CertificateException, IOException, NoSuchProviderException, NoSuchAlgorithmException, InvalidKeyException, SignatureException { - X509Certificate[] certs; + List<X509Certificate> certs; if (args.length == 1) { String certFilesDir = args[0]; certs = loadCertificates(certFilesDir); @@ -88,7 +90,7 @@ public class KeyAttestationExample { verifyCertificateChain(certs); - ParsedAttestationRecord parsedAttestationRecord = createParsedAttestationRecord(certs[0]); + ParsedAttestationRecord parsedAttestationRecord = createParsedAttestationRecord(certs); System.out.println("Attestation version: " + parsedAttestationRecord.attestationVersion); System.out.println( @@ -161,12 +163,16 @@ public class KeyAttestationExample { printOptional(authorizationList.attestationIdProduct, indent + "Attestation ID Product"); printOptional(authorizationList.attestationIdSerial, indent + "Attestation ID Serial"); printOptional(authorizationList.attestationIdImei, indent + "Attestation ID IMEI"); + printOptional( + authorizationList.attestationIdSecondImei, indent + "Attestation ID SECOND IMEI"); printOptional(authorizationList.attestationIdMeid, indent + "Attestation ID MEID"); printOptional( authorizationList.attestationIdManufacturer, indent + "Attestation ID Manufacturer"); printOptional(authorizationList.attestationIdModel, indent + "Attestation ID Model"); printOptional(authorizationList.vendorPatchLevel, indent + "Vendor Patch Level"); printOptional(authorizationList.bootPatchLevel, indent + "Boot Patch Level"); + System.out.println( + indent + "Identity Credential Key: " + authorizationList.identityCredentialKey); } private static void printRootOfTrust(Optional<RootOfTrust> rootOfTrust, String indent) { @@ -209,12 +215,12 @@ public class KeyAttestationExample { } } - private static void verifyCertificateChain(X509Certificate[] certs) + private static void verifyCertificateChain(List<X509Certificate> certs) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException, IOException { - X509Certificate parent = certs[certs.length - 1]; - for (int i = certs.length - 1; i >= 0; i--) { - X509Certificate cert = certs[i]; + X509Certificate parent = certs.get(certs.size() - 1); + for (int i = certs.size() - 1; i >= 0; i--) { + X509Certificate cert = certs.get(i); // Verify that the certificate has not expired. cert.checkValidity(); cert.verify(parent.getPublicKey()); @@ -232,47 +238,40 @@ public class KeyAttestationExample { } // If the attestation is trustworthy and the device ships with hardware- - // level key attestation, Android 7.0 (API level 24) or higher, and + // backed key attestation, Android 7.0 (API level 24) or higher, and // Google Play services, the root certificate should be signed with the // Google attestation root key. - X509Certificate secureRoot = - (X509Certificate) - CertificateFactory.getInstance("X.509") - .generateCertificate( - new ByteArrayInputStream(GOOGLE_ROOT_CERTIFICATE.getBytes(UTF_8))); + byte[] googleRootCaPubKey = Base64.decode(GOOGLE_ROOT_CA_PUB_KEY); if (Arrays.equals( - secureRoot.getPublicKey().getEncoded(), - certs[certs.length - 1].getPublicKey().getEncoded())) { + googleRootCaPubKey, + certs.get(certs.size() - 1).getPublicKey().getEncoded())) { System.out.println( "The root certificate is correct, so this attestation is trustworthy, as long as none of" - + " the certificates in the chain have been revoked. A production-level system" - + " should check the certificate revocation lists using the distribution points that" - + " are listed in the intermediate and root certificates."); + + " the certificates in the chain have been revoked."); } else { System.out.println( "The root certificate is NOT correct. The attestation was probably generated by" - + " software, not in secure hardware. This means that, although the attestation" - + " contents are probably valid and correct, there is no proof that they are in fact" - + " correct. If you're using a production-level system, you should now treat the" - + " properties of this attestation certificate as advisory only, and you shouldn't" - + " rely on this attestation certificate to provide security guarantees."); + + " software, not in secure hardware. This means that there is no guarantee that the" + + " claims within the attestation are correct. If you're using a production-level" + + " system, you should disregard any claims made within this attestation certificate" + + " as there is no authority backing them up."); } } - private static X509Certificate[] loadCertificates(String certFilesDir) + private static ImmutableList<X509Certificate> loadCertificates(String certFilesDir) throws CertificateException, IOException { // Load the attestation certificates from the directory in alphabetic order. List<Path> records; try (Stream<Path> pathStream = Files.walk(Paths.get(certFilesDir))) { records = pathStream.filter(Files::isRegularFile).sorted().collect(Collectors.toList()); } - X509Certificate[] certs = new X509Certificate[records.size()]; + ImmutableList.Builder<X509Certificate> certs = new ImmutableList.Builder<>(); CertificateFactory factory = CertificateFactory.getInstance("X.509"); for (int i = 0; i < records.size(); ++i) { byte[] encodedCert = Files.readAllBytes(records.get(i)); ByteArrayInputStream inputStream = new ByteArrayInputStream(encodedCert); - certs[i] = (X509Certificate) factory.generateCertificate(inputStream); + certs.add((X509Certificate) factory.generateCertificate(inputStream)); } - return certs; + return certs.build(); } } diff --git a/server/src/main/java/com/google/android/attestation/ASN1Parsing.java b/server/src/main/java/com/google/android/attestation/ASN1Parsing.java index 1735822..d08f9ad 100644 --- a/server/src/main/java/com/google/android/attestation/ASN1Parsing.java +++ b/server/src/main/java/com/google/android/attestation/ASN1Parsing.java @@ -27,7 +27,7 @@ class ASN1Parsing { if (asn1Value instanceof ASN1Boolean) { return ((ASN1Boolean) asn1Value).isTrue(); } else { - throw new RuntimeException( + throw new IllegalArgumentException( "Boolean value expected; found " + asn1Value.getClass().getName() + " instead."); } } diff --git a/server/src/main/java/com/google/android/attestation/AuthorizationList.java b/server/src/main/java/com/google/android/attestation/AuthorizationList.java index d86922d..2ae2b50 100644 --- a/server/src/main/java/com/google/android/attestation/AuthorizationList.java +++ b/server/src/main/java/com/google/android/attestation/AuthorizationList.java @@ -32,11 +32,13 @@ import static com.google.android.attestation.Constants.KM_TAG_ATTESTATION_ID_MAN import static com.google.android.attestation.Constants.KM_TAG_ATTESTATION_ID_MEID; import static com.google.android.attestation.Constants.KM_TAG_ATTESTATION_ID_MODEL; import static com.google.android.attestation.Constants.KM_TAG_ATTESTATION_ID_PRODUCT; +import static com.google.android.attestation.Constants.KM_TAG_ATTESTATION_ID_SECOND_IMEI; import static com.google.android.attestation.Constants.KM_TAG_ATTESTATION_ID_SERIAL; import static com.google.android.attestation.Constants.KM_TAG_AUTH_TIMEOUT; import static com.google.android.attestation.Constants.KM_TAG_BOOT_PATCH_LEVEL; import static com.google.android.attestation.Constants.KM_TAG_CREATION_DATE_TIME; import static com.google.android.attestation.Constants.KM_TAG_DEVICE_UNIQUE_ATTESTATION; +import static com.google.android.attestation.Constants.KM_TAG_IDENTITY_CREDENTIAL_KEY; import static com.google.android.attestation.Constants.KM_TAG_DIGEST; import static com.google.android.attestation.Constants.KM_TAG_EC_CURVE; import static com.google.android.attestation.Constants.KM_TAG_KEY_SIZE; @@ -66,6 +68,8 @@ import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.common.collect.ImmutableSet; import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1EncodableVector; import org.bouncycastle.asn1.ASN1Integer; @@ -128,12 +132,14 @@ public class AuthorizationList { public final Optional<byte[]> attestationIdProduct; public final Optional<byte[]> attestationIdSerial; public final Optional<byte[]> attestationIdImei; + public final Optional<byte[]> attestationIdSecondImei; public final Optional<byte[]> attestationIdMeid; public final Optional<byte[]> attestationIdManufacturer; public final Optional<byte[]> attestationIdModel; public final Optional<Integer> vendorPatchLevel; public final Optional<Integer> bootPatchLevel; public final boolean individualAttestation; + public final boolean identityCredentialKey; private AuthorizationList(ASN1Encodable[] authorizationList, int attestationVersion) { Map<Integer, ASN1Primitive> authorizationMap = getAuthorizationMap(authorizationList); @@ -206,6 +212,8 @@ public class AuthorizationList { findOptionalByteArrayAuthorizationListEntry(authorizationMap, KM_TAG_ATTESTATION_ID_SERIAL); this.attestationIdImei = findOptionalByteArrayAuthorizationListEntry(authorizationMap, KM_TAG_ATTESTATION_ID_IMEI); + this.attestationIdSecondImei = + findOptionalByteArrayAuthorizationListEntry(authorizationMap, KM_TAG_ATTESTATION_ID_SECOND_IMEI); this.attestationIdMeid = findOptionalByteArrayAuthorizationListEntry(authorizationMap, KM_TAG_ATTESTATION_ID_MEID); this.attestationIdManufacturer = @@ -219,6 +227,9 @@ public class AuthorizationList { findOptionalIntegerAuthorizationListEntry(authorizationMap, KM_TAG_BOOT_PATCH_LEVEL); this.individualAttestation = findBooleanAuthorizationListEntry(authorizationMap, KM_TAG_DEVICE_UNIQUE_ATTESTATION); + this.identityCredentialKey = + findBooleanAuthorizationListEntry(authorizationMap, KM_TAG_IDENTITY_CREDENTIAL_KEY); + } private AuthorizationList(Builder builder) { @@ -255,12 +266,14 @@ public class AuthorizationList { this.attestationIdProduct = Optional.ofNullable(builder.attestationIdProduct); this.attestationIdSerial = Optional.ofNullable(builder.attestationIdSerial); this.attestationIdImei = Optional.ofNullable(builder.attestationIdImei); + this.attestationIdSecondImei = Optional.ofNullable(builder.attestationIdSecondImei); this.attestationIdMeid = Optional.ofNullable(builder.attestationIdMeid); this.attestationIdManufacturer = Optional.ofNullable(builder.attestationIdManufacturer); this.attestationIdModel = Optional.ofNullable(builder.attestationIdModel); this.vendorPatchLevel = Optional.ofNullable(builder.vendorPatchLevel); this.bootPatchLevel = Optional.ofNullable(builder.bootPatchLevel); this.individualAttestation = builder.individualAttestation; + this.identityCredentialKey = builder.identityCredentialKey; } static AuthorizationList createAuthorizationList( @@ -340,7 +353,7 @@ public class AuthorizationList { // Visible for testing. static Set<UserAuthType> userAuthTypeToEnum(long userAuthType) { if (userAuthType == 0) { - return Set.of(USER_AUTH_TYPE_NONE); + return ImmutableSet.of(USER_AUTH_TYPE_NONE); } Set<UserAuthType> result = new HashSet<>(); @@ -367,7 +380,7 @@ public class AuthorizationList { return 0L; } - Long result = 0L; + long result = 0L; for (UserAuthType type : userAuthType) { switch (type) { @@ -427,6 +440,7 @@ public class AuthorizationList { addOptionalOctetString(KM_TAG_ATTESTATION_ID_PRODUCT, this.attestationIdProduct, vector); addOptionalOctetString(KM_TAG_ATTESTATION_ID_SERIAL, this.attestationIdSerial, vector); addOptionalOctetString(KM_TAG_ATTESTATION_ID_IMEI, this.attestationIdImei, vector); + addOptionalOctetString(KM_TAG_ATTESTATION_ID_SECOND_IMEI, this.attestationIdSecondImei, vector); addOptionalOctetString(KM_TAG_ATTESTATION_ID_MEID, this.attestationIdMeid, vector); addOptionalOctetString( KM_TAG_ATTESTATION_ID_MANUFACTURER, this.attestationIdManufacturer, vector); @@ -543,203 +557,261 @@ public class AuthorizationList { byte[] attestationIdProduct; byte[] attestationIdSerial; byte[] attestationIdImei; + byte[] attestationIdSecondImei; byte[] attestationIdMeid; byte[] attestationIdManufacturer; byte[] attestationIdModel; Integer vendorPatchLevel; Integer bootPatchLevel; boolean individualAttestation; + boolean identityCredentialKey; + @CanIgnoreReturnValue public Builder setPurpose(Set<Integer> purpose) { this.purpose = purpose; return this; } + @CanIgnoreReturnValue public Builder setAlgorithm(Integer algorithm) { this.algorithm = algorithm; return this; } + @CanIgnoreReturnValue public Builder setKeySize(Integer keySize) { this.keySize = keySize; return this; } + @CanIgnoreReturnValue public Builder setDigest(Set<Integer> digest) { this.digest = digest; return this; } + @CanIgnoreReturnValue public Builder setPadding(Set<Integer> padding) { this.padding = padding; return this; } + @CanIgnoreReturnValue public Builder setEcCurve(Integer ecCurve) { this.ecCurve = ecCurve; return this; } + @CanIgnoreReturnValue public Builder setRsaPublicExponent(Long rsaPublicExponent) { this.rsaPublicExponent = rsaPublicExponent; return this; } + @CanIgnoreReturnValue public Builder setRollbackResistance(boolean rollbackResistance) { this.rollbackResistance = rollbackResistance; return this; } + @CanIgnoreReturnValue public Builder setActiveDateTime(Instant activeDateTime) { this.activeDateTime = activeDateTime; return this; } + @CanIgnoreReturnValue public Builder setOriginationExpireDateTime(Instant originationExpireDateTime) { this.originationExpireDateTime = originationExpireDateTime; return this; } + @CanIgnoreReturnValue public Builder setUsageExpireDateTime(Instant usageExpireDateTime) { this.usageExpireDateTime = usageExpireDateTime; return this; } + @CanIgnoreReturnValue public Builder setNoAuthRequired(boolean noAuthRequired) { this.noAuthRequired = noAuthRequired; return this; } + @CanIgnoreReturnValue public Builder setUserAuthType(Set<UserAuthType> userAuthType) { this.userAuthType = userAuthType; return this; } + @CanIgnoreReturnValue public Builder setAuthTimeout(Duration authTimeout) { this.authTimeout = authTimeout; return this; } + @CanIgnoreReturnValue public Builder setAllowWhileOnBody(boolean allowWhileOnBody) { this.allowWhileOnBody = allowWhileOnBody; return this; } + @CanIgnoreReturnValue public Builder setTrustedUserPresenceRequired(boolean trustedUserPresenceRequired) { this.trustedUserPresenceRequired = trustedUserPresenceRequired; return this; } + @CanIgnoreReturnValue public Builder setTrustedConfirmationRequired(boolean trustedConfirmationRequired) { this.trustedConfirmationRequired = trustedConfirmationRequired; return this; } + @CanIgnoreReturnValue public Builder setUnlockedDeviceRequired(boolean unlockedDeviceRequired) { this.unlockedDeviceRequired = unlockedDeviceRequired; return this; } + @CanIgnoreReturnValue public Builder setAllApplications(boolean allApplications) { this.allApplications = allApplications; return this; } + @CanIgnoreReturnValue public Builder setApplicationId(byte[] applicationId) { this.applicationId = applicationId; return this; } + @CanIgnoreReturnValue public Builder setCreationDateTime(Instant creationDateTime) { this.creationDateTime = creationDateTime; return this; } + @CanIgnoreReturnValue public Builder setOrigin(Integer origin) { this.origin = origin; return this; } + @CanIgnoreReturnValue public Builder setRollbackResistant(boolean rollbackResistant) { this.rollbackResistant = rollbackResistant; return this; } + @CanIgnoreReturnValue public Builder setRootOfTrust(RootOfTrust rootOfTrust) { this.rootOfTrust = rootOfTrust; return this; } + @CanIgnoreReturnValue public Builder setOsVersion(Integer osVersion) { this.osVersion = osVersion; return this; } + @CanIgnoreReturnValue public Builder setOsPatchLevel(Integer osPatchLevel) { this.osPatchLevel = osPatchLevel; return this; } + @CanIgnoreReturnValue public Builder setAttestationApplicationId(AttestationApplicationId attestationApplicationId) { this.attestationApplicationId = attestationApplicationId; return this; } + @CanIgnoreReturnValue public Builder setAttestationApplicationIdBytes(byte[] attestationApplicationIdBytes) { this.attestationApplicationIdBytes = attestationApplicationIdBytes; return this; } + @CanIgnoreReturnValue public Builder setAttestationIdBrand(byte[] attestationIdBrand) { this.attestationIdBrand = attestationIdBrand; return this; } + @CanIgnoreReturnValue + public Builder setAttestationIdDevice(byte[] attestationIdDevice) { + this.attestationIdDevice = attestationIdDevice; + return this; + } + + @CanIgnoreReturnValue public Builder setAttestationIdProduct(byte[] attestationIdProduct) { this.attestationIdProduct = attestationIdProduct; return this; } + @CanIgnoreReturnValue public Builder setAttestationIdSerial(byte[] attestationIdSerial) { this.attestationIdSerial = attestationIdSerial; return this; } + @CanIgnoreReturnValue public Builder setAttestationIdImei(byte[] attestationIdImei) { this.attestationIdImei = attestationIdImei; return this; } + @CanIgnoreReturnValue + public Builder setAttestationIdSecondImei(byte[] attestationIdSecondImei) { + this.attestationIdSecondImei = attestationIdSecondImei; + return this; + } + + @CanIgnoreReturnValue public Builder setAttestationIdMeid(byte[] attestationIdMeid) { this.attestationIdMeid = attestationIdMeid; return this; } + @CanIgnoreReturnValue public Builder setAttestationIdManufacturer(byte[] attestationIdManufacturer) { this.attestationIdManufacturer = attestationIdManufacturer; return this; } + @CanIgnoreReturnValue public Builder setAttestationIdModel(byte[] attestationIdModel) { this.attestationIdModel = attestationIdModel; return this; } + @CanIgnoreReturnValue public Builder setVendorPatchLevel(Integer vendorPatchLevel) { this.vendorPatchLevel = vendorPatchLevel; return this; } + @CanIgnoreReturnValue public Builder setBootPatchLevel(Integer bootPatchLevel) { this.bootPatchLevel = bootPatchLevel; return this; } + @CanIgnoreReturnValue public Builder setIndividualAttestation(boolean individualAttestation) { this.individualAttestation = individualAttestation; return this; } + @CanIgnoreReturnValue + public Builder setIdentityCredentialKey(boolean identityCredentialKey) { + this.identityCredentialKey = identityCredentialKey; + return this; + } + public AuthorizationList build() { return new AuthorizationList(this); } diff --git a/server/src/main/java/com/google/android/attestation/BUILD b/server/src/main/java/com/google/android/attestation/BUILD index d2c2ae2..f1023c8 100644 --- a/server/src/main/java/com/google/android/attestation/BUILD +++ b/server/src/main/java/com/google/android/attestation/BUILD @@ -13,6 +13,8 @@ java_library( ], deps = [ "@maven//:com_google_code_gson_gson", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", "@maven//:org_bouncycastle_bcpkix_jdk15on", "@maven//:org_bouncycastle_bcprov_jdk15on", ], diff --git a/server/src/main/java/com/google/android/attestation/Constants.java b/server/src/main/java/com/google/android/attestation/Constants.java index 7db2744..a69d364 100644 --- a/server/src/main/java/com/google/android/attestation/Constants.java +++ b/server/src/main/java/com/google/android/attestation/Constants.java @@ -18,42 +18,23 @@ package com.google.android.attestation; /** Key Attestation constants */ public class Constants { - // The Google root certificate that must have been used to sign the root - // certificate in a real attestation certificate chain from a compliant - // device. - // (Note, the sample chain used here is not signed with this certificate.) - public static final String GOOGLE_ROOT_CERTIFICATE = - "-----BEGIN CERTIFICATE-----\n" - + "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\n" - + "-----END CERTIFICATE-----"; + // The Google root public key corresponding to the private key that must + // have been used to self-sign the root of a real attestation certificate + // chain from a compliant device. + // (Note, the sample chain used here is not signed with the Google root CA.) + public static final String GOOGLE_ROOT_CA_PUB_KEY = + "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xU" + + "FmOr75gvMsd/dTEDDJdSSxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5j" + + "lRfdnJLmN0pTy/4lj4/7tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y" + + "//0rb+T+W8a9nsNL/ggjnar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73X" + + "pXyTqRxB/M0n1n/W9nGqC4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYI" + + "mQQcHtGl/m00QLVWutHQoVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB" + + "+TxywElgS70vE0XmLD+OJtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7q" + + "uvmag8jfPioyKvxnK/EgsTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgp" + + "Zrt3i5MIlCaY504LzSRiigHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7" + + "gLiMm0jhO2B6tUXHI/+MRPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82" + + "ixPvZtXQpUpuL12ab+9EaDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+" + + "NpUFgNPN9PvQi8WEg5UmAGMCAwEAAQ=="; static final String KEY_DESCRIPTION_OID = "1.3.6.1.4.1.11129.2.1.17"; static final int ATTESTATION_VERSION_INDEX = 0; static final int ATTESTATION_SECURITY_LEVEL_INDEX = 1; @@ -103,6 +84,8 @@ public class Constants { static final int KM_TAG_VENDOR_PATCH_LEVEL = 718; static final int KM_TAG_BOOT_PATCH_LEVEL = 719; static final int KM_TAG_DEVICE_UNIQUE_ATTESTATION = 720; + static final int KM_TAG_IDENTITY_CREDENTIAL_KEY = 721; + static final int KM_TAG_ATTESTATION_ID_SECOND_IMEI = 723; static final int ROOT_OF_TRUST_VERIFIED_BOOT_KEY_INDEX = 0; static final int ROOT_OF_TRUST_DEVICE_LOCKED_INDEX = 1; static final int ROOT_OF_TRUST_VERIFIED_BOOT_STATE_INDEX = 2; diff --git a/server/src/main/java/com/google/android/attestation/ParsedAttestationRecord.java b/server/src/main/java/com/google/android/attestation/ParsedAttestationRecord.java index 053baae..f1aa7e8 100644 --- a/server/src/main/java/com/google/android/attestation/ParsedAttestationRecord.java +++ b/server/src/main/java/com/google/android/attestation/ParsedAttestationRecord.java @@ -30,6 +30,7 @@ import static com.google.android.attestation.Constants.UNIQUE_ID_INDEX; import java.io.IOException; import java.security.cert.X509Certificate; +import java.util.List; import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1Enumerated; import org.bouncycastle.asn1.ASN1InputStream; @@ -96,10 +97,23 @@ public class ParsedAttestationRecord { this.teeEnforced = teeEnforced; } - public static ParsedAttestationRecord createParsedAttestationRecord(X509Certificate cert) + public static ParsedAttestationRecord createParsedAttestationRecord(List<X509Certificate> certs) throws IOException { - ASN1Sequence extensionData = extractAttestationSequence(cert); - return new ParsedAttestationRecord(extensionData); + + // Parse the attestation record that is closest to the root. This prevents an adversary from + // attesting an attestation record of their choice with an otherwise trusted chain using the + // following attack: + // 1) having the TEE attest a key under the adversary's control, + // 2) using that key to sign a new leaf certificate with an attestation extension that has their chosen attestation record, then + // 3) appending that certificate to the original certificate chain. + for (int i = certs.size() - 1; i >= 0; i--) { + byte[] attestationExtensionBytes = certs.get(i).getExtensionValue(KEY_DESCRIPTION_OID); + if (attestationExtensionBytes != null && attestationExtensionBytes.length != 0) { + return new ParsedAttestationRecord(extractAttestationSequence(attestationExtensionBytes)); + } + } + + throw new IllegalArgumentException("Couldn't find the keystore attestation extension data."); } public static ParsedAttestationRecord create(ASN1Sequence extensionData) { @@ -151,13 +165,8 @@ public class ParsedAttestationRecord { throw new IllegalArgumentException("Invalid security level."); } - private static ASN1Sequence extractAttestationSequence(X509Certificate attestationCert) + private static ASN1Sequence extractAttestationSequence(byte[] attestationExtensionBytes) throws IOException { - byte[] attestationExtensionBytes = attestationCert.getExtensionValue(KEY_DESCRIPTION_OID); - if (attestationExtensionBytes == null || attestationExtensionBytes.length == 0) { - throw new IllegalArgumentException("Couldn't find the keystore attestation extension data."); - } - ASN1Sequence decodedSequence; try (ASN1InputStream asn1InputStream = new ASN1InputStream(attestationExtensionBytes)) { // The extension contains one object, a sequence, in the diff --git a/server/src/test/java/com/google/android/attestation/AuthorizationListTest.java b/server/src/test/java/com/google/android/attestation/AuthorizationListTest.java index 637e8ca..37e3319 100644 --- a/server/src/test/java/com/google/android/attestation/AuthorizationListTest.java +++ b/server/src/test/java/com/google/android/attestation/AuthorizationListTest.java @@ -26,10 +26,8 @@ import static com.google.common.truth.Truth8.assertThat; import static org.junit.Assert.fail; import com.google.common.collect.ImmutableSet; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.time.Instant; -import java.util.Set; import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1Sequence; @@ -113,6 +111,7 @@ public class AuthorizationListTest { assertThat(authorizationList.attestationApplicationIdBytes) .hasValue(EXPECTED_SW_ATTESTATION_APPLICATION_ID_BYTES); assertThat(authorizationList.individualAttestation).isFalse(); + assertThat(authorizationList.identityCredentialKey).isFalse(); } @Test @@ -135,15 +134,17 @@ public class AuthorizationListTest { assertThat(authorizationList.vendorPatchLevel).hasValue(EXPECTED_TEE_VENDOR_PATCH_LEVEL); assertThat(authorizationList.bootPatchLevel).hasValue(EXPECTED_TEE_BOOT_PATCH_LEVEL); assertThat(authorizationList.individualAttestation).isFalse(); + assertThat(authorizationList.identityCredentialKey).isFalse(); } @Test public void testUserAuthTypeToEnum() { - assertThat(userAuthTypeToEnum(0L)).isEqualTo(Set.of(USER_AUTH_TYPE_NONE)); - assertThat(userAuthTypeToEnum(1L)).isEqualTo(Set.of(PASSWORD)); - assertThat(userAuthTypeToEnum(2L)).isEqualTo(Set.of(FINGERPRINT)); - assertThat(userAuthTypeToEnum(3L)).isEqualTo(Set.of(PASSWORD, FINGERPRINT)); - assertThat(userAuthTypeToEnum(UINT32_MAX)).isEqualTo(Set.of(PASSWORD, FINGERPRINT, USER_AUTH_TYPE_ANY)); + assertThat(userAuthTypeToEnum(0L)).isEqualTo(ImmutableSet.of(USER_AUTH_TYPE_NONE)); + assertThat(userAuthTypeToEnum(1L)).isEqualTo(ImmutableSet.of(PASSWORD)); + assertThat(userAuthTypeToEnum(2L)).isEqualTo(ImmutableSet.of(FINGERPRINT)); + assertThat(userAuthTypeToEnum(3L)).isEqualTo(ImmutableSet.of(PASSWORD, FINGERPRINT)); + assertThat(userAuthTypeToEnum(UINT32_MAX)).isEqualTo(ImmutableSet.of(PASSWORD, FINGERPRINT, + USER_AUTH_TYPE_ANY)); try { @@ -170,6 +171,24 @@ public class AuthorizationListTest { assertThat(authorizationList.individualAttestation).isTrue(); } + private static final String EXTENTION_DATA_WITH_ID_CREDENTIAL_KEY = + "MIH0oQgxBgIBAgIBA6IDAgEBowQCAggApQUxAwIBBKYIMQYCAQMCAQW/" + + "gUgFAgMBAAG/g3cCBQC/hT4DAgEAv4VATDBKBCAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAEBAAoBAgQgEvR7Lf1t9nD6P2qyUmgi" + + "Q0mG+RixYnglj2TaAMZmHn2/hUEFAgMBrbC/hUIFAgMDFRi/hUYIBAZn" + + "b29nbGW/hUcHBAVzYXJnb7+FSAcEBXNhcmdvv4VMCAQGR29vZ2xlv4VN" + + "CgQIUGl4ZWwgM2G/hU4GAgQBND1lv4VPBgIEATQ9Zb+FUQIFAA=="; + + @Test + public void testCanParseIdentityCredentialTag() throws IOException { + AuthorizationList authorizationList = + AuthorizationList.createAuthorizationList( + getEncodableAuthorizationList(EXTENTION_DATA_WITH_ID_CREDENTIAL_KEY), + ATTESTATION_VERSION); + + assertThat(authorizationList.identityCredentialKey).isTrue(); + } + @Test public void testCreateAndParse() throws IOException { AuthorizationList authorizationList = diff --git a/server/src/test/java/com/google/android/attestation/ParsedAttestationRecordTest.java b/server/src/test/java/com/google/android/attestation/ParsedAttestationRecordTest.java index 678ea9a..678d08e 100644 --- a/server/src/test/java/com/google/android/attestation/ParsedAttestationRecordTest.java +++ b/server/src/test/java/com/google/android/attestation/ParsedAttestationRecordTest.java @@ -25,7 +25,9 @@ import java.io.IOException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import java.util.Set; +import java.util.Arrays; + +import com.google.common.collect.ImmutableSet; import org.bouncycastle.asn1.ASN1Sequence; import org.junit.Test; import org.junit.runner.RunWith; @@ -67,6 +69,31 @@ public class ParsedAttestationRecordTest { + "OIkXEoFqqGi9GKJXUT1KYi5NsigaYqu7FoN4Qsvs61pMUEfZSPP2AFwkA8uNFbmb9uxcxaGHCA8i" + "3i9VM6yOLIrP\n" + "-----END CERTIFICATE-----"; + private static final String CERT2 = + "-----BEGIN CERTIFICATE-----\n" + + "MIIEFDCCAnygAwIBAgIVAKZFQPAXr5VWrosuqx4C8tai2XbHMA0GCSqGSIb3DQEB\n" + + "CwUAMBgxFjAUBgNVBAMMDVVua25vd25Jc3N1ZXIwHhcNMjMwMjE1MTU0MzIwWhcN\n" + + "MjMwMjE1MTU0ODIwWjAdMRswGQYDVQQDDBJBbmRyb2lkQXR0ZXN0ZWRLZXkwggGi\n" + + "MA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDJAVfP/7F1bUbDqxMnOVXpSjt5\n" + + "NJwYemBJkN7l7TTbAhTfMW91006Si/snd79Y6bsJklVoiEN9LGL7tQrJEf5lSSLX\n" + + "ZeppjsbLqKnogFHhDJy2vaSiypV2wZdX+kO0qqIKjRvgSqHuTz3gemI1rWilrG3C\n" + + "vd3iHGlkw/4X5PpHQKz99/20p85HP6f/jydMHewFDRQCbkbo2pJ5WrJsyPe9me3o\n" + + "QE0O3lgij7jJ/UBHyb9iH0w13yi+1yZ/jgyojL4QNUeWZnxW656zfHCB8weePD+l\n" + + "tX4AAztZTziJQwk3zVClw4xIPTeztQV6ddRQgjSjGvWanpXqhJx8mq11gWaVJoCl\n" + + "q/I0KOguVsKq42M25uhF7/iAQjC+6lOUUfi2+aPwyTUfGHc5Bw/rTSw2LzvZDnUW\n" + + "8/yw4OUTyDravVcQLeoBES4+O5cVL0yTKDY0THG+ymgsFNgFS7PXUnAbXczYzvg8\n" + + "ldXKOXxnF5nWgg55n2iSQ6mqtHDEUsjcxjmuFcMCAwEAAaNQME4wTAYKKwYBBAHW\n" + + "eQIBEQQ+MDwCAQEKAQECAQIKAQEEEkEgcmFuZG9tIGNoYWxsZW5nZQQAMAAwFr+F\n" + + "SQgEBlNFUklBTL+FSgYEBElNRUkwDQYJKoZIhvcNAQELBQADggGBAHSms4IBjkc8\n" + + "1ZLHu5l70Ih2RrNU4XAc2E/oJX8OsBte9ZRwDT3TdcfLeg0rSneS+aB4xN1BGfmL\n" + + "DPZ1epRzMY4RagVhzBEauHpTaM2imRT9RN5TxbFvuMC4ELICYr5qHfqeALIlMET3\n" + + "TbCAo3njpNh5ids6qdlmpZRoYBQNMKfWJn8SUtCmVMk87FA7RZZCqCiRk+PBnciT\n" + + "O3LLbwT4aBlMinQ84gBfVXRqOvGAeGOgojDqGyK3tDMjIS7itpGb23vGogxHiHjA\n" + + "i8hiQhsHA+C89duCdeGyWZGmxwln7QRsosFI7G4ZOufXPLZt/DauNAC2Mb2OPcDw\n" + + "4tSKQvzQiL9UG4X3Cck0JnATxjT5sLttshJl98V6jQHcWSnjg8+oa3B8WgcePX8E\n" + + "QgcLhYaEGo9WDYJQvHfuUE5AquTxdTRbeiDbV7W+FAOQ5zi/wiGit86gF26120OQ\n" + + "KzQHP94/ORuAT/lkv3Fp3HytF4n3scur1nI0WqrfKpbUuPkmndCIbg==\n" + + "-----END CERTIFICATE-----\n"; private static final int EXPECTED_ATTESTATION_VERSION = 3; private static final SecurityLevel EXPECTED_ATTESTATION_SECURITY_LEVEL = @@ -82,15 +109,15 @@ public class ParsedAttestationRecordTest { X509Certificate cert = (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(certStr.getBytes(UTF_8))); - cert.checkValidity(); return cert; } @Test public void testParseAttestationRecord() throws CertificateException, IOException { X509Certificate x509Certificate = getAttestationRecord(CERT); + X509Certificate x509Certificate2 = getAttestationRecord(CERT2); ParsedAttestationRecord attestationRecord = - ParsedAttestationRecord.createParsedAttestationRecord(x509Certificate); + ParsedAttestationRecord.createParsedAttestationRecord(Arrays.asList(x509Certificate2, x509Certificate)); assertThat(attestationRecord.attestationVersion).isEqualTo(EXPECTED_ATTESTATION_VERSION); assertThat(attestationRecord.attestationSecurityLevel) @@ -107,7 +134,7 @@ public class ParsedAttestationRecordTest { @Test public void testCreateAndParseAttestationRecord() { AuthorizationList.Builder teeEnforcedBuilder = AuthorizationList.builder(); - teeEnforcedBuilder.userAuthType = Set.of(UserAuthType.FINGERPRINT); + teeEnforcedBuilder.userAuthType = ImmutableSet.of(UserAuthType.FINGERPRINT); teeEnforcedBuilder.attestationIdBrand = "free food".getBytes(UTF_8); ParsedAttestationRecord expected = ParsedAttestationRecord.create( @@ -119,7 +146,7 @@ public class ParsedAttestationRecordTest { /* uniqueId= */ "foodplease".getBytes(UTF_8), /* softwareEnforced= */ AuthorizationList.builder().build(), /* teeEnforced= */ AuthorizationList.builder() - .setUserAuthType(Set.of(UserAuthType.FINGERPRINT)) + .setUserAuthType(ImmutableSet.of(UserAuthType.FINGERPRINT)) .setAttestationIdBrand("free food".getBytes(UTF_8)).build()); ASN1Sequence seq = expected.toAsn1Sequence(); ParsedAttestationRecord actual = ParsedAttestationRecord.create(seq); |