Implementing verification of PROFILE_SELF_TRUSTED.
Implementing PROFILE_SELF_TRUSTED for testing purposes. Bug: 216476609 Test: SystemAttestationVerificationTest unit test Change-Id: Ie0ecf2f88dcc9591ca41dc7d9f30f8727bb5fa1e
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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();
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user