Loading services/core/java/com/android/server/security/AttestationVerificationManagerService.java +18 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); } Loading services/core/java/com/android/server/security/AttestationVerificationSelfTrustedVerifierForTesting.java 0 → 100644 +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(); } } tests/AttestationVerificationTest/src/android/security/attestationverification/SystemAttestationVerificationTest.kt +137 −5 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) Loading @@ -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) Loading @@ -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) Loading @@ -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" } } Loading
services/core/java/com/android/server/security/AttestationVerificationManagerService.java +18 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); } Loading
services/core/java/com/android/server/security/AttestationVerificationSelfTrustedVerifierForTesting.java 0 → 100644 +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(); } }
tests/AttestationVerificationTest/src/android/security/attestationverification/SystemAttestationVerificationTest.kt +137 −5 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) Loading @@ -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) Loading @@ -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) Loading @@ -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" } }