Merge "[test] Check vm attestation certificate chain in CTS tests" into main
diff --git a/service_vm/test_apk/Android.bp b/service_vm/test_apk/Android.bp
index e69b348..450d475 100644
--- a/service_vm/test_apk/Android.bp
+++ b/service_vm/test_apk/Android.bp
@@ -59,11 +59,20 @@
     test_config: "AndroidTest.rkpd.xml",
     static_libs: [
         "RkpdAppTestUtil",
+        "VmAttestationTestUtil",
         "androidx.work_work-testing",
-        "bouncycastle-unbundled",
     ],
     instrumentation_for: "rkpdapp",
     // This app is a variation of rkpdapp, with additional permissions to run
     // a VM. It is defined in packages/modules/RemoteKeyProvisioning.
     data: [":avf-rkpdapp"],
 }
+
+java_library {
+    name: "VmAttestationTestUtil",
+    srcs: ["src/java/com/android/virt/vm_attestation/util/*.java"],
+    static_libs: [
+        "bouncycastle-unbundled",
+        "truth",
+    ],
+}
diff --git a/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java b/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java
index a48fc81..ce7fc45 100644
--- a/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java
+++ b/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java
@@ -19,7 +19,6 @@
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.TruthJUnit.assume;
 
 import android.content.Context;
@@ -40,22 +39,15 @@
 import com.android.rkpdapp.provisioner.PeriodicProvisioner;
 import com.android.rkpdapp.testutil.SystemInterfaceSelector;
 import com.android.rkpdapp.utils.Settings;
-import com.android.rkpdapp.utils.X509Utils;
 import com.android.virt.vm_attestation.testservice.IAttestationService.SigningResult;
+import com.android.virt.vm_attestation.util.X509Utils;
 
-import org.bouncycastle.asn1.ASN1Boolean;
-import org.bouncycastle.asn1.ASN1Encodable;
-import org.bouncycastle.asn1.ASN1OctetString;
-import org.bouncycastle.asn1.ASN1Sequence;
-import org.bouncycastle.asn1.DEROctetString;
-import org.bouncycastle.asn1.DERUTF8String;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
-import java.security.Signature;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -86,7 +78,7 @@
 @RunWith(Parameterized.class)
 public class RkpdVmAttestationTest extends MicrodroidDeviceTestBase {
     private static final String TAG = "RkpdVmAttestationTest";
-    private static final String AVF_ATTESTATION_EXTENSION_OID = "1.3.6.1.4.1.11129.2.1.29.1";
+
     private static final String SERVICE_NAME = IRemotelyProvisionedComponent.DESCRIPTOR + "/avf";
     private static final String VM_PAYLOAD_PATH = "libvm_attestation_test_payload.so";
     private static final String MESSAGE = "Hello RKP from AVF!";
@@ -173,67 +165,9 @@
                 runVmAttestationService(TAG, vm, challenge, MESSAGE.getBytes());
 
         // Assert.
-        // Parsing the certificate chain successfully indicates that the certificate
-        // chain is valid, that each certificate is signed by the next one and the last
-        // one is self-signed.
-        X509Certificate[] certs = X509Utils.formatX509Certs(signingResult.certificateChain);
-        assertThat(certs.length).isGreaterThan(2);
-        assertWithMessage("The first certificate should be generated in the RKP VM")
-                .that(certs[0].getSubjectX500Principal().getName())
-                .isEqualTo("CN=Android Protected Virtual Machine Key");
-        checkAvfAttestationExtension(certs[0], challenge);
-        assertWithMessage("The second certificate should contain AVF in the subject")
-                .that(certs[1].getSubjectX500Principal().getName())
-                .contains("O=AVF");
-
-        // Verify the signature using the public key from the leaf certificate generated
-        // in the RKP VM.
-        Signature sig = Signature.getInstance("SHA256withECDSA");
-        sig.initVerify(certs[0].getPublicKey());
-        sig.update(MESSAGE.getBytes());
-        assertThat(sig.verify(signingResult.signature)).isTrue();
-    }
-
-    private void checkAvfAttestationExtension(X509Certificate cert, byte[] challenge)
-            throws Exception {
-        byte[] extensionValue = cert.getExtensionValue(AVF_ATTESTATION_EXTENSION_OID);
-        ASN1OctetString extString = ASN1OctetString.getInstance(extensionValue);
-        ASN1Sequence seq = ASN1Sequence.getInstance(extString.getOctets());
-        // AVF attestation extension should contain 3 elements in the following format:
-        //
-        //  AttestationExtension ::= SEQUENCE {
-        //     attestationChallenge       OCTET_STRING,
-        //     isVmSecure                 BOOLEAN,
-        //     vmComponents               SEQUENCE OF VmComponent,
-        //  }
-        //   VmComponent ::= SEQUENCE {
-        //     name               UTF8String,
-        //     securityVersion    INTEGER,
-        //     codeHash           OCTET STRING,
-        //     authorityHash      OCTET STRING,
-        //  }
-        assertThat(seq).hasSize(3);
-
-        ASN1OctetString expectedChallenge = new DEROctetString(challenge);
-        assertThat(seq.getObjectAt(0)).isEqualTo(expectedChallenge);
-        assertWithMessage("The VM should be unsecure as it is debuggable.")
-                .that(seq.getObjectAt(1))
-                .isEqualTo(ASN1Boolean.FALSE);
-        ASN1Sequence vmComponents = ASN1Sequence.getInstance(seq.getObjectAt(2));
-        assertExtensionContainsPayloadApk(vmComponents);
-    }
-
-    private void assertExtensionContainsPayloadApk(ASN1Sequence vmComponents) throws Exception {
-        DERUTF8String payloadApkName = new DERUTF8String("apk:" + TEST_APP_PACKAGE_NAME);
-        boolean found = false;
-        for (ASN1Encodable encodable : vmComponents) {
-            ASN1Sequence vmComponent = ASN1Sequence.getInstance(encodable);
-            assertThat(vmComponent).hasSize(4);
-            if (payloadApkName.equals(vmComponent.getObjectAt(0))) {
-                assertWithMessage("Payload APK should not be found twice.").that(found).isFalse();
-                found = true;
-            }
-        }
-        assertWithMessage("vmComponents should contain the payload APK.").that(found).isTrue();
+        X509Certificate[] certs =
+                X509Utils.validateAndParseX509CertChain(signingResult.certificateChain);
+        X509Utils.verifyAvfRelatedCerts(certs, challenge, TEST_APP_PACKAGE_NAME);
+        X509Utils.verifySignature(certs[0], MESSAGE.getBytes(), signingResult.signature);
     }
 }
diff --git a/service_vm/test_apk/src/java/com/android/virt/vm_attestation/util/X509Utils.java b/service_vm/test_apk/src/java/com/android/virt/vm_attestation/util/X509Utils.java
new file mode 100644
index 0000000..cfa3663
--- /dev/null
+++ b/service_vm/test_apk/src/java/com/android/virt/vm_attestation/util/X509Utils.java
@@ -0,0 +1,143 @@
+/**
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * <p>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
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>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.virt.vm_attestation.util;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import org.bouncycastle.asn1.ASN1Boolean;
+import org.bouncycastle.asn1.ASN1Encodable;
+import org.bouncycastle.asn1.ASN1OctetString;
+import org.bouncycastle.asn1.ASN1Sequence;
+import org.bouncycastle.asn1.DEROctetString;
+import org.bouncycastle.asn1.DERUTF8String;
+
+import java.io.ByteArrayInputStream;
+import java.security.Signature;
+import java.security.cert.CertPathValidator;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.PKIXParameters;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Set;
+
+/**
+ * Provides utility methods for parsing and verifying X.509 certificate chain issued from pVM remote
+ * attestation.
+ */
+public class X509Utils {
+    private static final String AVF_ATTESTATION_EXTENSION_OID = "1.3.6.1.4.1.11129.2.1.29.1";
+
+    /** Validates and parses the given DER-encoded X.509 certificate chain. */
+    public static X509Certificate[] validateAndParseX509CertChain(byte[] x509CertChain)
+            throws Exception {
+        CertificateFactory factory = CertificateFactory.getInstance("X.509");
+        ByteArrayInputStream in = new ByteArrayInputStream(x509CertChain);
+        ArrayList<Certificate> certs = new ArrayList<>(factory.generateCertificates(in));
+        X509Certificate[] certChain = certs.toArray(new X509Certificate[0]);
+        validateCertChain(certChain);
+        return certChain;
+    }
+
+    private static void validateCertChain(X509Certificate[] certChain) throws Exception {
+        X509Certificate rootCert = certChain[certChain.length - 1];
+        // The root certificate should be self-signed.
+        rootCert.verify(rootCert.getPublicKey());
+
+        // Only add the self-signed root certificate as trust anchor.
+        // All the other certificates in the chain should be signed by the previous cert's key.
+        Set<TrustAnchor> trustAnchors =
+                Set.of(new TrustAnchor(rootCert, /* nameConstraints= */ null));
+
+        CertificateFactory factory = CertificateFactory.getInstance("X.509");
+        CertPathValidator validator = CertPathValidator.getInstance("PKIX");
+        PKIXParameters parameters = new PKIXParameters(trustAnchors);
+        parameters.setRevocationEnabled(false);
+        validator.validate(factory.generateCertPath(Arrays.asList(certChain)), parameters);
+    }
+
+    /**
+     * Verifies the AVF related certificates in the given certificate chain. The AVF Attestation
+     * extension should be found in the leaf certificate.
+     */
+    public static void verifyAvfRelatedCerts(
+            X509Certificate[] certChain, byte[] challenge, String payloadApk) throws Exception {
+        assertThat(certChain.length).isGreaterThan(2);
+        assertWithMessage("The first certificate should be generated in the RKP VM")
+                .that(certChain[0].getSubjectX500Principal().getName())
+                .isEqualTo("CN=Android Protected Virtual Machine Key");
+        verifyAvfAttestationExtension(certChain[0], challenge, payloadApk);
+
+        assertWithMessage("The second certificate should contain AVF in the subject")
+                .that(certChain[1].getSubjectX500Principal().getName())
+                .contains("O=AVF");
+    }
+
+    private static void verifyAvfAttestationExtension(
+            X509Certificate cert, byte[] challenge, String payloadApk) throws Exception {
+        byte[] extensionValue = cert.getExtensionValue(AVF_ATTESTATION_EXTENSION_OID);
+        ASN1OctetString extString = ASN1OctetString.getInstance(extensionValue);
+        ASN1Sequence seq = ASN1Sequence.getInstance(extString.getOctets());
+        // AVF attestation extension should contain 3 elements in the following format:
+        //
+        //  AttestationExtension ::= SEQUENCE {
+        //     attestationChallenge       OCTET_STRING,
+        //     isVmSecure                 BOOLEAN,
+        //     vmComponents               SEQUENCE OF VmComponent,
+        //  }
+        //   VmComponent ::= SEQUENCE {
+        //     name               UTF8String,
+        //     securityVersion    INTEGER,
+        //     codeHash           OCTET STRING,
+        //     authorityHash      OCTET STRING,
+        //  }
+        assertThat(seq).hasSize(3);
+
+        ASN1OctetString expectedChallenge = new DEROctetString(challenge);
+        assertThat(seq.getObjectAt(0)).isEqualTo(expectedChallenge);
+        assertWithMessage("The VM should be unsecure as it is debuggable.")
+                .that(seq.getObjectAt(1))
+                .isEqualTo(ASN1Boolean.FALSE);
+        ASN1Sequence vmComponents = ASN1Sequence.getInstance(seq.getObjectAt(2));
+        assertExtensionContainsPayloadApk(vmComponents, payloadApk);
+    }
+
+    private static void assertExtensionContainsPayloadApk(
+            ASN1Sequence vmComponents, String payloadApk) throws Exception {
+        DERUTF8String payloadApkName = new DERUTF8String("apk:" + payloadApk);
+        boolean found = false;
+        for (ASN1Encodable encodable : vmComponents) {
+            ASN1Sequence vmComponent = ASN1Sequence.getInstance(encodable);
+            assertThat(vmComponent).hasSize(4);
+            if (payloadApkName.equals(vmComponent.getObjectAt(0))) {
+                assertWithMessage("Payload APK should not be found twice.").that(found).isFalse();
+                found = true;
+            }
+        }
+        assertWithMessage("vmComponents should contain the payload APK.").that(found).isTrue();
+    }
+
+    /** Verifies the given signature using the public key from the given certificate. */
+    public static void verifySignature(
+            X509Certificate publicKeyCert, byte[] messageToSign, byte[] signature)
+            throws Exception {
+        Signature sig = Signature.getInstance("SHA256withECDSA");
+        sig.initVerify(publicKeyCert.getPublicKey());
+        sig.update(messageToSign);
+        assertThat(sig.verify(signature)).isTrue();
+    }
+}
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 1ed48d0..84bf098 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -39,6 +39,7 @@
     srcs: ["src/java/**/*.java"],
     static_libs: [
         "MicrodroidDeviceTestHelper",
+        "VmAttestationTestUtil",
         "androidx.test.runner",
         "androidx.test.ext.junit",
         "authfs_test_apk_assets",
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index a9ef53a..1195cd3 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -69,6 +69,7 @@
 import com.android.microdroid.testservice.IAppCallback;
 import com.android.microdroid.testservice.ITestService;
 import com.android.microdroid.testservice.IVmCallback;
+import com.android.virt.vm_attestation.util.X509Utils;
 
 import com.google.common.base.Strings;
 import com.google.common.truth.BooleanSubject;
@@ -96,6 +97,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.security.cert.X509Certificate;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
@@ -117,6 +119,7 @@
 @RunWith(Parameterized.class)
 public class MicrodroidTests extends MicrodroidDeviceTestBase {
     private static final String TAG = "MicrodroidTests";
+    private static final String TEST_APP_PACKAGE_NAME = "com.android.microdroid.test";
     private static final String VM_ATTESTATION_PAYLOAD_PATH = "libvm_attestation_test_payload.so";
     private static final String VM_ATTESTATION_MESSAGE = "Hello RKP from AVF!";
 
@@ -277,8 +280,13 @@
                 .isAnyOf(
                         AttestationStatus.ATTESTATION_OK,
                         AttestationStatus.ATTESTATION_ERROR_ATTESTATION_FAILED);
-        // TODO(b/330662600): Check the certificate chain and the signature after refactoring the
-        // x509 util method in RkpdVmAttestationTest.
+        if (signingResult.status == AttestationStatus.ATTESTATION_OK) {
+            X509Certificate[] certs =
+                    X509Utils.validateAndParseX509CertChain(signingResult.certificateChain);
+            X509Utils.verifyAvfRelatedCerts(certs, challenge, TEST_APP_PACKAGE_NAME);
+            X509Utils.verifySignature(
+                    certs[0], VM_ATTESTATION_MESSAGE.getBytes(), signingResult.signature);
+        }
     }
 
     @Test