Implementing verification of PROFILE_PEER_DEVICE.
Bug: 216477071
Test: AttestationVerificationTest unit test
Change-Id: Ide254de1aaaad24a5ac9e449086192aa9f59a72b
diff --git a/services/core/java/com/android/server/security/AndroidKeystoreAttestationVerificationAttributes.java b/services/core/java/com/android/server/security/AndroidKeystoreAttestationVerificationAttributes.java
new file mode 100644
index 0000000..3543e93
--- /dev/null
+++ b/services/core/java/com/android/server/security/AndroidKeystoreAttestationVerificationAttributes.java
@@ -0,0 +1,468 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.security;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.framework.protobuf.ByteString;
+import com.android.internal.org.bouncycastle.asn1.ASN1Boolean;
+import com.android.internal.org.bouncycastle.asn1.ASN1Encodable;
+import com.android.internal.org.bouncycastle.asn1.ASN1Enumerated;
+import com.android.internal.org.bouncycastle.asn1.ASN1InputStream;
+import com.android.internal.org.bouncycastle.asn1.ASN1Integer;
+import com.android.internal.org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import com.android.internal.org.bouncycastle.asn1.ASN1OctetString;
+import com.android.internal.org.bouncycastle.asn1.ASN1Sequence;
+import com.android.internal.org.bouncycastle.asn1.ASN1Set;
+import com.android.internal.org.bouncycastle.asn1.ASN1TaggedObject;
+import com.android.internal.org.bouncycastle.asn1.x509.Certificate;
+
+import java.nio.charset.StandardCharsets;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Parsed {@link X509Certificate} attestation extension values for Android Keystore attestations.
+ *
+ * Pull fields out of the top-level sequence. A full description of this structure is at
+ * https://source.android.com/security/keystore/attestation.
+ *
+ * If a value is null or empty, then it was not set/found in the extension values.
+ *
+ */
+class AndroidKeystoreAttestationVerificationAttributes {
+ // The OID for the extension Android Keymaster puts into device-generated certificates.
+ private static final String ANDROID_KEYMASTER_KEY_DESCRIPTION_EXTENSION_OID =
+ "1.3.6.1.4.1.11129.2.1.17";
+
+ // ASN.1 sequence index values for the Android Keymaster extension.
+ private static final int ATTESTATION_VERSION_INDEX = 0;
+ private static final int ATTESTATION_SECURITY_LEVEL_INDEX = 1;
+ private static final int KEYMASTER_VERSION_INDEX = 2;
+ private static final int KEYMASTER_SECURITY_LEVEL_INDEX = 3;
+ private static final int ATTESTATION_CHALLENGE_INDEX = 4;
+ private static final int KEYMASTER_UNIQUE_ID_INDEX = 5;
+ private static final int SW_ENFORCED_INDEX = 6;
+ private static final int HW_ENFORCED_INDEX = 7;
+ private static final int VERIFIED_BOOT_KEY_INDEX = 0;
+ private static final int VERIFIED_BOOT_LOCKED_INDEX = 1;
+ private static final int VERIFIED_BOOT_STATE_INDEX = 2;
+ private static final int VERIFIED_BOOT_HASH_INDEX = 3;
+
+ // ASN.1 sequence index values for the Android Keystore application id.
+ private static final int PACKAGE_INFO_SET_INDEX = 0;
+ private static final int PACKAGE_SIGNATURE_SET_INDEX = 1;
+ private static final int PACKAGE_INFO_NAME_INDEX = 0;
+ private static final int PACKAGE_INFO_VERSION_INDEX = 1;
+
+ // See these AOSP files: hardware/libhardware/include/hardware/hw_auth_token.h
+ private static final int HW_AUTH_NONE = 0;
+
+ // Some keymaster constants. See this AOSP file:
+ // hardware/libhardware/include/hardware/keymaster_defs.h
+ private static final int KM_TAG_NO_AUTH_REQUIRED = 503;
+ private static final int KM_TAG_UNLOCKED_DEVICE_REQUIRED = 509;
+ private static final int KM_TAG_ALL_APPLICATIONS = 600;
+ private static final int KM_TAG_ROOT_OF_TRUST = 704;
+ private static final int KM_TAG_OS_VERSION = 705;
+ private static final int KM_TAG_OS_PATCHLEVEL = 706;
+ private static final int KM_TAG_ATTESTATION_APPLICATION_ID = 709;
+ private static final int KM_TAG_ATTESTATION_ID_BRAND = 710;
+ private static final int KM_TAG_ATTESTATION_ID_DEVICE = 711;
+ private static final int KM_TAG_ATTESTATION_ID_PRODUCT = 712;
+ private static final int KM_TAG_VENDOR_PATCHLEVEL = 718;
+ private static final int KM_TAG_BOOT_PATCHLEVEL = 719;
+
+ private static final int KM_SECURITY_LEVEL_SOFTWARE = 0;
+ private static final int KM_SECURITY_LEVEL_TRUSTED_ENVIRONMENT = 1;
+ private static final int KM_SECURITY_LEVEL_STRONG_BOX = 2;
+ private static final int KM_VERIFIED_BOOT_STATE_VERIFIED = 0;
+ private static final int KM_VERIFIED_BOOT_STATE_SELF_SIGNED = 1;
+ private static final int KM_VERIFIED_BOOT_STATE_UNVERIFIED = 2;
+ private static final int KM_VERIFIED_BOOT_STATE_FAILED = 3;
+
+ private Integer mAttestationVersion = null;
+ private SecurityLevel mAttestationSecurityLevel = null;
+ private boolean mAttestationHardwareBacked = false;
+ private Integer mKeymasterVersion = null;
+ private SecurityLevel mKeymasterSecurityLevel = null;
+ private boolean mKeymasterHardwareBacked = false;
+ private ByteString mAttestationChallenge = null;
+ private ByteString mKeymasterUniqueId = null;
+ private String mDeviceBrand = null;
+ private String mDeviceName = null;
+ private String mDeviceProductName = null;
+ private boolean mKeyAllowedForAllApplications = false;
+ private Integer mKeyAuthenticatorType = null;
+ private Integer mKeyBootPatchLevel = null;
+ private Integer mKeyOsPatchLevel = null;
+ private Integer mKeyOsVersion = null;
+ private Integer mKeyVendorPatchLevel = null;
+ private Boolean mKeyRequiresUnlockedDevice = null;
+ private ByteString mVerifiedBootHash = null;
+ private ByteString mVerifiedBootKey = null;
+ private Boolean mVerifiedBootLocked = null;
+ private VerifiedBootState mVerifiedBootState = null;
+ private Map<String, Long> mApplicationPackageNameVersion = null;
+ private List<ByteString> mApplicationCertificateDigests = null;
+
+ enum VerifiedBootState {
+ VERIFIED,
+ SELF_SIGNED,
+ UNVERIFIED,
+ FAILED
+ }
+
+ enum SecurityLevel {
+ SOFTWARE,
+ TRUSTED_ENVIRONMENT,
+ STRONG_BOX
+ }
+
+ /**
+ * Extracts attestation extension properties from {@link X509Certificate}
+ * and returns a {@link AndroidKeystoreAttestationVerificationAttributes} that encapsulates the
+ * properties.
+ */
+ @NonNull
+ static AndroidKeystoreAttestationVerificationAttributes fromCertificate(
+ @NonNull X509Certificate x509Certificate)
+ throws Exception {
+ return new AndroidKeystoreAttestationVerificationAttributes(x509Certificate);
+ }
+
+ int getAttestationVersion() {
+ return mAttestationVersion;
+ }
+
+ @Nullable
+ SecurityLevel getAttestationSecurityLevel() {
+ return mAttestationSecurityLevel;
+ }
+
+ boolean isAttestationHardwareBacked() {
+ return mAttestationHardwareBacked;
+ }
+
+ int getKeymasterVersion() {
+ return mKeymasterVersion;
+ }
+
+ @Nullable
+ SecurityLevel getKeymasterSecurityLevel() {
+ return mKeymasterSecurityLevel;
+ }
+
+ boolean isKeymasterHardwareBacked() {
+ return mKeymasterHardwareBacked;
+ }
+
+ @Nullable
+ ByteString getAttestationChallenge() {
+ return mAttestationChallenge;
+ }
+
+ @Nullable
+ ByteString getKeymasterUniqueId() {
+ return mKeymasterUniqueId;
+ }
+
+ @Nullable
+ String getDeviceBrand() {
+ return mDeviceBrand;
+ }
+
+ @Nullable
+ String getDeviceName() {
+ return mDeviceName;
+ }
+
+ @Nullable
+ String getDeviceProductName() {
+ return mDeviceProductName;
+ }
+
+ boolean isKeyAllowedForAllApplications() {
+ return mKeyAllowedForAllApplications;
+ }
+
+ int getKeyAuthenticatorType() {
+ if (mKeyAuthenticatorType == null) {
+ throw new IllegalStateException("KeyAuthenticatorType is not set.");
+ }
+ return mKeyAuthenticatorType;
+ }
+
+ int getKeyBootPatchLevel() {
+ if (mKeyBootPatchLevel == null) {
+ throw new IllegalStateException("KeyBootPatchLevel is not set.");
+ }
+ return mKeyBootPatchLevel;
+ }
+
+ int getKeyOsPatchLevel() {
+ if (mKeyOsPatchLevel == null) {
+ throw new IllegalStateException("KeyOsPatchLevel is not set.");
+ }
+ return mKeyOsPatchLevel;
+ }
+
+ int getKeyVendorPatchLevel() {
+ if (mKeyVendorPatchLevel == null) {
+ throw new IllegalStateException("KeyVendorPatchLevel is not set.");
+ }
+ return mKeyVendorPatchLevel;
+ }
+
+ int getKeyOsVersion() {
+ if (mKeyOsVersion == null) {
+ throw new IllegalStateException("KeyOsVersion is not set.");
+ }
+ return mKeyOsVersion;
+ }
+
+ boolean isKeyRequiresUnlockedDevice() {
+ if (mKeyRequiresUnlockedDevice == null) {
+ throw new IllegalStateException("KeyRequiresUnlockedDevice is not set.");
+ }
+ return mKeyRequiresUnlockedDevice;
+ }
+
+ @Nullable
+ ByteString getVerifiedBootHash() {
+ return mVerifiedBootHash;
+ }
+
+ @Nullable
+ ByteString getVerifiedBootKey() {
+ return mVerifiedBootKey;
+ }
+
+ boolean isVerifiedBootLocked() {
+ if (mVerifiedBootLocked == null) {
+ throw new IllegalStateException("VerifiedBootLocked is not set.");
+ }
+ return mVerifiedBootLocked;
+ }
+
+ @Nullable
+ VerifiedBootState getVerifiedBootState() {
+ return mVerifiedBootState;
+ }
+
+ @Nullable
+ Map<String, Long> getApplicationPackageNameVersion() {
+ return Collections.unmodifiableMap(mApplicationPackageNameVersion);
+ }
+
+ @Nullable
+ List<ByteString> getApplicationCertificateDigests() {
+ return Collections.unmodifiableList(mApplicationCertificateDigests);
+ }
+
+ private AndroidKeystoreAttestationVerificationAttributes(X509Certificate x509Certificate)
+ throws Exception {
+ Certificate certificate = Certificate.getInstance(
+ new ASN1InputStream(x509Certificate.getEncoded()).readObject());
+ ASN1Sequence keyAttributes = (ASN1Sequence) certificate.getTBSCertificate().getExtensions()
+ .getExtensionParsedValue(
+ new ASN1ObjectIdentifier(ANDROID_KEYMASTER_KEY_DESCRIPTION_EXTENSION_OID));
+ if (keyAttributes == null) {
+ throw new CertificateEncodingException(
+ "No attestation extension found in certificate.");
+ }
+ this.mAttestationVersion = getIntegerFromAsn1(
+ keyAttributes.getObjectAt(ATTESTATION_VERSION_INDEX));
+ this.mAttestationSecurityLevel = getSecurityLevelEnum(
+ keyAttributes.getObjectAt(ATTESTATION_SECURITY_LEVEL_INDEX));
+ this.mAttestationHardwareBacked =
+ this.mAttestationSecurityLevel == SecurityLevel.TRUSTED_ENVIRONMENT;
+ this.mAttestationChallenge = getOctetsFromAsn1(
+ keyAttributes.getObjectAt(ATTESTATION_CHALLENGE_INDEX));
+ this.mKeymasterVersion = getIntegerFromAsn1(
+ keyAttributes.getObjectAt(KEYMASTER_VERSION_INDEX));
+ this.mKeymasterUniqueId = getOctetsFromAsn1(
+ keyAttributes.getObjectAt(KEYMASTER_UNIQUE_ID_INDEX));
+ this.mKeymasterSecurityLevel = getSecurityLevelEnum(
+ keyAttributes.getObjectAt(KEYMASTER_SECURITY_LEVEL_INDEX));
+ this.mKeymasterHardwareBacked =
+ this.mKeymasterSecurityLevel == SecurityLevel.TRUSTED_ENVIRONMENT;
+
+ ASN1Encodable[] softwareEnforced = ((ASN1Sequence)
+ keyAttributes.getObjectAt(SW_ENFORCED_INDEX)).toArray();
+ for (ASN1Encodable entry : softwareEnforced) {
+ ASN1TaggedObject taggedEntry = (ASN1TaggedObject) entry;
+ switch (taggedEntry.getTagNo()) {
+ case KM_TAG_ATTESTATION_APPLICATION_ID:
+ parseAttestationApplicationId(
+ getOctetsFromAsn1(taggedEntry.getObject()).toByteArray());
+ break;
+ case KM_TAG_UNLOCKED_DEVICE_REQUIRED:
+ this.mKeyRequiresUnlockedDevice = getBoolFromAsn1(taggedEntry.getObject());
+ break;
+ default:
+ break;
+ }
+ }
+
+ ASN1Encodable[] hardwareEnforced = ((ASN1Sequence)
+ keyAttributes.getObjectAt(HW_ENFORCED_INDEX)).toArray();
+ for (ASN1Encodable entry : hardwareEnforced) {
+ ASN1TaggedObject taggedEntry = (ASN1TaggedObject) entry;
+ switch (taggedEntry.getTagNo()) {
+ case KM_TAG_NO_AUTH_REQUIRED:
+ this.mKeyAuthenticatorType = HW_AUTH_NONE;
+ break;
+ case KM_TAG_ALL_APPLICATIONS:
+ this.mKeyAllowedForAllApplications = true;
+ break;
+ case KM_TAG_ROOT_OF_TRUST:
+ ASN1Sequence rootOfTrust = (ASN1Sequence) taggedEntry.getObject();
+ this.mVerifiedBootKey =
+ getOctetsFromAsn1(rootOfTrust.getObjectAt(VERIFIED_BOOT_KEY_INDEX));
+ this.mVerifiedBootLocked =
+ getBoolFromAsn1(rootOfTrust.getObjectAt(VERIFIED_BOOT_LOCKED_INDEX));
+ this.mVerifiedBootState =
+ getVerifiedBootStateEnum(
+ rootOfTrust.getObjectAt(VERIFIED_BOOT_STATE_INDEX));
+ // The verified boot hash was added in structure version 3 (Keymaster 4.0).
+ if (mAttestationVersion >= 3) {
+ this.mVerifiedBootHash =
+ getOctetsFromAsn1(
+ rootOfTrust.getObjectAt(VERIFIED_BOOT_HASH_INDEX));
+ }
+ break;
+ case KM_TAG_OS_VERSION:
+ this.mKeyOsVersion = getIntegerFromAsn1(taggedEntry.getObject());
+ break;
+ case KM_TAG_OS_PATCHLEVEL:
+ this.mKeyOsPatchLevel = getIntegerFromAsn1(taggedEntry.getObject());
+ break;
+ case KM_TAG_ATTESTATION_ID_BRAND:
+ this.mDeviceBrand = getUtf8FromOctetsFromAsn1(taggedEntry.getObject());
+ break;
+ case KM_TAG_ATTESTATION_ID_DEVICE:
+ this.mDeviceName = getUtf8FromOctetsFromAsn1(taggedEntry.getObject());
+ break;
+ case KM_TAG_ATTESTATION_ID_PRODUCT:
+ this.mDeviceProductName = getUtf8FromOctetsFromAsn1(taggedEntry.getObject());
+ break;
+ case KM_TAG_VENDOR_PATCHLEVEL:
+ this.mKeyVendorPatchLevel = getIntegerFromAsn1(taggedEntry.getObject());
+ break;
+ case KM_TAG_BOOT_PATCHLEVEL:
+ this.mKeyBootPatchLevel = getIntegerFromAsn1(taggedEntry.getObject());
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ private void parseAttestationApplicationId(byte [] attestationApplicationId)
+ throws Exception {
+ ASN1Sequence outerSequence = ASN1Sequence.getInstance(
+ new ASN1InputStream(attestationApplicationId).readObject());
+ Map<String, Long> packageNameVersion = new HashMap<>();
+ ASN1Set packageInfoSet = (ASN1Set) outerSequence.getObjectAt(PACKAGE_INFO_SET_INDEX);
+ for (ASN1Encodable packageInfoEntry : packageInfoSet.toArray()) {
+ ASN1Sequence packageInfoSequence = (ASN1Sequence) packageInfoEntry;
+ packageNameVersion.put(
+ getUtf8FromOctetsFromAsn1(
+ packageInfoSequence.getObjectAt(PACKAGE_INFO_NAME_INDEX)),
+ getLongFromAsn1(packageInfoSequence.getObjectAt(PACKAGE_INFO_VERSION_INDEX)));
+ }
+ List<ByteString> certificateDigests = new ArrayList<>();
+ ASN1Set certificateDigestSet =
+ (ASN1Set) outerSequence.getObjectAt(PACKAGE_SIGNATURE_SET_INDEX);
+ for (ASN1Encodable certificateDigestEntry : certificateDigestSet.toArray()) {
+ certificateDigests.add(getOctetsFromAsn1(certificateDigestEntry));
+ }
+ this.mApplicationPackageNameVersion = Collections.unmodifiableMap(packageNameVersion);
+ this.mApplicationCertificateDigests = Collections.unmodifiableList(certificateDigests);
+
+ }
+
+ private VerifiedBootState getVerifiedBootStateEnum(ASN1Encodable asn1) {
+ int verifiedBoot = getEnumFromAsn1(asn1);
+ switch (verifiedBoot) {
+ case KM_VERIFIED_BOOT_STATE_VERIFIED:
+ return VerifiedBootState.VERIFIED;
+ case KM_VERIFIED_BOOT_STATE_SELF_SIGNED:
+ return VerifiedBootState.SELF_SIGNED;
+ case KM_VERIFIED_BOOT_STATE_UNVERIFIED:
+ return VerifiedBootState.UNVERIFIED;
+ case KM_VERIFIED_BOOT_STATE_FAILED:
+ return VerifiedBootState.FAILED;
+ default:
+ throw new IllegalArgumentException("Invalid verified boot state.");
+ }
+ }
+
+ private SecurityLevel getSecurityLevelEnum(ASN1Encodable asn1) {
+ int securityLevel = getEnumFromAsn1(asn1);
+ switch (securityLevel) {
+ case KM_SECURITY_LEVEL_SOFTWARE:
+ return SecurityLevel.SOFTWARE;
+ case KM_SECURITY_LEVEL_TRUSTED_ENVIRONMENT:
+ return SecurityLevel.TRUSTED_ENVIRONMENT;
+ case KM_SECURITY_LEVEL_STRONG_BOX:
+ return SecurityLevel.STRONG_BOX;
+ default:
+ throw new IllegalArgumentException("Invalid security level.");
+ }
+ }
+
+ @NonNull
+ private ByteString getOctetsFromAsn1(ASN1Encodable asn1) {
+ return ByteString.copyFrom(((ASN1OctetString) asn1).getOctets());
+ }
+
+ @NonNull
+ private String getUtf8FromOctetsFromAsn1(ASN1Encodable asn1) {
+ return new String(((ASN1OctetString) asn1).getOctets(), StandardCharsets.UTF_8);
+ }
+
+ @NonNull
+ private int getIntegerFromAsn1(ASN1Encodable asn1) {
+ return ((ASN1Integer) asn1).getValue().intValueExact();
+ }
+
+ @NonNull
+ private long getLongFromAsn1(ASN1Encodable asn1) {
+ return ((ASN1Integer) asn1).getValue().longValueExact();
+ }
+
+ @NonNull
+ private int getEnumFromAsn1(ASN1Encodable asn1) {
+ return ((ASN1Enumerated) asn1).getValue().intValueExact();
+ }
+
+ @Nullable
+ private Boolean getBoolFromAsn1(ASN1Encodable asn1) {
+ if (asn1 instanceof ASN1Boolean) {
+ return ((ASN1Boolean) asn1).isTrue();
+ }
+ return null;
+ }
+}
diff --git a/services/core/java/com/android/server/security/AttestationVerificationManagerService.java b/services/core/java/com/android/server/security/AttestationVerificationManagerService.java
index 243efb5..863f2d1 100644
--- a/services/core/java/com/android/server/security/AttestationVerificationManagerService.java
+++ b/services/core/java/com/android/server/security/AttestationVerificationManagerService.java
@@ -16,6 +16,7 @@
package com.android.server.security;
+import static android.security.attestationverification.AttestationVerificationManager.PROFILE_PEER_DEVICE;
import static android.security.attestationverification.AttestationVerificationManager.PROFILE_SELF_TRUSTED;
import static android.security.attestationverification.AttestationVerificationManager.RESULT_FAILURE;
import static android.security.attestationverification.AttestationVerificationManager.RESULT_UNKNOWN;
@@ -44,9 +45,11 @@
public class AttestationVerificationManagerService extends SystemService {
private static final String TAG = "AVF";
+ private final AttestationVerificationPeerDeviceVerifier mPeerDeviceVerifier;
- public AttestationVerificationManagerService(final Context context) {
+ public AttestationVerificationManagerService(final Context context) throws Exception {
super(context);
+ mPeerDeviceVerifier = new AttestationVerificationPeerDeviceVerifier(context);
}
private final IBinder mService = new IAttestationVerificationManagerService.Stub() {
@@ -83,7 +86,7 @@
result.token = null;
switch (profile.getAttestationProfileId()) {
case PROFILE_SELF_TRUSTED:
- Slog.d(TAG, "Verifying Self trusted profile.");
+ Slog.d(TAG, "Verifying Self Trusted profile.");
try {
result.resultCode =
AttestationVerificationSelfTrustedVerifierForTesting.getInstance()
@@ -92,6 +95,11 @@
result.resultCode = RESULT_FAILURE;
}
break;
+ case PROFILE_PEER_DEVICE:
+ Slog.d(TAG, "Verifying Peer Device profile.");
+ result.resultCode = mPeerDeviceVerifier.verifyAttestation(
+ localBindingType, requirements, attestation);
+ break;
default:
Slog.d(TAG, "No profile found, defaulting.");
result.resultCode = RESULT_UNKNOWN;
diff --git a/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java
new file mode 100644
index 0000000..0f8be5a
--- /dev/null
+++ b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java
@@ -0,0 +1,510 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.security;
+
+import static android.security.attestationverification.AttestationVerificationManager.PARAM_CHALLENGE;
+import static android.security.attestationverification.AttestationVerificationManager.PARAM_PUBLIC_KEY;
+import static android.security.attestationverification.AttestationVerificationManager.RESULT_FAILURE;
+import static android.security.attestationverification.AttestationVerificationManager.RESULT_SUCCESS;
+import static android.security.attestationverification.AttestationVerificationManager.TYPE_CHALLENGE;
+import static android.security.attestationverification.AttestationVerificationManager.TYPE_PUBLIC_KEY;
+
+import static com.android.server.security.AndroidKeystoreAttestationVerificationAttributes.VerifiedBootState.VERIFIED;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.json.JSONObject;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.net.URL;
+import java.security.cert.CertPath;
+import java.security.cert.CertPathValidator;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.PKIXCertPathChecker;
+import java.security.cert.PKIXParameters;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509Certificate;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Verifies Android key attestation according to the {@code PROFILE_PEER_DEVICE} profile.
+ *
+ * Trust anchors are vendor-defined via the vendor_required_attestation_certificates.xml resource.
+ * The profile is satisfied by checking all the following:
+ * * TrustAnchor match
+ * * Certificate validity
+ * * Android OS 10 or higher
+ * * Hardware backed key store
+ * * Verified boot locked
+ * * Remote Patch level must be within 1 year of local patch if local patch is less than 1 year old.
+ *
+ */
+class AttestationVerificationPeerDeviceVerifier {
+ private static final String TAG = "AVF";
+ private static final boolean DEBUG = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.VERBOSE);
+ private static final int MAX_PATCH_AGE_MONTHS = 12;
+
+ private final Context mContext;
+ private final Set<TrustAnchor> mTrustAnchors;
+ private final boolean mRevocationEnabled;
+ private final LocalDate mTestSystemDate;
+ private final LocalDate mTestLocalPatchDate;
+ private CertificateFactory mCertificateFactory;
+ private CertPathValidator mCertPathValidator;
+
+ private static void debugVerboseLog(String str, Throwable t) {
+ if (DEBUG) {
+ Slog.v(TAG, str, t);
+ }
+ }
+
+ private static void debugVerboseLog(String str) {
+ if (DEBUG) {
+ Slog.v(TAG, str);
+ }
+ }
+
+ AttestationVerificationPeerDeviceVerifier(@NonNull Context context) throws Exception {
+ mContext = Objects.requireNonNull(context);
+ mCertificateFactory = CertificateFactory.getInstance("X.509");
+ mCertPathValidator = CertPathValidator.getInstance("PKIX");
+ mTrustAnchors = getTrustAnchors();
+ mRevocationEnabled = true;
+ mTestSystemDate = null;
+ mTestLocalPatchDate = null;
+ }
+
+ // Use ONLY for hermetic unit testing.
+ @VisibleForTesting
+ AttestationVerificationPeerDeviceVerifier(@NonNull Context context,
+ Set<TrustAnchor> trustAnchors, boolean revocationEnabled,
+ LocalDate systemDate, LocalDate localPatchDate) throws Exception {
+ mContext = Objects.requireNonNull(context);
+ mCertificateFactory = CertificateFactory.getInstance("X.509");
+ mCertPathValidator = CertPathValidator.getInstance("PKIX");
+ mTrustAnchors = trustAnchors;
+ mRevocationEnabled = revocationEnabled;
+ mTestSystemDate = systemDate;
+ mTestLocalPatchDate = localPatchDate;
+ }
+
+ /**
+ * Verifies attestation for public key or challenge local binding.
+ *
+ * The attestations must be suitable for {@link java.security.cert.CertificateFactory}
+ * The certificates in the attestation provided must be DER-encoded and may be supplied in
+ * binary or printable (Base64) encoding. If the certificate is provided in Base64 encoding,
+ * it must be bounded at the beginning by -----BEGIN CERTIFICATE-----, and must be bounded at
+ * the end by -----END CERTIFICATE-----.
+ *
+ * @param localBindingType Only {@code TYPE_PUBLIC_KEY} and {@code TYPE_CHALLENGE} supported.
+ * @param requirements Only {@code PARAM_PUBLIC_KEY} and {@code PARAM_CHALLENGE} supported.
+ * @param attestation Certificates should be DER encoded with leaf certificate appended first.
+ */
+ int verifyAttestation(
+ int localBindingType, @NonNull Bundle requirements, @NonNull byte[] attestation) {
+ int status = RESULT_FAILURE;
+
+ if (mCertificateFactory == null) {
+ debugVerboseLog("Was unable to initialize CertificateFactory onCreate.");
+ return status;
+ }
+
+ if (mCertPathValidator == null) {
+ debugVerboseLog("Was unable to initialize CertPathValidator onCreate.");
+ return status;
+ }
+
+ List<X509Certificate> certificates;
+ try {
+ certificates = getCertificates(attestation);
+ } catch (CertificateException e) {
+ debugVerboseLog("Unable to parse attestation certificates.", e);
+ return status;
+ }
+
+ if (certificates.isEmpty()) {
+ debugVerboseLog("Attestation contains no certificates.");
+ return status;
+ }
+
+ X509Certificate leafNode = certificates.get(0);
+ if (validateRequirements(localBindingType, requirements)
+ && validateCertificateChain(certificates)
+ && checkCertificateAttributes(leafNode, localBindingType, requirements)) {
+ status = RESULT_SUCCESS;
+ } else {
+ status = RESULT_FAILURE;
+ }
+ return status;
+ }
+
+ @NonNull
+ private List<X509Certificate> getCertificates(byte[] attestation)
+ throws CertificateException {
+ List<X509Certificate> certificates = new ArrayList<>();
+ ByteArrayInputStream bis = new ByteArrayInputStream(attestation);
+ while (bis.available() > 0) {
+ certificates.add((X509Certificate) mCertificateFactory.generateCertificate(bis));
+ }
+
+ return certificates;
+ }
+
+ private boolean validateRequirements(int localBindingType, Bundle requirements) {
+ if (requirements.size() != 1) {
+ debugVerboseLog("Requirements does not contain exactly 1 key.");
+ return false;
+ }
+
+ if (localBindingType != TYPE_PUBLIC_KEY && localBindingType != TYPE_CHALLENGE) {
+ debugVerboseLog("Binding type is not supported: " + localBindingType);
+ return false;
+ }
+
+ if (localBindingType == TYPE_PUBLIC_KEY && !requirements.containsKey(PARAM_PUBLIC_KEY)) {
+ debugVerboseLog("Requirements does not contain key: " + PARAM_PUBLIC_KEY);
+ return false;
+ }
+
+ if (localBindingType == TYPE_CHALLENGE && !requirements.containsKey(PARAM_CHALLENGE)) {
+ debugVerboseLog("Requirements does not contain key: " + PARAM_CHALLENGE);
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean validateCertificateChain(List<X509Certificate> certificates) {
+ if (certificates.size() < 2) {
+ debugVerboseLog("Certificate chain less than 2 in size.");
+ return false;
+ }
+
+ try {
+ CertPath certificatePath = mCertificateFactory.generateCertPath(certificates);
+ PKIXParameters validationParams = new PKIXParameters(mTrustAnchors);
+ if (mRevocationEnabled) {
+ // Checks Revocation Status List based on
+ // https://developer.android.com/training/articles/security-key-attestation#certificate_status
+ PKIXCertPathChecker checker = new AndroidRevocationStatusListChecker();
+ validationParams.addCertPathChecker(checker);
+ }
+ // Do not use built-in revocation status checker.
+ validationParams.setRevocationEnabled(false);
+ mCertPathValidator.validate(certificatePath, validationParams);
+ } catch (Throwable t) {
+ debugVerboseLog("Invalid certificate chain.", t);
+ return false;
+ }
+ return true;
+ }
+
+ private Set<TrustAnchor> getTrustAnchors() throws CertPathValidatorException {
+ Set<TrustAnchor> modifiableSet = new HashSet<>();
+ try {
+ for (String certString: getTrustAnchorResources()) {
+ modifiableSet.add(
+ new TrustAnchor((X509Certificate) mCertificateFactory.generateCertificate(
+ new ByteArrayInputStream(getCertificateBytes(certString))), null));
+ }
+ } catch (CertificateException e) {
+ e.printStackTrace();
+ throw new CertPathValidatorException("Invalid trust anchor certificate.", e);
+ }
+ return Collections.unmodifiableSet(modifiableSet);
+ }
+
+ private byte[] getCertificateBytes(String certString) {
+ String formattedCertString = certString.replaceAll("\\s+", "\n");
+ formattedCertString = formattedCertString.replaceAll(
+ "-BEGIN\\nCERTIFICATE-", "-BEGIN CERTIFICATE-");
+ formattedCertString = formattedCertString.replaceAll(
+ "-END\\nCERTIFICATE-", "-END CERTIFICATE-");
+ return formattedCertString.getBytes(UTF_8);
+ }
+
+ private String[] getTrustAnchorResources() {
+ return mContext.getResources().getStringArray(
+ R.array.vendor_required_attestation_certificates);
+ }
+
+ private boolean checkCertificateAttributes(
+ X509Certificate leafCertificate, int localBindingType, Bundle requirements) {
+ AndroidKeystoreAttestationVerificationAttributes attestationAttributes;
+ try {
+ attestationAttributes =
+ AndroidKeystoreAttestationVerificationAttributes.fromCertificate(
+ leafCertificate);
+ } catch (Throwable t) {
+ debugVerboseLog("Could not get ParsedAttestationAttributes from Certificate.", t);
+ return false;
+ }
+
+ // Checks for support of Keymaster 4.
+ if (attestationAttributes.getAttestationVersion() < 3) {
+ debugVerboseLog("Attestation version is not at least 3 (Keymaster 4).");
+ return false;
+ }
+
+ // Checks for support of Keymaster 4.
+ if (attestationAttributes.getKeymasterVersion() < 4) {
+ debugVerboseLog("Keymaster version is not at least 4.");
+ return false;
+ }
+
+ // First two characters are Android OS version.
+ if (attestationAttributes.getKeyOsVersion() < 100000) {
+ debugVerboseLog("Android OS version is not 10+.");
+ return false;
+ }
+
+ if (!attestationAttributes.isAttestationHardwareBacked()) {
+ debugVerboseLog("Key is not HW backed.");
+ return false;
+ }
+
+ if (!attestationAttributes.isKeymasterHardwareBacked()) {
+ debugVerboseLog("Keymaster is not HW backed.");
+ return false;
+ }
+
+ if (attestationAttributes.getVerifiedBootState() != VERIFIED) {
+ debugVerboseLog("Boot state not Verified.");
+ return false;
+ }
+
+ try {
+ if (!attestationAttributes.isVerifiedBootLocked()) {
+ debugVerboseLog("Verified boot state is not locked.");
+ return false;
+ }
+ } catch (IllegalStateException e) {
+ debugVerboseLog("VerifiedBootLocked is not set.", e);
+ return false;
+ }
+
+ // Patch level integer YYYYMM is expected to be within 1 year of today.
+ if (!isValidPatchLevel(attestationAttributes.getKeyOsPatchLevel())) {
+ debugVerboseLog("OS patch level is not within valid range.");
+ return false;
+ }
+
+ // Patch level integer YYYYMMDD is expected to be within 1 year of today.
+ if (!isValidPatchLevel(attestationAttributes.getKeyBootPatchLevel())) {
+ debugVerboseLog("Boot patch level is not within valid range.");
+ return false;
+ }
+
+ if (!isValidPatchLevel(attestationAttributes.getKeyVendorPatchLevel())) {
+ debugVerboseLog("Vendor patch level is not within valid range.");
+ return false;
+ }
+
+ if (!isValidPatchLevel(attestationAttributes.getKeyBootPatchLevel())) {
+ debugVerboseLog("Boot patch level is not within valid range.");
+ return false;
+ }
+
+ // Verify leaf public key matches provided public key.
+ if (localBindingType == TYPE_PUBLIC_KEY
+ && !Arrays.equals(requirements.getByteArray(PARAM_PUBLIC_KEY),
+ leafCertificate.getPublicKey().getEncoded())) {
+ debugVerboseLog("Provided public key does not match leaf certificate public key.");
+ return false;
+ }
+
+ // Verify challenge matches provided challenge.
+ if (localBindingType == TYPE_CHALLENGE
+ && !Arrays.equals(requirements.getByteArray(PARAM_CHALLENGE),
+ attestationAttributes.getAttestationChallenge().toByteArray())) {
+ debugVerboseLog("Provided challenge does not match leaf certificate challenge.");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Validates patchLevel passed is within range of the local device patch date if local patch is
+ * not over one year old. Since the time can be changed on device, just checking the patch date
+ * is not enough. Therefore, we also confirm the patch level for the remote and local device are
+ * similar.
+ */
+ private boolean isValidPatchLevel(int patchLevel) {
+ LocalDate currentDate = mTestSystemDate != null
+ ? mTestSystemDate : LocalDate.now(ZoneId.systemDefault());
+
+ // Convert local patch date to LocalDate.
+ LocalDate localPatchDate;
+ try {
+ if (mTestLocalPatchDate != null) {
+ localPatchDate = mTestLocalPatchDate;
+ } else {
+ localPatchDate = LocalDate.parse(Build.VERSION.SECURITY_PATCH);
+ }
+ } catch (Throwable t) {
+ debugVerboseLog("Build.VERSION.SECURITY_PATCH: "
+ + Build.VERSION.SECURITY_PATCH + " is not in format YYYY-MM-DD");
+ return false;
+ }
+
+ // Check local patch date is not in last year of system clock.
+ if (ChronoUnit.MONTHS.between(localPatchDate, currentDate) > MAX_PATCH_AGE_MONTHS) {
+ return true;
+ }
+
+ // Convert remote patch dates to LocalDate.
+ String remoteDeviceDateStr = String.valueOf(patchLevel);
+ if (remoteDeviceDateStr.length() != 6 && remoteDeviceDateStr.length() != 8) {
+ debugVerboseLog("Patch level is not in format YYYYMM or YYYYMMDD");
+ return false;
+ }
+
+ int patchYear = Integer.parseInt(remoteDeviceDateStr.substring(0, 4));
+ int patchMonth = Integer.parseInt(remoteDeviceDateStr.substring(4, 6));
+ LocalDate remotePatchDate = LocalDate.of(patchYear, patchMonth, 1);
+
+ // Check patch dates are within 1 year of each other
+ boolean IsRemotePatchWithinOneYearOfLocalPatch;
+ if (remotePatchDate.compareTo(localPatchDate) > 0) {
+ IsRemotePatchWithinOneYearOfLocalPatch = ChronoUnit.MONTHS.between(
+ localPatchDate, remotePatchDate) <= MAX_PATCH_AGE_MONTHS;
+ } else if (remotePatchDate.compareTo(localPatchDate) < 0) {
+ IsRemotePatchWithinOneYearOfLocalPatch = ChronoUnit.MONTHS.between(
+ remotePatchDate, localPatchDate) <= MAX_PATCH_AGE_MONTHS;
+ } else {
+ IsRemotePatchWithinOneYearOfLocalPatch = true;
+ }
+
+ return IsRemotePatchWithinOneYearOfLocalPatch;
+ }
+
+ /**
+ * Checks certificate revocation status.
+ *
+ * Queries status list from android.googleapis.com/attestation/status and checks for
+ * the existence of certificate's serial number. If serial number exists in map, then fail.
+ */
+ private final class AndroidRevocationStatusListChecker extends PKIXCertPathChecker {
+ private static final String TOP_LEVEL_JSON_PROPERTY_KEY = "entries";
+ private static final String STATUS_PROPERTY_KEY = "status";
+ private static final String REASON_PROPERTY_KEY = "reason";
+ private String mStatusUrl;
+ private JSONObject mJsonStatusMap;
+
+ @Override
+ public void init(boolean forward) throws CertPathValidatorException {
+ mStatusUrl = getRevocationListUrl();
+ if (mStatusUrl == null || mStatusUrl.isEmpty()) {
+ throw new CertPathValidatorException(
+ "R.string.vendor_required_attestation_revocation_list_url is empty.");
+ }
+ // TODO(b/221067843): Update to only pull status map on non critical path and if
+ // out of date (24hrs).
+ mJsonStatusMap = getStatusMap(mStatusUrl);
+ }
+
+ @Override
+ public boolean isForwardCheckingSupported() {
+ return false;
+ }
+
+ @Override
+ public Set<String> getSupportedExtensions() {
+ return null;
+ }
+
+ @Override
+ public void check(Certificate cert, Collection<String> unresolvedCritExts)
+ throws CertPathValidatorException {
+ X509Certificate x509Certificate = (X509Certificate) cert;
+ // The json key is the certificate's serial number converted to lowercase hex.
+ String serialNumber = x509Certificate.getSerialNumber().toString(16);
+
+ if (serialNumber == null) {
+ throw new CertPathValidatorException("Certificate serial number can not be null.");
+ }
+
+ if (mJsonStatusMap.has(serialNumber)) {
+ JSONObject revocationStatus;
+ String status;
+ String reason;
+ try {
+ revocationStatus = mJsonStatusMap.getJSONObject(serialNumber);
+ status = revocationStatus.getString(STATUS_PROPERTY_KEY);
+ reason = revocationStatus.getString(REASON_PROPERTY_KEY);
+ } catch (Throwable t) {
+ throw new CertPathValidatorException("Unable get properties for certificate "
+ + "with serial number " + serialNumber);
+ }
+ throw new CertPathValidatorException(
+ "Invalid certificate with serial number " + serialNumber
+ + " has status " + status
+ + " because reason " + reason);
+ }
+ }
+
+ private JSONObject getStatusMap(String stringUrl) throws CertPathValidatorException {
+ URL url;
+ try {
+ url = new URL(stringUrl);
+ } catch (Throwable t) {
+ throw new CertPathValidatorException(
+ "Unable to get revocation status from " + mStatusUrl, t);
+ }
+
+ try (InputStream inputStream = url.openStream()) {
+ JSONObject statusListJson = new JSONObject(
+ new String(inputStream.readAllBytes(), UTF_8));
+ return statusListJson.getJSONObject(TOP_LEVEL_JSON_PROPERTY_KEY);
+ } catch (Throwable t) {
+ throw new CertPathValidatorException(
+ "Unable to parse revocation status from " + mStatusUrl, t);
+ }
+ }
+
+ private String getRevocationListUrl() {
+ return mContext.getResources().getString(
+ R.string.vendor_required_attestation_revocation_list_url);
+ }
+ }
+}
diff --git a/tests/AttestationVerificationTest/Android.bp b/tests/AttestationVerificationTest/Android.bp
index a4741eed..b98f8cb 100644
--- a/tests/AttestationVerificationTest/Android.bp
+++ b/tests/AttestationVerificationTest/Android.bp
@@ -40,5 +40,6 @@
"androidx.test.rules",
"androidx.test.ext.junit",
"platform-test-annotations",
+ "services.core",
],
}
diff --git a/tests/AttestationVerificationTest/AndroidManifest.xml b/tests/AttestationVerificationTest/AndroidManifest.xml
index c42bde9..37321ad8 100755
--- a/tests/AttestationVerificationTest/AndroidManifest.xml
+++ b/tests/AttestationVerificationTest/AndroidManifest.xml
@@ -24,6 +24,7 @@
<application>
<uses-library android:name="android.test.runner"/>
<activity android:name=".SystemAttestationVerificationTest$TestActivity" />
+ <activity android:name=".PeerDeviceSystemAttestationVerificationTest$TestActivity" />
</application>
<!-- self-instrumenting test package. -->
diff --git a/tests/AttestationVerificationTest/assets/test_attestation_with_root_certs.pem b/tests/AttestationVerificationTest/assets/test_attestation_with_root_certs.pem
new file mode 100644
index 0000000..e29ff48
--- /dev/null
+++ b/tests/AttestationVerificationTest/assets/test_attestation_with_root_certs.pem
@@ -0,0 +1,81 @@
+-----BEGIN CERTIFICATE-----
+MIICkjCCAjmgAwIBAgIBATAKBggqhkjOPQQDAjA5MQwwCgYDVQQMDANURUUxKTAn
+BgNVBAUTIDg2ZTQ0MjRhMjY2NDlhZDcyZWZhNWM0MWEwM2IyN2QxMCAXDTcwMDEw
+MTAwMDAwMFoYDzIxMDYwMjA3MDYyODE1WjAfMR0wGwYDVQQDDBRBbmRyb2lkIEtl
+eXN0b3JlIEtleTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIlTwcvhe+DLV45X
+RCTO7HoN20Ib7IbCEhV5+YdMiYOp/0AdKk8oYvsri1XODeC4zcoPfHNdQGt/68i0
+ADbilJmjggFIMIIBRDAOBgNVHQ8BAf8EBAMCB4AwggEwBgorBgEEAdZ5AgERBIIB
+IDCCARwCAQMKAQECAQQKAQEECXBsYXllcjQ1NgQAMFe/hT0IAgYBfvkgVei/hUVH
+BEUwQzEdMBsEFmNvbS5nb29nbGUuYXR0ZXN0YXRpb24CAQExIgQgOqyVXRJUdAGY
+/XVx8y/uRPiebqlyELt1EpqIz29h5tUwgaehCDEGAgECAgEDogMCAQOjBAICAQCl
+CDEGAgEEAgEGqgMCAQG/g3cCBQC/hT4DAgEAv4VATDBKBCCEZx8qY8Ys0HC2TqPq
+74eYPzh5L/agxD7Bn7zVBQHoNAEB/woBAAQguJwoDfWBjRaedzQ6TJPFJJKs+ytr
++8Vu2CSmqifFBHW/hUEFAgMB1MC/hUIFAgMDFdm/hU4GAgQBNIjJv4VPBgIEATSI
+yTAKBggqhkjOPQQDAgNHADBEAiBdGxfMEx59k5+zo+hV3Q9kgjbGi0zU3WH355P5
+JZttBwIgY4FZsSreUJL8RY3JvfvD8BRw8GuXcB1OQ600hwaYYC4=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIB8zCCAXqgAwIBAgIRAOuuukN0OHbNQvKngECkewEwCgYIKoZIzj0EAwIwOTEM
+MAoGA1UEDAwDVEVFMSkwJwYDVQQFEyA3MDkxMmRmNDYxMDRmYWFlOTQ3ODY0ZTU4
+MDRmMWY4ZDAeFw0yMDA5MjgyMDI3NTZaFw0zMDA5MjYyMDI3NTZaMDkxDDAKBgNV
+BAwMA1RFRTEpMCcGA1UEBRMgODZlNDQyNGEyNjY0OWFkNzJlZmE1YzQxYTAzYjI3
+ZDEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAT3Mjl05ewv6G8zAR4fXJy2iadU
+yK7rNvzlECy2+nhEieL8BFXDvo0tx5fYs8qr67j/KvluFBfp2r9s+ckWz3Kzo2Mw
+YTAdBgNVHQ4EFgQUsVKBzAs1lMXAauQ3mGAJZJqK5tAwHwYDVR0jBBgwFoAUEsQA
+i8d2oLULSi5Ix4BTGGbvUEkwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
+AgQwCgYIKoZIzj0EAwIDZwAwZAIwfFziBCwuM1thLUSUNI61Xx/vnDnNkSv/aX5D
+yLjxbLlgnFSzIrc+6vf6h6L/+TjYAjAq6h9GKtMn4R0286MoqYqzp/rHn6JD2sqH
+iM8KZ0oA+Ut242EcmGjAoNfGZGZGddQ=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDkzCCAXugAwIBAgIQNTAX5z3CBac6nD3hQiMDcDANBgkqhkiG9w0BAQsFADAb
+MRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MB4XDTIwMDkyODIwMjUwMloXDTMw
+MDkyNjIwMjUwMlowOTEMMAoGA1UEDAwDVEVFMSkwJwYDVQQFEyA3MDkxMmRmNDYx
+MDRmYWFlOTQ3ODY0ZTU4MDRmMWY4ZDB2MBAGByqGSM49AgEGBSuBBAAiA2IABA/7
+xZFlFtTjdy2B3p7E+FsrBjyhBSqY4a9FywawXMJRSja3HAK36ruzJjWlEkD+D0vq
+HI2joY39FHmWoZWwm2cq9gOleFGYOSCpMr4ib7xtq/6nefvKTP5rutxudF97t6Nj
+MGEwHQYDVR0OBBYEFBLEAIvHdqC1C0ouSMeAUxhm71BJMB8GA1UdIwQYMBaAFDZh
+4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD
+AgIEMA0GCSqGSIb3DQEBCwUAA4ICAQAaMONDQxJz3PRn9gHQW5KP+TIoBPJZyGa1
+QFuEBcMDTtIxBxEh5Pj3ivPBc76PrdYu5U47Ve5YYCPsTpUTj7dOxbzGSZjfjvHF
+fNwy24g1Lah2iAdQRVErhWKBlpnQhBnnRrrNmTTmzhl8NvSExqAPP746dqwm1kQ7
+YesC5yoEAHpxamhlZpIKAjSxSZeHWace2qV00M8qWd/7lIpqttJjFFrhCjzR0dtr
+oIIpC5EtmqIWdLeg6yZjJkX+Cjv4F8mRfBtwuNuxFsfALQ3D5l8WKw3iwPebmCy1
+kEby8Eoq88FxzXQp/XgAaljlrKXyuxptrc1noRuob4g42Oh6wetueYRSCtO6Bkym
+0UMnld/kG77aeiHOMVVb86wrhNuAGir1vgDGOBsclITVyuu9ka0YVQjjDm3phTpd
+O8JV16gbei2Phn+FfRV1MSDsZo/wu0i2KVzgs27bfJocMHXv+GzvwfefYgMJ/rYq
+Bg27lpsWzmFEPv2cyhA5PwwbG8ceswa3RZE/2eS9o7STkz93jr/KsKLcMBY6cX2C
+q4CBJByKFJtVANOVj+neFNxc2sQgeTT33yYNKbe4b5bm7Ki1FbrhFVckpzUGDnKs
+gL+AxvALWOoryDGwNbJiW8PRiD3HHByiMvSEQ7e7BSc2KjbsaWbCfYZAMZJEhEsc
+P1l8lcUVuA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFHDCCAwSgAwIBAgIJANUP8luj8tazMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
+BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTkxMTIyMjAzNzU4WhcNMzQxMTE4MjAz
+NzU4WjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
+Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
+tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
+nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
+C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
+oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
+JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
+sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
+igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
+RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
+aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
+AGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1Ud
+IwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYD
+VR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQBOMaBc8oumXb2voc7XCWnu
+XKhBBK3e2KMGz39t7lA3XXRe2ZLLAkLM5y3J7tURkf5a1SutfdOyXAmeE6SRo83U
+h6WszodmMkxK5GM4JGrnt4pBisu5igXEydaW7qq2CdC6DOGjG+mEkN8/TA6p3cno
+L/sPyz6evdjLlSeJ8rFBH6xWyIZCbrcpYEJzXaUOEaxxXxgYz5/cTiVKN2M1G2ok
+QBUIYSY6bjEL4aUN5cfo7ogP3UvliEo3Eo0YgwuzR2v0KR6C1cZqZJSTnghIC/vA
+D32KdNQ+c3N+vl2OTsUVMC1GiWkngNx1OO1+kXW+YTnnTUOtOIswUP/Vqd5SYgAI
+mMAfY8U9/iIgkQj6T2W6FsScy94IN9fFhE1UtzmLoBIuUFsVXJMTz+Jucth+IqoW
+Fua9v1R93/k98p41pjtFX+H8DslVgfP097vju4KDlqN64xV1grw3ZLl4CiOe/A91
+oeLm2UHOq6wn3esB4r2EIQKb6jTVGu5sYCcdWpXr0AUVqcABPdgL+H7qJguBw09o
+jm6xNIrw2OocrDKsudk/okr/AwqEyPKw9WnMlQgLIKw1rODG2NvU9oR3GVGdMkUB
+ZutL8VuFkERQGt6vQ2OCw0sV47VMkuYbacK/xyZFiRcrPJPb41zgbQj9XAEyLKCH
+ex0SdDrx+tWUDqG8At2JHA==
+-----END CERTIFICATE-----
diff --git a/tests/AttestationVerificationTest/assets/test_attestation_wrong_root_certs.pem b/tests/AttestationVerificationTest/assets/test_attestation_wrong_root_certs.pem
new file mode 100644
index 0000000..3d6410a
--- /dev/null
+++ b/tests/AttestationVerificationTest/assets/test_attestation_wrong_root_certs.pem
@@ -0,0 +1,30 @@
+-----BEGIN CERTIFICATE-----
+MIIGCDCCBHCgAwIBAgIBATANBgkqhkiG9w0BAQsFADApMRkwFwYDVQQFExAyZGM1OGIyZDFhMjQx
+MzI2MQwwCgYDVQQMDANURUUwIBcNNzAwMTAxMDAwMDAwWhgPMjEwNjAyMDcwNjI4MTVaMB8xHTAb
+BgNVBAMMFEFuZHJvaWQgS2V5c3RvcmUgS2V5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEApNVcnyN40MANMbbo2nMGNq2NNysDSjfLm0W3i6wPKf0ffCYkhWM4dCmQKKf50uAZTBeTit4c
+NwXeZn3qellMlOsIN3Qc384rfN/8cikrRvUAgibz0Jy7STykjwa7x6tKwqITxbO8HqAhKo8/BQXU
+xzrOdIg5ciy+UM7Vgh7a7ogen0KL2iGgrsalb1ti7Vlzb6vIJ4WzIC3TGD2sCkoPahghwqFDZZCo
+/FzaLoNY0jAUX2mL+kf8aUaoxz7xA9FTvgara+1pLBR1s4c8xPS2HdZipcVXWfey0wujv1VAKs4+
+tXjKlHkYBHBBceEjxUtEmrapSQEdpHPv7Xh9Uanq4QIDAQABo4ICwTCCAr0wDgYDVR0PAQH/BAQD
+AgeAMIICqQYKKwYBBAHWeQIBEQSCApkwggKVAgEDCgEBAgEECgEBBANhYmMEADCCAc2/hT0IAgYB
+ZOYGEYe/hUWCAbsEggG3MIIBszGCAYswDAQHYW5kcm9pZAIBHTAZBBRjb20uYW5kcm9pZC5rZXlj
+aGFpbgIBHTAZBBRjb20uYW5kcm9pZC5zZXR0aW5ncwIBHTAZBBRjb20ucXRpLmRpYWdzZXJ2aWNl
+cwIBHTAaBBVjb20uYW5kcm9pZC5keW5zeXN0ZW0CAR0wHQQYY29tLmFuZHJvaWQuaW5wdXRkZXZp
+Y2VzAgEdMB8EGmNvbS5hbmRyb2lkLmxvY2FsdHJhbnNwb3J0AgEdMB8EGmNvbS5hbmRyb2lkLmxv
+Y2F0aW9uLmZ1c2VkAgEdMB8EGmNvbS5hbmRyb2lkLnNlcnZlci50ZWxlY29tAgEdMCAEG2NvbS5h
+bmRyb2lkLndhbGxwYXBlcmJhY2t1cAIBHTAhBBxjb20uZ29vZ2xlLlNTUmVzdGFydERldGVjdG9y
+AgEdMCIEHWNvbS5nb29nbGUuYW5kcm9pZC5oaWRkZW5tZW51AgEBMCMEHmNvbS5hbmRyb2lkLnBy
+b3ZpZGVycy5zZXR0aW5ncwIBHTEiBCAwGqPLCBE0UBxF8UIqvGbCQiT9Xe1f3I8X5pcXb9hmqjCB
+rqEIMQYCAQICAQOiAwIBAaMEAgIIAKUFMQMCAQSmCDEGAgEDAgEFv4FIBQIDAQABv4N3AgUAv4U+
+AwIBAL+FQEwwSgQgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQAKAQIEIHKNsSdP
+HxzxVx3kOAsEilVKxKOA529TVQg1KQhKk3gBv4VBAwIBAL+FQgUCAwMUs7+FTgUCAwMUs7+FTwUC
+AwMUszANBgkqhkiG9w0BAQsFAAOCAYEAJMIuzdNUdfrE6sIdmsnMn/scSG2odbphj8FkX9JGdF2S
+OT599HuDY9qhvkru2Dza4sLKK3f4ViBhuR9lpfeprKvstxbtBO7jkLYfVn0ZRzHRHVEyiW5IVKh+
+qOXVJ9S1lMShOTlsaYJytLKIlcrRAZBEXZiNbzTuVh1CH6X9Ni1dog14snm+lcOeORdL9fht2CHa
+u/caRnpWiZbjoAoJp0O89uBrRkXPpln51+3jPY6AFny30grNAvKguauDcPPhNV1yR+ylSsQi2gm3
+Rs4pgtlxFLMfZLgT0cbkl+9zk/QUqlpBP8ftUBsOI0ARr8xhFN3cvq9kXGLtJ9hEP9PRaflAFREk
+DK3IBIbVcAFZBFoAQOdE9zy0+F5bQrznPGaZg4Dzhcx33qMDUTgHtWoy+k3ePGQMEtmoTTLgQywW
+OIkXEoFqqGi9GKJXUT1KYi5NsigaYqu7FoN4Qsvs61pMUEfZSPP2AFwkA8uNFbmb9uxcxaGHCA8i
+3i9VM6yOLIrP
+-----END CERTIFICATE-----
diff --git a/tests/AttestationVerificationTest/assets/test_no_attestation_ext_certs.pem b/tests/AttestationVerificationTest/assets/test_no_attestation_ext_certs.pem
new file mode 100644
index 0000000..6d261fa
--- /dev/null
+++ b/tests/AttestationVerificationTest/assets/test_no_attestation_ext_certs.pem
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFoDCCA4igAwIBAgIQTfpKgAsLZJhp2V4xUriMADANBgkqhkiG9w0BAQ0FADBp
+MQswCQYDVQQGEwJVUzEUMBIGA1UECgwLR29vZ2xlIEluYy4xFzAVBgNVBAsMDkFu
+ZHJvaWQgVGhpbmdzMSswKQYDVQQDDCJBbmRyb2lkIFRoaW5ncyBBdHRlc3RhdGlv
+biBSb290IENBMCAXDTE3MDYyMTIwMjQzN1oYDzIwNTcwNjExMjAyNDM3WjBpMQsw
+CQYDVQQGEwJVUzEUMBIGA1UECgwLR29vZ2xlIEluYy4xFzAVBgNVBAsMDkFuZHJv
+aWQgVGhpbmdzMSswKQYDVQQDDCJBbmRyb2lkIFRoaW5ncyBBdHRlc3RhdGlvbiBS
+b290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuO82oerGivb9
+G9bWyM8Pg0y6SOnAC8/8b92dp1v4Npnc+QpjPRUKgn8lzjQ9Jo6IGY3OShRBiQYl
+bbZYkfJnC5HtqbOETdPLZclErVE/G6Oda1IeZWvQVMjNImEYOLL5ct2RxiPttd8v
+SLyOSNFPf5/SeFqX/La0NcmXMOvPSrTW3qO34brnC+ih7mlpJFLz6Up93N3Umxsl
+IElz2wCG72t6k3+caWLyIPVgIPmsQrfTeBK/hN5dAJgAN65BsTevLHRP9J610wj3
+RagSIK1NdTuJRnr5ZyTQrwE2nA8H3IJ7/eo6IlGhXPwLKDhbdxYygPxdlCq6Rl96
+aVLjfpqDPtJ9ow+QKZuEDbYJ4z4olNXC6O5G7vqnCuULA/2E7y7DZObjzXOrdx2z
+9YKd8BrIDMTN/5mmw2us8tywiaQhbl8vOtjU+A+iBBnkj/wt9TYyLKopdrDlo5mz
+wy5l750HOkVZXC3VkeECnp+9duSHdS4qeUf/W1j9nPM7kY0HFLPUVX9AFVp2JXnC
+iKZC32GQAVsDc1iyAZWAVTqA7E0fBHhk9jUnA0W9b5Lq06oW95ngNR1MIFY871i8
+aLHCBpIix8DuMe8NB9spCIP6WCQqGiWQQpzbeuBPtoi424xwZTO4oectTd77bs9V
+Rvunn49fz308KnoWjk/by1N7gWyTb38CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB
+/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMDQ1I0RKwFCI+Fy9uIIJ/HrXuqu
+MA0GCSqGSIb3DQEBDQUAA4ICAQB09qkyEpEDocdN5pPeXqtjj9d0AXREUGH2LhnC
+z9KZcUFR+JskwEMHCaOENOmKI3zWRmxT7d8cVywwGk+ExE7EBQoeHlh3Yo44M8aj
+ZL7RHCvHRYsePhAJkYpJ02IMR60TV+1jhMqE8+BnqFivS7kft4t295EyrnLRZE3b
+Nfc0t011j02RwUrioR57mdvS9EZBRnMQkobhn+jWt9O+V3mtplW+1A2n4ec6uni1
+2MMgAWHuO1sKVYd5Sp4JMUpNnfmQAMnNiOMF6VxkpaoF1lZWo4TrLxuDKJG3O8h1
+fByjCpNVY8kOvvYEadbldzh6Agy/3ppb9yfG7X7FtHr1ghNjuNT6w5VgvbRtoRja
+/ZSKuJMaKm5emMWNkls/cwVSPJIvTOzPTeYK1BKSyAL2LDJ93HI7x8h79/Q7gKRi
+kL8qT7GW2FqpWTK0253sJHqCJJP4A5Rxtf2+Afwqadfc6Ga4jJHb7rPXngz4j1ZB
+gl5yjXgWF9wHGxqrjKWe2EA3d47BC4HG3Rf5L56KQiRPhTqTk5vtZwtwLRLFDLt7
+Hdff13O1oLhn+2z9xkASUL3rFE/qWajZP7fk3CvzcuXwKDTZomIC4nNaglx4nLdj
+lHhOq+6ON8MZC46sLStD+D4a9A1HOoihJgI/yGGkwdrp4KQIveRkEBO/x9v3NNBE
+bMwG9w==
+-----END CERTIFICATE-----
diff --git a/tests/AttestationVerificationTest/assets/test_root_certs.pem b/tests/AttestationVerificationTest/assets/test_root_certs.pem
new file mode 100644
index 0000000..c51851fe
--- /dev/null
+++ b/tests/AttestationVerificationTest/assets/test_root_certs.pem
@@ -0,0 +1,61 @@
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIJAOj6GWMU0voYMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
+BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTYwNTI2MTYyODUyWhcNMjYwNTI0MTYy
+ODUyWjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
+Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
+tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
+nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
+C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
+oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
+JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
+sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
+igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
+RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
+aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
+AGMCAwEAAaOBpjCBozAdBgNVHQ4EFgQUNmHhAHyIBQlRi0RsR/8aTMnqTxIwHwYD
+VR0jBBgwFoAUNmHhAHyIBQlRi0RsR/8aTMnqTxIwDwYDVR0TAQH/BAUwAwEB/zAO
+BgNVHQ8BAf8EBAMCAYYwQAYDVR0fBDkwNzA1oDOgMYYvaHR0cHM6Ly9hbmRyb2lk
+Lmdvb2dsZWFwaXMuY29tL2F0dGVzdGF0aW9uL2NybC8wDQYJKoZIhvcNAQELBQAD
+ggIBACDIw41L3KlXG0aMiS//cqrG+EShHUGo8HNsw30W1kJtjn6UBwRM6jnmiwfB
+Pb8VA91chb2vssAtX2zbTvqBJ9+LBPGCdw/E53Rbf86qhxKaiAHOjpvAy5Y3m00m
+qC0w/Zwvju1twb4vhLaJ5NkUJYsUS7rmJKHHBnETLi8GFqiEsqTWpG/6ibYCv7rY
+DBJDcR9W62BW9jfIoBQcxUCUJouMPH25lLNcDc1ssqvC2v7iUgI9LeoM1sNovqPm
+QUiG9rHli1vXxzCyaMTjwftkJLkf6724DFhuKug2jITV0QkXvaJWF4nUaHOTNA4u
+JU9WDvZLI1j83A+/xnAJUucIv/zGJ1AMH2boHqF8CY16LpsYgBt6tKxxWH00XcyD
+CdW2KlBCeqbQPcsFmWyWugxdcekhYsAWyoSf818NUsZdBWBaR/OukXrNLfkQ79Iy
+ZohZbvabO/X+MVT3rriAoKc8oE2Uws6DF+60PV7/WIPjNvXySdqspImSN78mflxD
+qwLqRBYkA3I75qppLGG9rp7UCdRjxMl8ZDBld+7yvHVgt1cVzJx9xnyGCC23Uaic
+MDSXYrB4I4WHXPGjxhZuCuPBLTdOLU8YRvMYdEvYebWHMpvwGCF6bAx3JBpIeOQ1
+wDB5y0USicV3YgYGmi+NZfhA4URSh77Yd6uuJOJENRaNVTzk
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFHDCCAwSgAwIBAgIJANUP8luj8tazMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
+BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTkxMTIyMjAzNzU4WhcNMzQxMTE4MjAz
+NzU4WjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
+Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
+tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
+nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
+C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
+oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
+JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
+sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
+igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
+RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
+aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
+AGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1Ud
+IwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYD
+VR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQBOMaBc8oumXb2voc7XCWnu
+XKhBBK3e2KMGz39t7lA3XXRe2ZLLAkLM5y3J7tURkf5a1SutfdOyXAmeE6SRo83U
+h6WszodmMkxK5GM4JGrnt4pBisu5igXEydaW7qq2CdC6DOGjG+mEkN8/TA6p3cno
+L/sPyz6evdjLlSeJ8rFBH6xWyIZCbrcpYEJzXaUOEaxxXxgYz5/cTiVKN2M1G2ok
+QBUIYSY6bjEL4aUN5cfo7ogP3UvliEo3Eo0YgwuzR2v0KR6C1cZqZJSTnghIC/vA
+D32KdNQ+c3N+vl2OTsUVMC1GiWkngNx1OO1+kXW+YTnnTUOtOIswUP/Vqd5SYgAI
+mMAfY8U9/iIgkQj6T2W6FsScy94IN9fFhE1UtzmLoBIuUFsVXJMTz+Jucth+IqoW
+Fua9v1R93/k98p41pjtFX+H8DslVgfP097vju4KDlqN64xV1grw3ZLl4CiOe/A91
+oeLm2UHOq6wn3esB4r2EIQKb6jTVGu5sYCcdWpXr0AUVqcABPdgL+H7qJguBw09o
+jm6xNIrw2OocrDKsudk/okr/AwqEyPKw9WnMlQgLIKw1rODG2NvU9oR3GVGdMkUB
+ZutL8VuFkERQGt6vQ2OCw0sV47VMkuYbacK/xyZFiRcrPJPb41zgbQj9XAEyLKCH
+ex0SdDrx+tWUDqG8At2JHA==
+-----END CERTIFICATE-----
diff --git a/tests/AttestationVerificationTest/assets/test_virtual_device_attestation_certs.pem b/tests/AttestationVerificationTest/assets/test_virtual_device_attestation_certs.pem
new file mode 100644
index 0000000..2827710
--- /dev/null
+++ b/tests/AttestationVerificationTest/assets/test_virtual_device_attestation_certs.pem
@@ -0,0 +1,50 @@
+-----BEGIN CERTIFICATE-----
+MIIC7DCCApGgAwIBAgIBATAKBggqhkjOPQQDAjCBiDELMAkGA1UEBhMCVVMxEzAR
+BgNVBAgMCkNhbGlmb3JuaWExFTATBgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UE
+CwwHQW5kcm9pZDE7MDkGA1UEAwwyQW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBB
+dHRlc3RhdGlvbiBJbnRlcm1lZGlhdGUwHhcNNzAwMTAxMDAwMDAwWhcNNjkxMjMx
+MjM1OTU5WjAfMR0wGwYDVQQDDBRBbmRyb2lkIEtleXN0b3JlIEtleTBZMBMGByqG
+SM49AgEGCCqGSM49AwEHA0IABEYtCH28qu+St0F0TixVsQz0L/Y7DcRHgYAU98E6
+edwOpACFmmseYxMjvmZv/4jURSG2/Z0J1s3A/qFzIY96/tyjggFSMIIBTjALBgNV
+HQ8EBAMCB4AwggEcBgorBgEEAdZ5AgERBIIBDDCCAQgCAQQKAQACASkKAQAECXBs
+YXllcjQ1NgQAMIHqoQgxBgIBAgIBA6IDAgEDowQCAgEApQgxBgIBBAIBBqoDAgEB
+v4N3AgUAv4U9CAIGAX8DoY9Qv4U+AwIBAL+FQEwwSgQgAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAABAQAKAQIEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAv4VBBQIDAa2wv4VCBQIDAxUbv4VFRwRFMEMxHTAbBBZjb20uZ29v
+Z2xlLmF0dGVzdGF0aW9uAgEBMSIEIDqslV0SVHQBmP11cfMv7kT4nm6pchC7dRKa
+iM9vYebVMAAwHwYDVR0jBBgwFoAUP/ys1hqxOp6BILjVJRzFZbsekakwCgYIKoZI
+zj0EAwIDSQAwRgIhAMzs7gWWBIITpeLeEEx9B8ihdhkFqpMGlsYLRO01ZIOeAiEA
+uKs9xfK3fIOpVAhDmsrp+zE8KUwyvqCU/IS13tXz7Ng=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICeDCCAh6gAwIBAgICEAEwCgYIKoZIzj0EAwIwgZgxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBWaWV3MRUwEwYD
+VQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJvaWQxMzAxBgNVBAMMKkFu
+ZHJvaWQgS2V5c3RvcmUgU29mdHdhcmUgQXR0ZXN0YXRpb24gUm9vdDAeFw0xNjAx
+MTEwMDQ2MDlaFw0yNjAxMDgwMDQ2MDlaMIGIMQswCQYDVQQGEwJVUzETMBEGA1UE
+CAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdB
+bmRyb2lkMTswOQYDVQQDDDJBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVz
+dGF0aW9uIEludGVybWVkaWF0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOue
+efhCY1msyyqRTImGzHCtkGaTgqlzJhP+rMv4ISdMIXSXSir+pblNf2bU4GUQZjW8
+U7ego6ZxWD7bPhGuEBSjZjBkMB0GA1UdDgQWBBQ//KzWGrE6noEguNUlHMVlux6R
+qTAfBgNVHSMEGDAWgBTIrel3TEXDo88NFhDkeUM6IVowzzASBgNVHRMBAf8ECDAG
+AQH/AgEAMA4GA1UdDwEB/wQEAwIChDAKBggqhkjOPQQDAgNIADBFAiBLipt77oK8
+wDOHri/AiZi03cONqycqRZ9pDMfDktQPjgIhAO7aAV229DLp1IQ7YkyUBO86fMy9
+Xvsiu+f+uXc/WT/7
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICizCCAjKgAwIBAgIJAKIFntEOQ1tXMAoGCCqGSM49BAMCMIGYMQswCQYDVQQG
+EwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmll
+dzEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMTMwMQYD
+VQQDDCpBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0aW9uIFJvb3Qw
+HhcNMTYwMTExMDA0MzUwWhcNMzYwMTA2MDA0MzUwWjCBmDELMAkGA1UEBhMCVVMx
+EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFTAT
+BgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDEzMDEGA1UEAwwq
+QW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBBdHRlc3RhdGlvbiBSb290MFkwEwYH
+KoZIzj0CAQYIKoZIzj0DAQcDQgAE7l1ex+HA220Dpn7mthvsTWpdamguD/9/SQ59
+dx9EIm29sa/6FsvHrcV30lacqrewLVQBXT5DKyqO107sSHVBpKNjMGEwHQYDVR0O
+BBYEFMit6XdMRcOjzw0WEOR5QzohWjDPMB8GA1UdIwQYMBaAFMit6XdMRcOjzw0W
+EOR5QzohWjDPMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgKEMAoGCCqG
+SM49BAMCA0cAMEQCIDUho++LNEYenNVg8x1YiSBq3KNlQfYNns6KGYxmSGB7AiBN
+C/NR2TB8fVvaNTQdqEcbY6WFZTytTySn502vQX3xvw==
+-----END CERTIFICATE-----
diff --git a/tests/AttestationVerificationTest/src/android/security/attestationverification/PeerDeviceSystemAttestationVerificationTest.kt b/tests/AttestationVerificationTest/src/android/security/attestationverification/PeerDeviceSystemAttestationVerificationTest.kt
new file mode 100644
index 0000000..32c2230
--- /dev/null
+++ b/tests/AttestationVerificationTest/src/android/security/attestationverification/PeerDeviceSystemAttestationVerificationTest.kt
@@ -0,0 +1,161 @@
+package android.security.attestationverification
+
+import android.app.Activity
+import android.os.Bundle
+import android.security.attestationverification.AttestationVerificationManager.PARAM_CHALLENGE
+import android.security.attestationverification.AttestationVerificationManager.PARAM_PUBLIC_KEY
+import android.security.attestationverification.AttestationVerificationManager.PROFILE_PEER_DEVICE
+import android.security.attestationverification.AttestationVerificationManager.RESULT_FAILURE
+import android.security.attestationverification.AttestationVerificationManager.TYPE_CHALLENGE
+import android.security.attestationverification.AttestationVerificationManager.TYPE_PUBLIC_KEY
+import android.security.attestationverification.AttestationVerificationManager.TYPE_UNKNOWN
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.ByteArrayOutputStream
+import java.security.cert.CertificateFactory
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+
+/** Test for system-defined attestation verifiers. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PeerDeviceSystemAttestationVerificationTest {
+
+ @get:Rule
+ val rule = ActivityScenarioRule(TestActivity::class.java)
+
+ private val certifcateFactory = CertificateFactory.getInstance("X.509")
+ private lateinit var activity: Activity
+ private lateinit var avm: AttestationVerificationManager
+ private lateinit var invalidAttestationByteArray: ByteArray
+
+ @Before
+ fun setup() {
+ rule.getScenario().onActivity {
+ avm = it.getSystemService(AttestationVerificationManager::class.java)
+ activity = it
+ }
+ invalidAttestationByteArray = TEST_ATTESTATION_CERT_FILENAME.fromPEMFileToByteArray()
+ }
+
+ @Test
+ fun verifyAttestation_returnsFailureWrongBindingType() {
+ val future = CompletableFuture<Int>()
+ val profile = AttestationProfile(PROFILE_PEER_DEVICE)
+ avm.verifyAttestation(profile, TYPE_UNKNOWN, Bundle(),
+ invalidAttestationByteArray, activity.mainExecutor) { result, _ ->
+ future.complete(result)
+ }
+
+ assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE)
+ }
+
+ @Test
+ fun verifyAttestation_returnsFailureEmptyRequirements() {
+ val future = CompletableFuture<Int>()
+ val profile = AttestationProfile(PROFILE_PEER_DEVICE)
+ avm.verifyAttestation(profile, TYPE_PUBLIC_KEY, Bundle(),
+ invalidAttestationByteArray, activity.mainExecutor) { result, _ ->
+ future.complete(result)
+ }
+
+ assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE)
+ }
+
+ @Test
+ fun verifyAttestation_returnsFailureMismatchBindingType() {
+ val future = CompletableFuture<Int>()
+ val profile = AttestationProfile(PROFILE_PEER_DEVICE)
+ val publicKeyRequirements = Bundle()
+ publicKeyRequirements.putByteArray(PARAM_PUBLIC_KEY, "publicKeyStr".encodeToByteArray())
+ avm.verifyAttestation(profile, TYPE_CHALLENGE, publicKeyRequirements,
+ invalidAttestationByteArray, activity.mainExecutor) { result, _ ->
+ future.complete(result)
+ }
+
+ assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE)
+
+ val future2 = CompletableFuture<Int>()
+ val challengeRequirements = Bundle()
+ challengeRequirements.putByteArray(PARAM_CHALLENGE, "challengeStr".encodeToByteArray())
+ avm.verifyAttestation(profile, TYPE_PUBLIC_KEY, challengeRequirements,
+ invalidAttestationByteArray, activity.mainExecutor) { result, _ ->
+ future2.complete(result)
+ }
+
+ assertThat(future2.getSoon()).isEqualTo(RESULT_FAILURE)
+ }
+
+ @Test
+ fun verifyAttestation_returnsFailureWrongResourceKey() {
+ val future = CompletableFuture<Int>()
+ val profile = AttestationProfile(PROFILE_PEER_DEVICE)
+ val wrongKeyRequirements = Bundle()
+ wrongKeyRequirements.putByteArray("wrongReqKey", "publicKeyStr".encodeToByteArray())
+ avm.verifyAttestation(profile, TYPE_PUBLIC_KEY, wrongKeyRequirements,
+ invalidAttestationByteArray, activity.mainExecutor) { result, _ ->
+ future.complete(result)
+ }
+
+ assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE)
+ }
+
+ @Test
+ fun verifyAttestation_returnsFailureEmptyAttestation() {
+ val future = CompletableFuture<Int>()
+ val profile = AttestationProfile(PROFILE_PEER_DEVICE)
+ val requirements = Bundle()
+ requirements.putByteArray(PARAM_PUBLIC_KEY, "publicKeyStr".encodeToByteArray())
+ avm.verifyAttestation(profile, TYPE_PUBLIC_KEY, requirements, ByteArray(0),
+ activity.mainExecutor) { result, _ ->
+ future.complete(result)
+ }
+
+ assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE)
+ }
+
+ @Test
+ fun verifyAttestation_returnsFailureTrustAnchorMismatch() {
+ val future = CompletableFuture<Int>()
+ val profile = AttestationProfile(PROFILE_PEER_DEVICE)
+ val challengeRequirements = Bundle()
+ challengeRequirements.putByteArray(PARAM_CHALLENGE, "player456".encodeToByteArray())
+ avm.verifyAttestation(profile, TYPE_CHALLENGE, challengeRequirements,
+ invalidAttestationByteArray, activity.mainExecutor) { result, _ ->
+ future.complete(result)
+ }
+ assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE)
+ }
+
+ private fun <T> CompletableFuture<T>.getSoon(): T {
+ return this.get(1, TimeUnit.SECONDS)
+ }
+
+ private fun String.fromPEMFileToByteArray(): ByteArray {
+ val certs = certifcateFactory.generateCertificates(
+ InstrumentationRegistry.getInstrumentation().getContext().getResources().getAssets()
+ .open(this))
+ val bos = ByteArrayOutputStream()
+ certs.forEach {
+ bos.write(it.encoded)
+ }
+ return bos.toByteArray()
+ }
+
+ class TestActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ }
+ }
+
+ companion object {
+ private const val TEST_ATTESTATION_CERT_FILENAME = "test_attestation_wrong_root_certs.pem"
+ }
+}
diff --git a/tests/AttestationVerificationTest/src/android/security/attestationverification/SystemAttestationVerificationTest.kt b/tests/AttestationVerificationTest/src/android/security/attestationverification/SystemAttestationVerificationTest.kt
index 6290292..169effa 100644
--- a/tests/AttestationVerificationTest/src/android/security/attestationverification/SystemAttestationVerificationTest.kt
+++ b/tests/AttestationVerificationTest/src/android/security/attestationverification/SystemAttestationVerificationTest.kt
@@ -12,8 +12,8 @@
import org.junit.runner.RunWith
import com.google.common.truth.Truth.assertThat
import android.security.attestationverification.AttestationVerificationManager.PARAM_CHALLENGE
-import android.security.attestationverification.AttestationVerificationManager.PROFILE_PEER_DEVICE
import android.security.attestationverification.AttestationVerificationManager.PROFILE_SELF_TRUSTED
+import android.security.attestationverification.AttestationVerificationManager.PROFILE_UNKNOWN
import android.security.attestationverification.AttestationVerificationManager.RESULT_FAILURE
import android.security.attestationverification.AttestationVerificationManager.RESULT_SUCCESS
import android.security.attestationverification.AttestationVerificationManager.RESULT_UNKNOWN
@@ -52,7 +52,7 @@
@Test
fun verifyAttestation_returnsUnknown() {
val future = CompletableFuture<Int>()
- val profile = AttestationProfile(PROFILE_PEER_DEVICE)
+ val profile = AttestationProfile(PROFILE_UNKNOWN)
avm.verifyAttestation(profile, TYPE_PUBLIC_KEY, Bundle(), ByteArray(0),
activity.mainExecutor) { result, _ ->
future.complete(result)
@@ -137,7 +137,7 @@
@Test
fun verifyToken_returnsUnknown() {
val future = CompletableFuture<Int>()
- val profile = AttestationProfile(PROFILE_PEER_DEVICE)
+ val profile = AttestationProfile(PROFILE_SELF_TRUSTED)
avm.verifyAttestation(profile, TYPE_PUBLIC_KEY, Bundle(), ByteArray(0),
activity.mainExecutor) { _, token ->
val result = avm.verifyToken(profile, TYPE_PUBLIC_KEY, Bundle(), token, null)
@@ -150,7 +150,7 @@
@Test
fun verifyToken_tooBigMaxAgeThrows() {
val future = CompletableFuture<VerificationToken>()
- val profile = AttestationProfile(PROFILE_PEER_DEVICE)
+ val profile = AttestationProfile(PROFILE_SELF_TRUSTED)
avm.verifyAttestation(profile, TYPE_PUBLIC_KEY, Bundle(), ByteArray(0),
activity.mainExecutor) { _, token ->
future.complete(token)
diff --git a/tests/AttestationVerificationTest/src/com/android/server/security/AndroidKeystoreAttestationVerificationAttributesTest.java b/tests/AttestationVerificationTest/src/com/android/server/security/AndroidKeystoreAttestationVerificationAttributesTest.java
new file mode 100644
index 0000000..0d15fe7
--- /dev/null
+++ b/tests/AttestationVerificationTest/src/com/android/server/security/AndroidKeystoreAttestationVerificationAttributesTest.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.security;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.hamcrest.CoreMatchers;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/** Test for data class holding parsed X509Certificate attestation attributes. */
+@RunWith(AndroidJUnit4.class)
+public class AndroidKeystoreAttestationVerificationAttributesTest {
+ @Rule public ExpectedException mException = ExpectedException.none();
+ private static final String TEST_PHYSCIAL_DEVICE_CERTS =
+ "test_attestation_wrong_root_certs.pem";
+ private static final String TEST_PHYSICAL_DEVICE_CERTS_2 =
+ "test_attestation_with_root_certs.pem";
+ private static final String TEST_VIRTUAL_DEVICE_CERTS =
+ "test_virtual_device_attestation_certs.pem";
+ private static final String TEST_CERT_NO_ATTESTATION_EXTENSION =
+ "test_no_attestation_ext_certs.pem";
+ private static final String TEST_CERTS_NO_ATTESTATION_EXTENSION_2 =
+ "test_root_certs.pem";
+
+
+ private CertificateFactory mFactory;
+ private AndroidKeystoreAttestationVerificationAttributes mPhysicalDeviceAttributes;
+ private AndroidKeystoreAttestationVerificationAttributes mPhysicalDeviceAttributes2;
+ private AndroidKeystoreAttestationVerificationAttributes mVirtualDeviceAttributes;
+
+ @Before
+ public void setUp() throws Exception {
+ mFactory = CertificateFactory.getInstance("X.509");
+ mPhysicalDeviceAttributes =
+ AndroidKeystoreAttestationVerificationAttributes.fromCertificate(
+ generateCertificate(TEST_PHYSCIAL_DEVICE_CERTS));
+ mPhysicalDeviceAttributes2 =
+ AndroidKeystoreAttestationVerificationAttributes.fromCertificate(
+ generateCertificates(TEST_PHYSICAL_DEVICE_CERTS_2).get(0));
+ mVirtualDeviceAttributes =
+ AndroidKeystoreAttestationVerificationAttributes.fromCertificate(
+ generateCertificates(TEST_VIRTUAL_DEVICE_CERTS).get(0));
+ }
+
+ @Test
+ public void parseCertificate_noAttestationExtension() throws Exception {
+ List<X509Certificate> certsNoAttestation =
+ generateCertificates(TEST_CERTS_NO_ATTESTATION_EXTENSION_2);
+ certsNoAttestation.add(generateCertificate(TEST_CERT_NO_ATTESTATION_EXTENSION));
+ for (X509Certificate cert: certsNoAttestation) {
+ mException.expect(CertificateEncodingException.class);
+ mException.expectMessage(
+ CoreMatchers.containsString("No attestation extension found in certificate."));
+
+ AndroidKeystoreAttestationVerificationAttributes.fromCertificate(cert);
+ }
+ }
+
+ @Test
+ public void parseCertificate_attestationLevel() {
+ assertThat(mPhysicalDeviceAttributes.getAttestationVersion()).isEqualTo(3);
+ assertThat(mPhysicalDeviceAttributes2.getAttestationVersion()).isEqualTo(3);
+ assertThat(mVirtualDeviceAttributes.getAttestationVersion()).isEqualTo(4);
+ }
+
+ @Test
+ public void parseCertificate_attestationSecurityLevel() {
+ assertThat(mPhysicalDeviceAttributes.getAttestationSecurityLevel()).isEqualTo(
+ AndroidKeystoreAttestationVerificationAttributes.SecurityLevel.TRUSTED_ENVIRONMENT);
+ assertThat(mPhysicalDeviceAttributes2.getAttestationSecurityLevel()).isEqualTo(
+ AndroidKeystoreAttestationVerificationAttributes.SecurityLevel.TRUSTED_ENVIRONMENT);
+ assertThat(mVirtualDeviceAttributes.getAttestationSecurityLevel()).isEqualTo(
+ AndroidKeystoreAttestationVerificationAttributes.SecurityLevel.SOFTWARE);
+ }
+
+ @Test
+ public void parseCertificate_isAttestationHardwareBacked() {
+ assertThat(mPhysicalDeviceAttributes.isAttestationHardwareBacked()).isTrue();
+ assertThat(mPhysicalDeviceAttributes2.isAttestationHardwareBacked()).isTrue();
+ assertThat(mVirtualDeviceAttributes.isAttestationHardwareBacked()).isFalse();
+ }
+
+ @Test
+ public void parseCertificate_keymasterLevel() {
+ assertThat(mPhysicalDeviceAttributes.getKeymasterVersion()).isEqualTo(4);
+ assertThat(mPhysicalDeviceAttributes2.getKeymasterVersion()).isEqualTo(4);
+ assertThat(mVirtualDeviceAttributes.getKeymasterVersion()).isEqualTo(41);
+ }
+
+ @Test
+ public void parseCertificate_keymasterSecurityLevel() {
+ assertThat(mPhysicalDeviceAttributes.getKeymasterSecurityLevel()).isEqualTo(
+ AndroidKeystoreAttestationVerificationAttributes.SecurityLevel.TRUSTED_ENVIRONMENT);
+ assertThat(mPhysicalDeviceAttributes2.getKeymasterSecurityLevel()).isEqualTo(
+ AndroidKeystoreAttestationVerificationAttributes.SecurityLevel.TRUSTED_ENVIRONMENT);
+ assertThat(mVirtualDeviceAttributes.getKeymasterSecurityLevel()).isEqualTo(
+ AndroidKeystoreAttestationVerificationAttributes.SecurityLevel.SOFTWARE);
+ }
+
+ @Test
+ public void parseCertificate_isKeymasterHardwareBacked() {
+ assertThat(mPhysicalDeviceAttributes.isKeymasterHardwareBacked()).isTrue();
+ assertThat(mPhysicalDeviceAttributes2.isKeymasterHardwareBacked()).isTrue();
+ assertThat(mVirtualDeviceAttributes.isKeymasterHardwareBacked()).isFalse();
+ }
+
+ @Test
+ public void parseCertificate_attestationChallenge() {
+ assertThat(mPhysicalDeviceAttributes.getAttestationChallenge().toByteArray()).isEqualTo(
+ "abc".getBytes(UTF_8));
+ assertThat(mPhysicalDeviceAttributes2.getAttestationChallenge().toByteArray()).isEqualTo(
+ "player456".getBytes(UTF_8));
+ assertThat(mVirtualDeviceAttributes.getAttestationChallenge().toByteArray()).isEqualTo(
+ "player456".getBytes(UTF_8));
+ }
+
+ @Test
+ public void parseCertificate_verifiedBootState() {
+ assertThat(mPhysicalDeviceAttributes.getVerifiedBootState()).isEqualTo(
+ AndroidKeystoreAttestationVerificationAttributes.VerifiedBootState.UNVERIFIED);
+ assertThat(mPhysicalDeviceAttributes2.getVerifiedBootState()).isEqualTo(
+ AndroidKeystoreAttestationVerificationAttributes.VerifiedBootState.VERIFIED);
+ assertThat(mVirtualDeviceAttributes.getVerifiedBootState()).isNull();
+ }
+
+ @Test
+ public void parseCertificate_keyBootPatchLevel() {
+ assertThat(mPhysicalDeviceAttributes.getKeyBootPatchLevel()).isEqualTo(201907);
+ assertThat(mPhysicalDeviceAttributes2.getKeyBootPatchLevel()).isEqualTo(20220105);
+ }
+
+ @Test
+ public void parseCertificate_keyBootPatchLevelNotSetException() {
+ mException.expect(IllegalStateException.class);
+ mException.expectMessage(
+ CoreMatchers.containsString("KeyBootPatchLevel is not set."));
+
+ mVirtualDeviceAttributes.getKeyBootPatchLevel();
+ }
+
+ @Test
+ public void parseCertificate_keyOsPatchLevel() {
+ assertThat(mPhysicalDeviceAttributes.getKeyOsPatchLevel()).isEqualTo(201907);
+ assertThat(mPhysicalDeviceAttributes2.getKeyOsPatchLevel()).isEqualTo(202201);
+ }
+
+ @Test
+ public void parseCertificate_keyOsPatchLevelNotSetException() {
+ mException.expect(IllegalStateException.class);
+ mException.expectMessage(
+ CoreMatchers.containsString("KeyOsPatchLevel is not set."));
+
+ mVirtualDeviceAttributes.getKeyOsPatchLevel();
+ }
+
+ @Test
+ public void parseCertificate_keyVendorPatchLevel() {
+ assertThat(mPhysicalDeviceAttributes.getKeyVendorPatchLevel()).isEqualTo(201907);
+ assertThat(mPhysicalDeviceAttributes2.getKeyVendorPatchLevel()).isEqualTo(20220105);
+ }
+
+ @Test
+ public void parseCertificate_keyVendorPatchLevelNotSetException() {
+ mException.expect(IllegalStateException.class);
+ mException.expectMessage(
+ CoreMatchers.containsString("KeyVendorPatchLevel is not set."));
+
+ mVirtualDeviceAttributes.getKeyVendorPatchLevel();
+ }
+
+ @Test
+ public void parseCertificate_keyAuthenticatorType() {
+ assertThat(mPhysicalDeviceAttributes.getKeyAuthenticatorType()).isEqualTo(0);
+ assertThat(mPhysicalDeviceAttributes2.getKeyAuthenticatorType()).isEqualTo(0);
+ }
+
+ @Test
+ public void parseCertificate_keyOsVersion() {
+ assertThat(mPhysicalDeviceAttributes.getKeyOsVersion()).isEqualTo(0);
+ assertThat(mPhysicalDeviceAttributes2.getKeyOsVersion()).isEqualTo(120000);
+ }
+
+ @Test
+ public void parseCertificate_keyOsVersionNotSetException() {
+ mException.expect(IllegalStateException.class);
+ mException.expectMessage(
+ CoreMatchers.containsString("KeyOsVersion is not set."));
+
+ mVirtualDeviceAttributes.getKeyOsVersion();
+ }
+
+ @Test
+ public void parseCertificate_verifiedBootHash() {
+ assertThat(mPhysicalDeviceAttributes.getVerifiedBootHash()).isNotEmpty();
+ assertThat(mPhysicalDeviceAttributes2.getVerifiedBootHash()).isNotEmpty();
+ }
+
+ @Test
+ public void parseCertificate_verifiedBootKey() {
+ assertThat(mPhysicalDeviceAttributes.getVerifiedBootKey()).isNotEmpty();
+ assertThat(mPhysicalDeviceAttributes2.getVerifiedBootKey()).isNotEmpty();
+ }
+
+ @Test
+ public void parseCertificate_isVerifiedBootLocked() {
+ assertThat(mPhysicalDeviceAttributes.isVerifiedBootLocked()).isFalse();
+ assertThat(mPhysicalDeviceAttributes2.isVerifiedBootLocked()).isTrue();
+ }
+
+ @Test
+ public void parseCertificate_isVerifiedBootLockedNotSetException() {
+ mException.expect(IllegalStateException.class);
+ mException.expectMessage(
+ CoreMatchers.containsString("VerifiedBootLocked is not set."));
+
+ mVirtualDeviceAttributes.isVerifiedBootLocked();
+ }
+
+ @Test
+ public void parseCertificate_applicationPackageNameVersion() {
+ assertThat(mPhysicalDeviceAttributes.getApplicationPackageNameVersion()).isNotEmpty();
+ }
+
+ @Test
+ public void parseCertificate_applicationCertificateDigests() {
+ assertThat(mPhysicalDeviceAttributes.getApplicationCertificateDigests()).isNotEmpty();
+ }
+
+ @Test
+ public void parseCertificate_valuesNotSet() {
+ assertThat(mPhysicalDeviceAttributes.getDeviceBrand()).isNull();
+ assertThat(mPhysicalDeviceAttributes.getDeviceName()).isNull();
+ assertThat(mPhysicalDeviceAttributes.getDeviceProductName()).isNull();
+ assertThat(mPhysicalDeviceAttributes.isKeyAllowedForAllApplications()).isFalse();
+ assertThat(mPhysicalDeviceAttributes2.getDeviceBrand()).isNull();
+ assertThat(mPhysicalDeviceAttributes2.getDeviceName()).isNull();
+ assertThat(mPhysicalDeviceAttributes2.getDeviceProductName()).isNull();
+ assertThat(mPhysicalDeviceAttributes2.isKeyAllowedForAllApplications()).isFalse();
+ }
+
+ @Test
+ public void parseCertificate_keyRequiresUnlockedDeviceNotSetException() {
+ mException.expect(IllegalStateException.class);
+ mException.expectMessage(
+ CoreMatchers.containsString("KeyRequiresUnlockedDevice is not set."));
+
+ mPhysicalDeviceAttributes.isKeyRequiresUnlockedDevice();
+ }
+
+ private X509Certificate generateCertificate(String certificateString)
+ throws Exception {
+ return generateCertificates(certificateString).get(0);
+ }
+
+ private List<X509Certificate> generateCertificates(String certificateString)
+ throws Exception {
+ Collection<? extends Certificate> certificates = mFactory.generateCertificates(
+ InstrumentationRegistry.getInstrumentation().getContext().getResources().getAssets()
+ .open(certificateString));
+
+ ArrayList<X509Certificate> x509Certs = new ArrayList<>();
+ for (Certificate cert : certificates) {
+ x509Certs.add((X509Certificate) cert);
+ }
+ return x509Certs;
+ }
+}
diff --git a/tests/AttestationVerificationTest/src/com/android/server/security/AttestationVerificationPeerDeviceVerifierTest.kt b/tests/AttestationVerificationTest/src/com/android/server/security/AttestationVerificationPeerDeviceVerifierTest.kt
new file mode 100644
index 0000000..45f2e5c
--- /dev/null
+++ b/tests/AttestationVerificationTest/src/com/android/server/security/AttestationVerificationPeerDeviceVerifierTest.kt
@@ -0,0 +1,175 @@
+package com.android.server.security
+
+import android.app.Activity
+import android.content.Context
+import android.os.Bundle
+import android.security.attestationverification.AttestationVerificationManager.PARAM_CHALLENGE
+import android.security.attestationverification.AttestationVerificationManager.PARAM_PUBLIC_KEY
+import android.security.attestationverification.AttestationVerificationManager.RESULT_FAILURE
+import android.security.attestationverification.AttestationVerificationManager.RESULT_SUCCESS
+import android.security.attestationverification.AttestationVerificationManager.TYPE_CHALLENGE
+import android.security.attestationverification.AttestationVerificationManager.TYPE_PUBLIC_KEY
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import java.io.ByteArrayOutputStream
+import java.security.cert.Certificate
+import java.security.cert.CertificateFactory
+import java.security.cert.TrustAnchor
+import java.security.cert.X509Certificate
+import java.time.LocalDate
+
+/** Test for Peer Device attestation verifier. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AttestationVerificationPeerDeviceVerifierTest {
+ private val certificateFactory = CertificateFactory.getInstance("X.509")
+ @Mock private lateinit var context: Context
+ private lateinit var trustAnchors: HashSet<TrustAnchor>
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ val rootCerts = TEST_ROOT_CERT_FILENAME.fromPEMFileToCerts()
+ trustAnchors = HashSet<TrustAnchor>()
+ rootCerts.forEach {
+ trustAnchors.add(TrustAnchor(it as X509Certificate, null))
+ }
+ }
+
+ @Test
+ fun verifyAttestation_returnsSuccessTypeChallenge() {
+ val verifier = AttestationVerificationPeerDeviceVerifier(
+ context, trustAnchors, false, LocalDate.of(2022, 2, 1),
+ LocalDate.of(2021, 8, 1))
+ val challengeRequirements = Bundle()
+ challengeRequirements.putByteArray(PARAM_CHALLENGE, "player456".encodeToByteArray())
+
+ val result = verifier.verifyAttestation(TYPE_CHALLENGE, challengeRequirements,
+ TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray())
+ assertThat(result).isEqualTo(RESULT_SUCCESS)
+ }
+
+ @Test
+ fun verifyAttestation_returnsSuccessLocalPatchOlderThanOneYear() {
+ val verifier = AttestationVerificationPeerDeviceVerifier(
+ context, trustAnchors, false, LocalDate.of(2022, 2, 1),
+ LocalDate.of(2021, 1, 1))
+ val challengeRequirements = Bundle()
+ challengeRequirements.putByteArray(PARAM_CHALLENGE, "player456".encodeToByteArray())
+
+ val result = verifier.verifyAttestation(TYPE_CHALLENGE, challengeRequirements,
+ TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray())
+ assertThat(result).isEqualTo(RESULT_SUCCESS)
+ }
+
+ @Test
+ fun verifyAttestation_returnsSuccessTypePublicKey() {
+ val verifier = AttestationVerificationPeerDeviceVerifier(
+ context, trustAnchors, false, LocalDate.of(2022, 2, 1),
+ LocalDate.of(2021, 8, 1))
+
+ val leafCert =
+ (TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToCerts() as List)[0]
+ as X509Certificate
+ val pkRequirements = Bundle()
+ pkRequirements.putByteArray(PARAM_PUBLIC_KEY, leafCert.publicKey.encoded)
+
+ val result = verifier.verifyAttestation(
+ TYPE_PUBLIC_KEY, pkRequirements,
+ TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray())
+ assertThat(result).isEqualTo(RESULT_SUCCESS)
+ }
+
+ @Test
+ fun verifyAttestation_returnsFailurePatchDateNotWithinOneYearLocalPatch() {
+ val verifier = AttestationVerificationPeerDeviceVerifier(
+ context, trustAnchors, false, LocalDate.of(2023, 3, 1),
+ LocalDate.of(2023, 2, 1))
+ val challengeRequirements = Bundle()
+ challengeRequirements.putByteArray(PARAM_CHALLENGE, "player456".encodeToByteArray())
+
+ val result = verifier.verifyAttestation(TYPE_CHALLENGE, challengeRequirements,
+ TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray())
+ assertThat(result).isEqualTo(RESULT_FAILURE)
+ }
+
+ @Test
+ fun verifyAttestation_returnsFailureTrustedAnchorEmpty() {
+ val verifier = AttestationVerificationPeerDeviceVerifier(
+ context, HashSet(), false, LocalDate.of(2022, 1, 1),
+ LocalDate.of(2022, 1, 1))
+ val challengeRequirements = Bundle()
+ challengeRequirements.putByteArray(PARAM_CHALLENGE, "player456".encodeToByteArray())
+
+ val result = verifier.verifyAttestation(TYPE_CHALLENGE, challengeRequirements,
+ TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray())
+ assertThat(result).isEqualTo(RESULT_FAILURE)
+ }
+
+ @Test
+ fun verifyAttestation_returnsFailureTrustedAnchorMismatch() {
+ val badTrustAnchorsCerts = TEST_ATTESTATION_CERT_FILENAME.fromPEMFileToCerts()
+ val badTrustAnchors = HashSet<TrustAnchor>()
+ badTrustAnchorsCerts.forEach {
+ badTrustAnchors.add(TrustAnchor(it as X509Certificate, null))
+ }
+
+ val verifier = AttestationVerificationPeerDeviceVerifier(
+ context, badTrustAnchors, false, LocalDate.of(2022, 1, 1),
+ LocalDate.of(2022, 1, 1))
+ val challengeRequirements = Bundle()
+ challengeRequirements.putByteArray(PARAM_CHALLENGE, "player456".encodeToByteArray())
+
+ val result = verifier.verifyAttestation(TYPE_CHALLENGE, challengeRequirements,
+ TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray())
+ assertThat(result).isEqualTo(RESULT_FAILURE)
+ }
+
+ fun verifyAttestation_returnsFailureChallenge() {
+ val verifier = AttestationVerificationPeerDeviceVerifier(
+ context, trustAnchors, false, LocalDate.of(2022, 1, 1),
+ LocalDate.of(2022, 1, 1))
+ val challengeRequirements = Bundle()
+ challengeRequirements.putByteArray(PARAM_CHALLENGE, "wrong".encodeToByteArray())
+
+ val result = verifier.verifyAttestation(TYPE_CHALLENGE, challengeRequirements,
+ TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray())
+ assertThat(result).isEqualTo(RESULT_FAILURE)
+ }
+
+ private fun String.fromPEMFileToCerts(): Collection<Certificate> {
+ return certificateFactory.generateCertificates(
+ InstrumentationRegistry.getInstrumentation().getContext().getResources().getAssets()
+ .open(this))
+ }
+
+ private fun String.fromPEMFileToByteArray(): ByteArray {
+ val certs = this.fromPEMFileToCerts()
+ val bos = ByteArrayOutputStream()
+ certs.forEach {
+ bos.write(it.encoded)
+ }
+ return bos.toByteArray()
+ }
+
+ class TestActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ }
+ }
+
+ companion object {
+ private const val TEST_ROOT_CERT_FILENAME = "test_root_certs.pem"
+ private const val TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME =
+ "test_attestation_with_root_certs.pem"
+ private const val TEST_ATTESTATION_CERT_FILENAME = "test_attestation_wrong_root_certs.pem"
+ }
+}