1 /**
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  * <p>http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * <p>Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11  * express or implied. See the License for the specific language governing permissions and
12  * limitations under the License.
13  */
14 package com.android.virt.vm_attestation.util;
15 
16 import static com.google.common.truth.Truth.assertThat;
17 import static com.google.common.truth.Truth.assertWithMessage;
18 
19 import org.bouncycastle.asn1.ASN1Boolean;
20 import org.bouncycastle.asn1.ASN1Encodable;
21 import org.bouncycastle.asn1.ASN1OctetString;
22 import org.bouncycastle.asn1.ASN1Sequence;
23 import org.bouncycastle.asn1.DEROctetString;
24 import org.bouncycastle.asn1.DERUTF8String;
25 
26 import java.io.ByteArrayInputStream;
27 import java.security.Signature;
28 import java.security.cert.CertPathValidator;
29 import java.security.cert.Certificate;
30 import java.security.cert.CertificateFactory;
31 import java.security.cert.PKIXParameters;
32 import java.security.cert.TrustAnchor;
33 import java.security.cert.X509Certificate;
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.Set;
37 
38 /**
39  * Provides utility methods for parsing and verifying X.509 certificate chain issued from pVM remote
40  * attestation.
41  */
42 public class X509Utils {
43     private static final String AVF_ATTESTATION_EXTENSION_OID = "1.3.6.1.4.1.11129.2.1.29.1";
44 
45     /** Validates and parses the given DER-encoded X.509 certificate chain. */
validateAndParseX509CertChain(byte[] x509CertChain)46     public static X509Certificate[] validateAndParseX509CertChain(byte[] x509CertChain)
47             throws Exception {
48         CertificateFactory factory = CertificateFactory.getInstance("X.509");
49         ByteArrayInputStream in = new ByteArrayInputStream(x509CertChain);
50         ArrayList<Certificate> certs = new ArrayList<>(factory.generateCertificates(in));
51         X509Certificate[] certChain = certs.toArray(new X509Certificate[0]);
52         validateCertChain(certChain);
53         return certChain;
54     }
55 
validateCertChain(X509Certificate[] certChain)56     private static void validateCertChain(X509Certificate[] certChain) throws Exception {
57         X509Certificate rootCert = certChain[certChain.length - 1];
58         // The root certificate should be self-signed.
59         rootCert.verify(rootCert.getPublicKey());
60 
61         // Only add the self-signed root certificate as trust anchor.
62         // All the other certificates in the chain should be signed by the previous cert's key.
63         Set<TrustAnchor> trustAnchors =
64                 Set.of(new TrustAnchor(rootCert, /* nameConstraints= */ null));
65 
66         CertificateFactory factory = CertificateFactory.getInstance("X.509");
67         CertPathValidator validator = CertPathValidator.getInstance("PKIX");
68         PKIXParameters parameters = new PKIXParameters(trustAnchors);
69         parameters.setRevocationEnabled(false);
70         validator.validate(factory.generateCertPath(Arrays.asList(certChain)), parameters);
71     }
72 
73     /**
74      * Verifies the AVF related certificates in the given certificate chain. The AVF Attestation
75      * extension should be found in the leaf certificate.
76      */
verifyAvfRelatedCerts( X509Certificate[] certChain, byte[] challenge, String payloadApk)77     public static void verifyAvfRelatedCerts(
78             X509Certificate[] certChain, byte[] challenge, String payloadApk) throws Exception {
79         assertThat(certChain.length).isGreaterThan(2);
80         assertWithMessage("The first certificate should be generated in the RKP VM")
81                 .that(certChain[0].getSubjectX500Principal().getName())
82                 .isEqualTo("CN=Android Protected Virtual Machine Key");
83         verifyAvfAttestationExtension(certChain[0], challenge, payloadApk);
84 
85         assertWithMessage("The second certificate should contain AVF in the subject")
86                 .that(certChain[1].getSubjectX500Principal().getName())
87                 .contains("O=AVF");
88     }
89 
verifyAvfAttestationExtension( X509Certificate cert, byte[] challenge, String payloadApk)90     private static void verifyAvfAttestationExtension(
91             X509Certificate cert, byte[] challenge, String payloadApk) throws Exception {
92         byte[] extensionValue = cert.getExtensionValue(AVF_ATTESTATION_EXTENSION_OID);
93         ASN1OctetString extString = ASN1OctetString.getInstance(extensionValue);
94         ASN1Sequence seq = ASN1Sequence.getInstance(extString.getOctets());
95         // AVF attestation extension should contain 3 elements in the following format:
96         //
97         //  AttestationExtension ::= SEQUENCE {
98         //     attestationChallenge       OCTET_STRING,
99         //     isVmSecure                 BOOLEAN,
100         //     vmComponents               SEQUENCE OF VmComponent,
101         //  }
102         //   VmComponent ::= SEQUENCE {
103         //     name               UTF8String,
104         //     securityVersion    INTEGER,
105         //     codeHash           OCTET STRING,
106         //     authorityHash      OCTET STRING,
107         //  }
108         assertThat(seq).hasSize(3);
109 
110         ASN1OctetString expectedChallenge = new DEROctetString(challenge);
111         assertThat(seq.getObjectAt(0)).isEqualTo(expectedChallenge);
112         assertWithMessage("The VM should be unsecure as it is debuggable.")
113                 .that(seq.getObjectAt(1))
114                 .isEqualTo(ASN1Boolean.FALSE);
115         ASN1Sequence vmComponents = ASN1Sequence.getInstance(seq.getObjectAt(2));
116         assertExtensionContainsPayloadApk(vmComponents, payloadApk);
117     }
118 
assertExtensionContainsPayloadApk( ASN1Sequence vmComponents, String payloadApk)119     private static void assertExtensionContainsPayloadApk(
120             ASN1Sequence vmComponents, String payloadApk) throws Exception {
121         DERUTF8String payloadApkName = new DERUTF8String("apk:" + payloadApk);
122         boolean found = false;
123         for (ASN1Encodable encodable : vmComponents) {
124             ASN1Sequence vmComponent = ASN1Sequence.getInstance(encodable);
125             assertThat(vmComponent).hasSize(4);
126             if (payloadApkName.equals(vmComponent.getObjectAt(0))) {
127                 assertWithMessage("Payload APK should not be found twice.").that(found).isFalse();
128                 found = true;
129             }
130         }
131         assertWithMessage("vmComponents should contain the payload APK.").that(found).isTrue();
132     }
133 
134     /** Verifies the given signature using the public key from the given certificate. */
verifySignature( X509Certificate publicKeyCert, byte[] messageToSign, byte[] signature)135     public static void verifySignature(
136             X509Certificate publicKeyCert, byte[] messageToSign, byte[] signature)
137             throws Exception {
138         Signature sig = Signature.getInstance("SHA256withECDSA");
139         sig.initVerify(publicKeyCert.getPublicKey());
140         sig.update(messageToSign);
141         assertThat(sig.verify(signature)).isTrue();
142     }
143 }
144