From beff3729c1f786195710a2444d7617e4e0b4eb40 Mon Sep 17 00:00:00 2001 From: Justin McClain Date: Tue, 30 Nov 2021 16:12:33 +0000 Subject: [PATCH] Implementing verification of PROFILE_SELF_TRUSTED. Implementing PROFILE_SELF_TRUSTED for testing purposes. Bug: 216476609 Test: SystemAttestationVerificationTest unit test Change-Id: Ie0ecf2f88dcc9591ca41dc7d9f30f8727bb5fa1e --- ...AttestationVerificationManagerService.java | 20 +- ...ficationSelfTrustedVerifierForTesting.java | 224 ++++++++++++++++++ .../SystemAttestationVerificationTest.kt | 142 ++++++++++- 3 files changed, 379 insertions(+), 7 deletions(-) create mode 100644 services/core/java/com/android/server/security/AttestationVerificationSelfTrustedVerifierForTesting.java diff --git a/services/core/java/com/android/server/security/AttestationVerificationManagerService.java b/services/core/java/com/android/server/security/AttestationVerificationManagerService.java index f519ceda1e501..243efb5e58cec 100644 --- a/services/core/java/com/android/server/security/AttestationVerificationManagerService.java +++ b/services/core/java/com/android/server/security/AttestationVerificationManagerService.java @@ -16,6 +16,8 @@ package com.android.server.security; +import static android.security.attestationverification.AttestationVerificationManager.PROFILE_SELF_TRUSTED; +import static android.security.attestationverification.AttestationVerificationManager.RESULT_FAILURE; import static android.security.attestationverification.AttestationVerificationManager.RESULT_UNKNOWN; import android.content.Context; @@ -76,10 +78,24 @@ public class AttestationVerificationManagerService extends SystemService { private void verifyAttestationForAllVerifiers( AttestationProfile profile, int localBindingType, Bundle requirements, byte[] attestation, AndroidFuture resultCallback) { - // TODO(b/201696614): Implement IVerificationResult result = new IVerificationResult(); - result.resultCode = RESULT_UNKNOWN; + // TODO(b/201696614): Implement result.token = null; + switch (profile.getAttestationProfileId()) { + case PROFILE_SELF_TRUSTED: + Slog.d(TAG, "Verifying Self trusted profile."); + try { + result.resultCode = + AttestationVerificationSelfTrustedVerifierForTesting.getInstance() + .verifyAttestation(localBindingType, requirements, attestation); + } catch (Throwable t) { + result.resultCode = RESULT_FAILURE; + } + break; + default: + Slog.d(TAG, "No profile found, defaulting."); + result.resultCode = RESULT_UNKNOWN; + } resultCallback.complete(result); } diff --git a/services/core/java/com/android/server/security/AttestationVerificationSelfTrustedVerifierForTesting.java b/services/core/java/com/android/server/security/AttestationVerificationSelfTrustedVerifierForTesting.java new file mode 100644 index 0000000000000..58df2bd982dc3 --- /dev/null +++ b/services/core/java/com/android/server/security/AttestationVerificationSelfTrustedVerifierForTesting.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.server.security; + +import static android.security.attestationverification.AttestationVerificationManager.PARAM_CHALLENGE; +import static android.security.attestationverification.AttestationVerificationManager.RESULT_FAILURE; +import static android.security.attestationverification.AttestationVerificationManager.RESULT_SUCCESS; +import static android.security.attestationverification.AttestationVerificationManager.TYPE_CHALLENGE; + +import android.annotation.NonNull; +import android.os.Build; +import android.os.Bundle; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.org.bouncycastle.asn1.ASN1InputStream; +import com.android.internal.org.bouncycastle.asn1.ASN1ObjectIdentifier; +import com.android.internal.org.bouncycastle.asn1.ASN1OctetString; +import com.android.internal.org.bouncycastle.asn1.ASN1Sequence; +import com.android.internal.org.bouncycastle.asn1.x509.Certificate; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.cert.CertPath; +import java.security.cert.CertPathValidator; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +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.Collections; +import java.util.List; +import java.util.Set; + +/** + * Verifies {@code PROFILE_SELF_TRUSTED} attestations. + * + * Verifies that the attesting environment can create an attestation with the same root certificate + * as the verifying device with a matching attestation challenge. Skips CRL revocations checking + * so this verifier can work in a hermetic test environment. + * + * This verifier profile is intended to be used only for testing. + */ +class AttestationVerificationSelfTrustedVerifierForTesting { + private static final String TAG = "AVF"; + private static final boolean DEBUG = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.VERBOSE); + + // The OID for the extension Android Keymint puts into device-generated certificates. + private static final String ANDROID_KEYMINT_KEY_DESCRIPTION_EXTENSION_OID = + "1.3.6.1.4.1.11129.2.1.17"; + + // ASN.1 sequence index values for the Android Keymint extension. + private static final int ATTESTATION_CHALLENGE_INDEX = 4; + + private static final String ANDROID_KEYSTORE = "AndroidKeyStore"; + private static final String GOLDEN_ALIAS = + AttestationVerificationSelfTrustedVerifierForTesting.class.getCanonicalName() + + ".Golden"; + + private static volatile AttestationVerificationSelfTrustedVerifierForTesting + sAttestationVerificationSelfTrustedVerifier = null; + + private final CertificateFactory mCertificateFactory; + private final CertPathValidator mCertPathValidator; + private final KeyStore mAndroidKeyStore; + private X509Certificate mGoldenRootCert; + + static AttestationVerificationSelfTrustedVerifierForTesting getInstance() + throws Exception { + if (sAttestationVerificationSelfTrustedVerifier == null) { + synchronized (AttestationVerificationSelfTrustedVerifierForTesting.class) { + if (sAttestationVerificationSelfTrustedVerifier == null) { + sAttestationVerificationSelfTrustedVerifier = + new AttestationVerificationSelfTrustedVerifierForTesting(); + } + } + } + return sAttestationVerificationSelfTrustedVerifier; + } + + private static void debugVerboseLog(String str, Throwable t) { + if (DEBUG) { + Slog.v(TAG, str, t); + } + } + + private static void debugVerboseLog(String str) { + if (DEBUG) { + Slog.v(TAG, str); + } + } + + private AttestationVerificationSelfTrustedVerifierForTesting() throws Exception { + mCertificateFactory = CertificateFactory.getInstance("X.509"); + mCertPathValidator = CertPathValidator.getInstance("PKIX"); + mAndroidKeyStore = KeyStore.getInstance(ANDROID_KEYSTORE); + mAndroidKeyStore.load(null); + if (!mAndroidKeyStore.containsAlias(GOLDEN_ALIAS)) { + KeyPairGenerator kpg = + KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, ANDROID_KEYSTORE); + KeyGenParameterSpec parameterSpec = new KeyGenParameterSpec.Builder( + GOLDEN_ALIAS, KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY) + .setAttestationChallenge(GOLDEN_ALIAS.getBytes()) + .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512).build(); + kpg.initialize(parameterSpec); + kpg.generateKeyPair(); + } + + X509Certificate[] goldenCerts = (X509Certificate[]) + ((KeyStore.PrivateKeyEntry) mAndroidKeyStore.getEntry(GOLDEN_ALIAS, null)) + .getCertificateChain(); + mGoldenRootCert = goldenCerts[goldenCerts.length - 1]; + } + + int verifyAttestation( + int localBindingType, @NonNull Bundle requirements, @NonNull byte[] attestation) { + List certificates = new ArrayList<>(); + ByteArrayInputStream bis = new ByteArrayInputStream(attestation); + try { + while (bis.available() > 0) { + certificates.add((X509Certificate) mCertificateFactory.generateCertificate(bis)); + } + } catch (CertificateException e) { + debugVerboseLog("Unable to parse certificates from attestation", e); + return RESULT_FAILURE; + } + + if (localBindingType == TYPE_CHALLENGE + && validateRequirements(requirements) + && checkLeafChallenge(requirements, certificates) + && verifyCertificateChain(certificates)) { + return RESULT_SUCCESS; + } + + return RESULT_FAILURE; + } + + private boolean verifyCertificateChain(List certificates) { + if (certificates.size() < 2) { + debugVerboseLog("Certificate chain less than 2 in size."); + return false; + } + + try { + CertPath certificatePath = mCertificateFactory.generateCertPath(certificates); + PKIXParameters validationParams = new PKIXParameters(getTrustAnchors()); + // Skipping revocation checking because we want this to work in a hermetic test + // environment. + validationParams.setRevocationEnabled(false); + mCertPathValidator.validate(certificatePath, validationParams); + } catch (Throwable t) { + debugVerboseLog("Invalid certificate chain", t); + return false; + } + + return true; + } + + private Set getTrustAnchors() { + return Collections.singleton(new TrustAnchor(mGoldenRootCert, null)); + } + + private boolean validateRequirements(Bundle requirements) { + if (requirements.size() != 1) { + debugVerboseLog("Requirements does not contain exactly 1 key."); + return false; + } + if (!requirements.containsKey(PARAM_CHALLENGE)) { + debugVerboseLog("Requirements does not contain key: " + PARAM_CHALLENGE); + return false; + } + return true; + } + + private boolean checkLeafChallenge(Bundle requirements, List certificates) { + // Verify challenge + byte[] challenge; + try { + challenge = getChallengeFromCert(certificates.get(0)); + } catch (Throwable t) { + debugVerboseLog("Unable to parse challenge from certificate.", t); + return false; + } + + if (Arrays.equals(requirements.getByteArray(PARAM_CHALLENGE), challenge)) { + return true; + } else { + debugVerboseLog("Self-Trusted validation failed; challenge mismatch."); + return false; + } + } + + private byte[] getChallengeFromCert(@NonNull X509Certificate x509Certificate) + throws CertificateEncodingException, IOException { + Certificate certificate = Certificate.getInstance( + new ASN1InputStream(x509Certificate.getEncoded()).readObject()); + ASN1Sequence keyAttributes = (ASN1Sequence) certificate.getTBSCertificate().getExtensions() + .getExtensionParsedValue( + new ASN1ObjectIdentifier(ANDROID_KEYMINT_KEY_DESCRIPTION_EXTENSION_OID)); + return ((ASN1OctetString) keyAttributes.getObjectAt(ATTESTATION_CHALLENGE_INDEX)) + .getOctets(); + } +} diff --git a/tests/AttestationVerificationTest/src/android/security/attestationverification/SystemAttestationVerificationTest.kt b/tests/AttestationVerificationTest/src/android/security/attestationverification/SystemAttestationVerificationTest.kt index 48bfd6f5d33c0..62902929fcd54 100644 --- a/tests/AttestationVerificationTest/src/android/security/attestationverification/SystemAttestationVerificationTest.kt +++ b/tests/AttestationVerificationTest/src/android/security/attestationverification/SystemAttestationVerificationTest.kt @@ -11,10 +11,20 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import com.google.common.truth.Truth.assertThat +import android.security.attestationverification.AttestationVerificationManager.PARAM_CHALLENGE +import android.security.attestationverification.AttestationVerificationManager.PROFILE_PEER_DEVICE import android.security.attestationverification.AttestationVerificationManager.PROFILE_SELF_TRUSTED -import android.security.attestationverification.AttestationVerificationManager.TYPE_PUBLIC_KEY +import android.security.attestationverification.AttestationVerificationManager.RESULT_FAILURE +import android.security.attestationverification.AttestationVerificationManager.RESULT_SUCCESS import android.security.attestationverification.AttestationVerificationManager.RESULT_UNKNOWN +import android.security.attestationverification.AttestationVerificationManager.TYPE_PUBLIC_KEY +import android.security.attestationverification.AttestationVerificationManager.TYPE_CHALLENGE +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties import java.lang.IllegalArgumentException +import java.io.ByteArrayOutputStream +import java.security.KeyPairGenerator +import java.security.KeyStore import java.time.Duration import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit @@ -23,25 +33,26 @@ import java.util.concurrent.TimeUnit @SmallTest @RunWith(AndroidJUnit4::class) class SystemAttestationVerificationTest { - @get:Rule val rule = ActivityScenarioRule(TestActivity::class.java) private lateinit var activity: Activity private lateinit var avm: AttestationVerificationManager + private lateinit var androidKeystore: KeyStore @Before fun setup() { rule.getScenario().onActivity { avm = it.getSystemService(AttestationVerificationManager::class.java) activity = it + androidKeystore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) } } } @Test fun verifyAttestation_returnsUnknown() { val future = CompletableFuture() - val profile = AttestationProfile(PROFILE_SELF_TRUSTED) + val profile = AttestationProfile(PROFILE_PEER_DEVICE) avm.verifyAttestation(profile, TYPE_PUBLIC_KEY, Bundle(), ByteArray(0), activity.mainExecutor) { result, _ -> future.complete(result) @@ -51,9 +62,82 @@ class SystemAttestationVerificationTest { } @Test - fun verifyToken_returnsUnknown() { + fun verifyAttestation_returnsFailureWithEmptyAttestation() { val future = CompletableFuture() val profile = AttestationProfile(PROFILE_SELF_TRUSTED) + avm.verifyAttestation(profile, TYPE_CHALLENGE, Bundle(), ByteArray(0), + activity.mainExecutor) { result, _ -> + future.complete(result) + } + + assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE) + } + + @Test + fun verifyAttestation_returnsFailureWithEmptyRequirements() { + val future = CompletableFuture() + val selfTrusted = TestSelfTrustedAttestation("test", "challengeStr") + avm.verifyAttestation(selfTrusted.profile, selfTrusted.localBindingType, + Bundle(), selfTrusted.attestation, activity.mainExecutor) { result, _ -> + future.complete(result) + } + assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE) + } + + @Test + fun verifyAttestation_returnsFailureWithWrongBindingType() { + val future = CompletableFuture() + val selfTrusted = TestSelfTrustedAttestation("test", "challengeStr") + avm.verifyAttestation(selfTrusted.profile, TYPE_PUBLIC_KEY, + selfTrusted.requirements, selfTrusted.attestation, activity.mainExecutor) { result, _ -> + future.complete(result) + } + assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE) + } + + @Test + fun verifyAttestation_returnsFailureWithWrongRequirements() { + val future = CompletableFuture() + val selfTrusted = TestSelfTrustedAttestation("test", "challengeStr") + val wrongKeyRequirements = Bundle() + wrongKeyRequirements.putByteArray( + "wrongBindingKey", "challengeStr".encodeToByteArray()) + avm.verifyAttestation(selfTrusted.profile, selfTrusted.localBindingType, + wrongKeyRequirements, selfTrusted.attestation, activity.mainExecutor) { result, _ -> + future.complete(result) + } + assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE) + } + + @Test + fun verifyAttestation_returnsFailureWithWrongChallenge() { + val future = CompletableFuture() + val selfTrusted = TestSelfTrustedAttestation("test", "challengeStr") + val wrongChallengeRequirements = Bundle() + wrongChallengeRequirements.putByteArray(PARAM_CHALLENGE, "wrong".encodeToByteArray()) + avm.verifyAttestation(selfTrusted.profile, selfTrusted.localBindingType, + wrongChallengeRequirements, selfTrusted.attestation, activity.mainExecutor) { + result, _ -> future.complete(result) + } + assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE) + } + + // TODO(b/216144791): Add more failure tests for PROFILE_SELF_TRUSTED. + @Test + fun verifyAttestation_returnsSuccess() { + val future = CompletableFuture() + val selfTrusted = TestSelfTrustedAttestation("test", "challengeStr") + avm.verifyAttestation(selfTrusted.profile, selfTrusted.localBindingType, + selfTrusted.requirements, selfTrusted.attestation, activity.mainExecutor) { result, _ -> + future.complete(result) + } + assertThat(future.getSoon()).isEqualTo(RESULT_SUCCESS) + } + + @Test + fun verifyToken_returnsUnknown() { + val future = CompletableFuture() + val profile = AttestationProfile(PROFILE_PEER_DEVICE) avm.verifyAttestation(profile, TYPE_PUBLIC_KEY, Bundle(), ByteArray(0), activity.mainExecutor) { _, token -> val result = avm.verifyToken(profile, TYPE_PUBLIC_KEY, Bundle(), token, null) @@ -66,7 +150,7 @@ class SystemAttestationVerificationTest { @Test fun verifyToken_tooBigMaxAgeThrows() { val future = CompletableFuture() - val profile = AttestationProfile(PROFILE_SELF_TRUSTED) + val profile = AttestationProfile(PROFILE_PEER_DEVICE) avm.verifyAttestation(profile, TYPE_PUBLIC_KEY, Bundle(), ByteArray(0), activity.mainExecutor) { _, token -> future.complete(token) @@ -87,4 +171,52 @@ class SystemAttestationVerificationTest { super.onCreate(savedInstanceState) } } + + inner class TestSelfTrustedAttestation(val alias: String, val challenge: String) { + val profile = AttestationProfile(PROFILE_SELF_TRUSTED) + val localBindingType = TYPE_CHALLENGE + val requirements: Bundle + val attestation: ByteArray + + init { + val challengeByteArray = challenge.encodeToByteArray() + generateAndStoreKey(alias, challengeByteArray) + attestation = generateCertificatesByteArray(alias) + requirements = Bundle() + requirements.putByteArray(PARAM_CHALLENGE, challengeByteArray) + } + + private fun generateAndStoreKey(alias: String, challenge: ByteArray) { + val kpg: KeyPairGenerator = KeyPairGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_EC, + ANDROID_KEYSTORE + ) + val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder( + alias, + KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY + ).run { + // a challenge results in a generated attestation + setAttestationChallenge(challenge) + setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512) + build() + } + kpg.initialize(parameterSpec) + kpg.generateKeyPair() + } + + private fun generateCertificatesByteArray(alias: String): ByteArray { + val pkEntry = androidKeystore.getEntry(alias, null) as KeyStore.PrivateKeyEntry + val certs = pkEntry.certificateChain + val bos = ByteArrayOutputStream() + certs.forEach { + bos.write(it.encoded) + } + return bos.toByteArray() + } + } + + companion object { + private const val TAG = "AVFTEST" + private const val ANDROID_KEYSTORE = "AndroidKeyStore" + } }