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