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"
+    }
+}