Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit beff3729 authored by Justin McClain's avatar Justin McClain
Browse files

Implementing verification of PROFILE_SELF_TRUSTED.

Implementing PROFILE_SELF_TRUSTED for testing purposes.

Bug: 216476609
Test: SystemAttestationVerificationTest unit test
Change-Id: Ie0ecf2f88dcc9591ca41dc7d9f30f8727bb5fa1e
parent 8ae64dc0
Loading
Loading
Loading
Loading
+18 −2
Original line number Diff line number Diff line
@@ -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<IVerificationResult> 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);
    }

+224 −0
Original line number Diff line number Diff line
/*
 * 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<X509Certificate> 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<X509Certificate> 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<TrustAnchor> 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<X509Certificate> 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();
    }
}
+137 −5
Original line number Diff line number Diff line
@@ -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<Int>()
        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<Int>()
        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<Int>()
        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<Int>()
        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<Int>()
        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<Int>()
        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<Int>()
        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<Int>()
        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<VerificationToken>()
        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"
    }
}