[test] Check vm attestation certificate chain in CTS tests
This cl also contains a refactoring that moves the X509
verification methods for AVF e2e tests to an independent
library. The new library no longer depends on rkpd for
certificates verification.
This refactoring allows us to reuse these methods within CTS
tests for VM attestation.
Bug: 330662600
Test: atest AvfRkpdVmAttestationTestApp
Change-Id: I1c0e94c08d8c61c6221685783e7cea28c0a19740
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 aae1068..c3d9757 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