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