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

Commit 57ca3da2 authored by Dmitry Dementyev's avatar Dmitry Dementyev
Browse files

Add support for testing mode root certificate.

1) Add Certificate
2) Helper class for end-to-end tests
3) Only create snapshot for passwords with special prefix in test mode
4) Sync only keys with insecure prefix in test mode.

Bug: 76433465
Test: adb shell am instrument -w -e package
com.android.server.locksettings.recoverablekeystore
com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner

Change-Id: I6edc8c4716c3a034b6b79c7aa6f4b8478e9a3c9e
parent 7a33063b
Loading
Loading
Loading
Loading
+67 −3
Original line number Diff line number Diff line
@@ -37,6 +37,40 @@ public final class TrustedRootCertificates {

    public static final String GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_ALIAS =
            "GoogleCloudKeyVaultServiceV1";
    /**
     * Certificate used for client-side end-to-end encryption tests.
     * When recovery controller is initialized with the certificate, recovery snapshots will only
     * contain application keys started with {@link INSECURE_KEY_ALIAS}.
     * Recovery snapshot will only be created if device is unlocked with password started with
     * {@link #INSECURE_PASSWORD_PREFIX}.
     *
     * @hide
     */
    public static final String TEST_ONLY_INSECURE_CERTIFICATE_ALIAS =
            "TEST_ONLY_INSECURE_CERTIFICATE_ALIAS";

    /**
     * TODO: Add insecure certificate to TestApi.
     * @hide
     */
    public static @NonNull X509Certificate getTestOnlyInsecureCertificate() {
        return parseBase64Certificate(TEST_ONLY_INSECURE_CERTIFICATE_BASE64);
    }
    /**
     * Keys, which alias starts with the prefix are not protected if
     * recovery agent uses {@link #TEST_ONLY_INSECURE_CERTIFICATE_ALIAS} root certificate.
     * @hide
     */
    public static final String INSECURE_KEY_ALIAS_PREFIX =
            "INSECURE_KEY_ALIAS_KEY_MATERIAL_IS_NOT_PROTECTED_";
    /**
     * Prefix for insecure passwords with length 14.
     * Passwords started with the prefix are not protected if recovery agent uses
     * {@link #TEST_ONLY_INSECURE_CERTIFICATE_ALIAS} root certificate.
     * @hide
     */
    public static final String INSECURE_PASSWORD_PREFIX =
            "INSECURE_PSWD_";

    private static final String GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_BASE64 = ""
            + "MIIFJjCCAw6gAwIBAgIJAIobXsJlzhNdMA0GCSqGSIb3DQEBDQUAMCAxHjAcBgNV"
@@ -68,13 +102,43 @@ public final class TrustedRootCertificates {
            + "/oM58v0orUWINtIc2hBlka36PhATYQiLf+AiWKnwhCaaHExoYKfQlMtXBodNvOK8"
            + "xqx69x05q/qbHKEcTHrsss630vxrp1niXvA=";

    private static final String TEST_ONLY_INSECURE_CERTIFICATE_BASE64 = ""
            + "MIIFMDCCAxigAwIBAgIJAIZ9/G8KQie9MA0GCSqGSIb3DQEBDQUAMCUxIzAhBgNV"
            + "BAMMGlRlc3QgT25seSBVbnNlY3VyZSBSb290IENBMB4XDTE4MDMyODAwMzIyM1oX"
            + "DTM4MDMyMzAwMzIyM1owJTEjMCEGA1UEAwwaVGVzdCBPbmx5IFVuc2VjdXJlIFJv"
            + "b3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDGxFNzAEyzSPmw"
            + "E5gfuBXdXq++bl9Ep62V7Xn1UiejvmS+pRHT39pf/M7sl4Zr9ezanJTrFvf9+B85"
            + "VGehdsD32TgfEjThcqaoQCI6pKkHYsUo7FZ5n+G3eE8oabWRZJMVo3QDjnnFYp7z"
            + "20vnpjDofI2oQyxHcb/1yep+ca1+4lIvbUp/ybhNFqhRXAMcDXo7pyH38eUQ1JdK"
            + "Q/QlBbShpFEqx1Y6KilKfTDf7Wenqr67LkaEim//yLZjlHzn/BpuRTrpo+XmJZx1"
            + "P9CX9LGOXTtmsaCcYgD4yijOvV8aEsIJaf1kCIO558oH0oQc+0JG5aXeLN7BDlyZ"
            + "vH0RdSx5nQLS9kj2I6nthOw/q00/L+S6A0m5jyNZOAl1SY78p+wO0d9eHbqQzJwf"
            + "EsSq3qGAqlgQyyjp6oxHBqT9hZtN4rxw+iq0K1S4kmTLNF1FvmIB1BE+lNvvoGdY"
            + "5G0b6Pe4R5JFn9LV3C3PEmSYnae7iG0IQlKmRADIuvfJ7apWAVanJPJAAWh2Akfp"
            + "8Uxr02cHoY6o7vsEhJJOeMkipaBHThESm/XeFVubQzNfZ9gjQnB9ZX2v+lyj+WYZ"
            + "SAz3RuXx6TlLrmWccMpQDR1ibcgyyjLUtX3kwZl2OxmJXitjuD7xlxvAXYob15N+"
            + "K4xKHgxUDrbt2zU/tY0vgepAUg/xbwIDAQABo2MwYTAdBgNVHQ4EFgQUwyeNpYgs"
            + "XXYvh9z0/lFrja7sV+swHwYDVR0jBBgwFoAUwyeNpYgsXXYvh9z0/lFrja7sV+sw"
            + "DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQENBQAD"
            + "ggIBAGuOsvMN5SD3RIQnMJtBpcHNrxun+QFjPZFlYCLfIPrUkHpn5O1iIIq8tVLd"
            + "2V+12VKnToUEANsYBD3MP8XjP+6GZ7ZQ2rwLGvUABKSX4YXvmjEEXZUZp0y3tIV4"
            + "kUDlbACzguPneZDp5Qo7YWH4orgqzHkn0sD/ikO5XrAqmzc245ewJlrf+V11mjcu"
            + "ELfDrEejpPhi7Hk/ZNR0ftP737Hs/dNoCLCIaVNgYzBZhgo4kd220TeJu2ttW0XZ"
            + "ldyShtpcOmyWKBgVseixR6L/3sspPHyAPXkSuRo0Eh1xvzDKCg9ttb0qoacTlXMF"
            + "GkBpNzmVq67NWFGGa9UElift1mv6RfktPCAGZ+Ai8xUiKAUB0Eookpt/8gX9Senq"
            + "yP/jMxkxXmHWxUu8+KnLvj6WLrfftuuD7u3cfc7j5kkrheDz3O4h4477GnqL5wdo"
            + "9DuEsNc4FxJVz8Iy8RS6cJuW4pihYpM1Tyn7uopLnImpYzEY+R5aQqqr+q/A1diq"
            + "ogbEKPH6oUiqJUwq3nD70gPBUKJmIzS4vLwLouqUHEm1k/MgHV/BkEU0uVHszPFa"
            + "XUMMCHb0iT9P8LuZ7Ajer3SR/0TRVApCrk/6OV68e+6k/OFpM5kcZnNMD5ANyBri"
            + "Tsz3NrDwSw4i4+Dsfh6A9dB/cEghw4skLaBxnQLQIgVeqCzK";

    /**
     * The X509 certificate of the trusted root CA cert for the recoverable key store service.
     *
     * TODO: Change it to the production certificate root CA before the final launch.
     */
    private static final X509Certificate GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_CERTIFICATE =
            parseGoogleCloudKeyVaultServiceV1Certificate();
            parseBase64Certificate(GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_BASE64);

    private static final int NUMBER_OF_ROOT_CERTIFICATES = 1;

@@ -107,9 +171,9 @@ public final class TrustedRootCertificates {
        return certificates;
    }

    private static X509Certificate parseGoogleCloudKeyVaultServiceV1Certificate() {
    private static X509Certificate parseBase64Certificate(String base64Certificate) {
        try {
            return decodeBase64Cert(GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_BASE64);
            return decodeBase64Cert(base64Certificate);
        } catch (CertificateException e) {
            // Should not happen
            throw new RuntimeException(e);
+24 −20
Original line number Diff line number Diff line
@@ -79,6 +79,7 @@ public class KeySyncTask implements Runnable {
    private final PlatformKeyManager mPlatformKeyManager;
    private final RecoverySnapshotStorage mRecoverySnapshotStorage;
    private final RecoverySnapshotListenersStorage mSnapshotListenersStorage;
    private final TestOnlyInsecureCertificateHelper mTestOnlyInsecureCertificateHelper;

    public static KeySyncTask newInstance(
            Context context,
@@ -98,7 +99,8 @@ public class KeySyncTask implements Runnable {
                credentialType,
                credential,
                credentialUpdated,
                PlatformKeyManager.getInstance(context, recoverableKeyStoreDb));
                PlatformKeyManager.getInstance(context, recoverableKeyStoreDb),
                new TestOnlyInsecureCertificateHelper());
    }

    /**
@@ -110,6 +112,7 @@ public class KeySyncTask implements Runnable {
     * @param credential The credential, encoded as a {@link String}.
     * @param credentialUpdated signals weather credentials were updated.
     * @param platformKeyManager platform key manager
     * @param TestOnlyInsecureCertificateHelper utility class used for end-to-end tests
     */
    @VisibleForTesting
    KeySyncTask(
@@ -120,7 +123,8 @@ public class KeySyncTask implements Runnable {
            int credentialType,
            String credential,
            boolean credentialUpdated,
            PlatformKeyManager platformKeyManager) {
            PlatformKeyManager platformKeyManager,
            TestOnlyInsecureCertificateHelper TestOnlyInsecureCertificateHelper) {
        mSnapshotListenersStorage = recoverySnapshotListenersStorage;
        mRecoverableKeyStoreDb = recoverableKeyStoreDb;
        mUserId = userId;
@@ -129,6 +133,7 @@ public class KeySyncTask implements Runnable {
        mCredentialUpdated = credentialUpdated;
        mPlatformKeyManager = platformKeyManager;
        mRecoverySnapshotStorage = snapshotStorage;
        mTestOnlyInsecureCertificateHelper = TestOnlyInsecureCertificateHelper;
    }

    @Override
@@ -189,8 +194,9 @@ public class KeySyncTask implements Runnable {
        PublicKey publicKey;
        String rootCertAlias =
                mRecoverableKeyStoreDb.getActiveRootOfTrust(mUserId, recoveryAgentUid);
        rootCertAlias = mTestOnlyInsecureCertificateHelper
                .getDefaultCertificateAliasIfEmpty(rootCertAlias);

        rootCertAlias = replaceEmptyValueWithSecureDefault(rootCertAlias);
        CertPath certPath = mRecoverableKeyStoreDb.getRecoveryServiceCertPath(mUserId,
                recoveryAgentUid, rootCertAlias);
        if (certPath != null) {
@@ -212,12 +218,18 @@ public class KeySyncTask implements Runnable {
            return;
        }

        // The only place in this class which uses credential value
        if (!TrustedRootCertificates.GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_ALIAS.equals(
                rootCertAlias)) {
            // TODO: allow only whitelisted LSKF usage
            Log.w(TAG, "Untrusted root certificate is used by recovery agent "
        if (mTestOnlyInsecureCertificateHelper.isTestOnlyCertificate(rootCertAlias)) {
            Log.w(TAG, "Insecure root certificate is used by recovery agent "
                    + recoveryAgentUid);
            if (mTestOnlyInsecureCertificateHelper.doesCredentailSupportInsecureMode(
                    mCredentialType, mCredential)) {
                Log.w(TAG, "Whitelisted credential is used to generate snapshot by "
                        + "recovery agent "+ recoveryAgentUid);
            } else {
                Log.w(TAG, "Non whitelisted credential is used to generate recovery snapshot by "
                        + recoveryAgentUid + " - ignore attempt.");
                return; // User secret will not be used.
            }
        }

        byte[] salt = generateSalt();
@@ -239,8 +251,10 @@ public class KeySyncTask implements Runnable {
            return;
        }

        // TODO: filter raw keys based on the root of trust.
        // It is the only place in the class where raw key material is used.
        // Only include insecure key material for test
        if (mTestOnlyInsecureCertificateHelper.isTestOnlyCertificate(rootCertAlias)) {
            rawKeys = mTestOnlyInsecureCertificateHelper.keepOnlyWhitelistedInsecureKeys(rawKeys);
        }
        SecretKey recoveryKey;
        try {
            recoveryKey = generateRecoveryKey();
@@ -467,14 +481,4 @@ public class KeySyncTask implements Runnable {
        }
        return keyEntries;
    }

    private @NonNull String replaceEmptyValueWithSecureDefault(
            @Nullable String rootCertificateAlias) {
        if (rootCertificateAlias == null || rootCertificateAlias.isEmpty()) {
            Log.e(TAG, "rootCertificateAlias is null or empty");
            // Use the default Google Key Vault Service CA certificate if the alias is not provided
            rootCertificateAlias = TrustedRootCertificates.GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_ALIAS;
        }
        return rootCertificateAlias;
    }
}
+18 −30
Original line number Diff line number Diff line
@@ -38,7 +38,6 @@ import android.security.keystore.recovery.KeyChainProtectionParams;
import android.security.keystore.recovery.KeyChainSnapshot;
import android.security.keystore.recovery.RecoveryCertPath;
import android.security.keystore.recovery.RecoveryController;
import android.security.keystore.recovery.TrustedRootCertificates;
import android.security.keystore.recovery.WrappedApplicationKey;
import android.security.KeyStore;
import android.util.ArrayMap;
@@ -100,6 +99,7 @@ public class RecoverableKeyStoreManager {
    private final RecoverySnapshotStorage mSnapshotStorage;
    private final PlatformKeyManager mPlatformKeyManager;
    private final ApplicationKeyStorage mApplicationKeyStorage;
    private final TestOnlyInsecureCertificateHelper mTestCertHelper;

    /**
     * Returns a new or existing instance.
@@ -130,7 +130,8 @@ public class RecoverableKeyStoreManager {
                    RecoverySnapshotStorage.newInstance(),
                    new RecoverySnapshotListenersStorage(),
                    platformKeyManager,
                    applicationKeyStorage);
                    applicationKeyStorage,
                    new TestOnlyInsecureCertificateHelper());
        }
        return mInstance;
    }
@@ -144,7 +145,8 @@ public class RecoverableKeyStoreManager {
            RecoverySnapshotStorage snapshotStorage,
            RecoverySnapshotListenersStorage listenersStorage,
            PlatformKeyManager platformKeyManager,
            ApplicationKeyStorage applicationKeyStorage) {
            ApplicationKeyStorage applicationKeyStorage,
            TestOnlyInsecureCertificateHelper TestOnlyInsecureCertificateHelper) {
        mContext = context;
        mDatabase = recoverableKeyStoreDb;
        mRecoverySessionStorage = recoverySessionStorage;
@@ -153,6 +155,7 @@ public class RecoverableKeyStoreManager {
        mSnapshotStorage = snapshotStorage;
        mPlatformKeyManager = platformKeyManager;
        mApplicationKeyStorage = applicationKeyStorage;
        mTestCertHelper = TestOnlyInsecureCertificateHelper;

        try {
            mRecoverableKeyGenerator = RecoverableKeyGenerator.newInstance(mDatabase);
@@ -171,7 +174,8 @@ public class RecoverableKeyStoreManager {
        checkRecoverKeyStorePermission();
        int userId = UserHandle.getCallingUserId();
        int uid = Binder.getCallingUid();
        rootCertificateAlias = replaceEmptyValueWithSecureDefault(rootCertificateAlias);
        rootCertificateAlias
                = mTestCertHelper.getDefaultCertificateAliasIfEmpty(rootCertificateAlias);

        // Always set active alias to the argument of the last call to initRecoveryService method,
        // even if cert file is incorrect.
@@ -216,7 +220,8 @@ public class RecoverableKeyStoreManager {

        // Randomly choose and validate an endpoint certificate from the list
        CertPath certPath;
        X509Certificate rootCert = getRootCertificate(rootCertificateAlias);
        X509Certificate rootCert =
                mTestCertHelper.getRootCertificate(rootCertificateAlias);
        try {
            Log.d(TAG, "Getting and validating a random endpoint certificate");
            certPath = certXml.getRandomEndpointCert(rootCert);
@@ -265,7 +270,8 @@ public class RecoverableKeyStoreManager {
            @NonNull byte[] recoveryServiceSigFile)
            throws RemoteException {
        checkRecoverKeyStorePermission();
        rootCertificateAlias = replaceEmptyValueWithSecureDefault(rootCertificateAlias);
        rootCertificateAlias =
                mTestCertHelper.getDefaultCertificateAliasIfEmpty(rootCertificateAlias);
        Preconditions.checkNotNull(recoveryServiceCertFile, "recoveryServiceCertFile is null");
        Preconditions.checkNotNull(recoveryServiceSigFile, "recoveryServiceSigFile is null");

@@ -279,7 +285,8 @@ public class RecoverableKeyStoreManager {
                    ERROR_BAD_CERTIFICATE_FORMAT, "Failed to parse the sig file.");
        }

        X509Certificate rootCert = getRootCertificate(rootCertificateAlias);
        X509Certificate rootCert =
                mTestCertHelper.getRootCertificate(rootCertificateAlias);
        try {
            sigXml.verifyFileSignature(rootCert, recoveryServiceCertFile);
        } catch (CertValidationException e) {
@@ -519,7 +526,8 @@ public class RecoverableKeyStoreManager {
            @NonNull List<KeyChainProtectionParams> secrets)
            throws RemoteException {
        checkRecoverKeyStorePermission();
        rootCertificateAlias = replaceEmptyValueWithSecureDefault(rootCertificateAlias);
        rootCertificateAlias =
                mTestCertHelper.getDefaultCertificateAliasIfEmpty(rootCertificateAlias);
        Preconditions.checkNotNull(sessionId, "invalid session");
        Preconditions.checkNotNull(verifierCertPath, "verifierCertPath is null");
        Preconditions.checkNotNull(vaultParams, "vaultParams is null");
@@ -534,7 +542,8 @@ public class RecoverableKeyStoreManager {
        }

        try {
            CertUtils.validateCertPath(getRootCertificate(rootCertificateAlias), certPath);
            CertUtils.validateCertPath(
                    mTestCertHelper.getRootCertificate(rootCertificateAlias), certPath);
        } catch (CertValidationException e) {
            Log.e(TAG, "Failed to validate the given cert path", e);
            throw new ServiceSpecificException(ERROR_INVALID_CERTIFICATE, e.getMessage());
@@ -960,27 +969,6 @@ public class RecoverableKeyStoreManager {
        }
    }

    private X509Certificate getRootCertificate(String rootCertificateAlias) throws RemoteException {
        rootCertificateAlias = replaceEmptyValueWithSecureDefault(rootCertificateAlias);
        X509Certificate rootCertificate =
                TrustedRootCertificates.getRootCertificate(rootCertificateAlias);
        if (rootCertificate == null) {
            throw new ServiceSpecificException(
                    ERROR_INVALID_CERTIFICATE, "The provided root certificate alias is invalid");
        }
        return rootCertificate;
    }

    private @NonNull String replaceEmptyValueWithSecureDefault(
            @Nullable String rootCertificateAlias) {
        if (rootCertificateAlias == null || rootCertificateAlias.isEmpty()) {
            Log.e(TAG, "rootCertificateAlias is null or empty");
            // Use the default Google Key Vault Service CA certificate if the alias is not provided
            rootCertificateAlias = TrustedRootCertificates.GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_ALIAS;
        }
        return rootCertificateAlias;
    }

    private void checkRecoverKeyStorePermission() {
        mContext.enforceCallingOrSelfPermission(
                Manifest.permission.RECOVER_KEYSTORE,
+103 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.locksettings.recoverablekeystore;

import static android.security.keystore.recovery.RecoveryController.ERROR_INVALID_CERTIFICATE;

import com.android.internal.widget.LockPatternUtils;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.security.keystore.recovery.TrustedRootCertificates;
import android.util.Log;

import java.util.HashMap;
import java.security.cert.X509Certificate;
import java.util.Map;
import javax.crypto.SecretKey;

/**
 * The class provides helper methods to support end-to-end test with insecure certificate.
 */
public class TestOnlyInsecureCertificateHelper {
    private static final String TAG = "TestCertHelper";

    /**
     * Constructor for the helper class.
     */
    public TestOnlyInsecureCertificateHelper() {
    }

    /**
     * Returns a root certificate installed in the system for given alias.
     * Returns default secure certificate if alias is empty or null.
     * Can return insecure certificate for its alias.
     */
    public @NonNull X509Certificate
            getRootCertificate(String rootCertificateAlias) throws RemoteException {
        rootCertificateAlias = getDefaultCertificateAliasIfEmpty(rootCertificateAlias);
        if (isTestOnlyCertificate(rootCertificateAlias)) {
            return TrustedRootCertificates.getTestOnlyInsecureCertificate();
        }

        X509Certificate rootCertificate =
                TrustedRootCertificates.getRootCertificate(rootCertificateAlias);
        if (rootCertificate == null) {
            throw new ServiceSpecificException(
                    ERROR_INVALID_CERTIFICATE, "The provided root certificate alias is invalid");
        }
        return rootCertificate;
    }

    public @NonNull String getDefaultCertificateAliasIfEmpty(
            @Nullable String rootCertificateAlias) {
        if (rootCertificateAlias == null || rootCertificateAlias.isEmpty()) {
            Log.e(TAG, "rootCertificateAlias is null or empty - use secure default value");
            // Use the default Google Key Vault Service CA certificate if the alias is not provided
            rootCertificateAlias = TrustedRootCertificates.GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_ALIAS;
        }
        return rootCertificateAlias;
    }

    public boolean isTestOnlyCertificate(String rootCertificateAlias) {
        return TrustedRootCertificates.TEST_ONLY_INSECURE_CERTIFICATE_ALIAS
                .equals(rootCertificateAlias);
    }

    public boolean doesCredentailSupportInsecureMode(int credentialType, String credential) {
        return (credentialType == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD)
            && (credential != null)
            && credential.startsWith(TrustedRootCertificates.INSECURE_PASSWORD_PREFIX);
    }

    public Map<String, SecretKey> keepOnlyWhitelistedInsecureKeys(Map<String, SecretKey> rawKeys) {
        if (rawKeys == null) {
            return null;
        }
        Map<String, SecretKey> filteredKeys = new HashMap<>();
        for (Map.Entry<String, SecretKey> entry : rawKeys.entrySet()) {
            String alias = entry.getKey();
            if (alias != null
                    && alias.startsWith(TrustedRootCertificates.INSECURE_KEY_ALIAS_PREFIX)) {
                filteredKeys.put(entry.getKey(), entry.getValue());
                Log.d(TAG, "adding key with insecure alias " + alias + " to the recovery snapshot");
            }
        }
        return filteredKeys;
    }
}
+112 −6

File changed.

Preview size limit exceeded, changes collapsed.

Loading