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

Commit fda47fb1 authored by Eran Messeri's avatar Eran Messeri
Browse files

KeyStore: Surface RKP failures

On systems that rely solely on remotely-provisioned keys (RKP),
the attestation keys may run out or be unavailable for attesting
a newly-generated key. This could happen when:
* the device first connects to the Internet
* The device had all the keys used and:
 ** It hadn't yet completed obtaining new ones.
 ** The RKP server declines to issue new keys.

In these cases, the caller must be informed that their key generation
request failed (likely temporarily), and that they should retry it.

The retry policy returned tells the caller when to re-try.
Bug: 227306369
Test: atest android.keystore.cts.KeyStoreExceptionTest

Change-Id: Ief30a3ab97da95b68d172e725c38acbefab92fa9
parent 7436573a
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -37575,10 +37575,12 @@ package android.security {
  public class KeyStoreException extends java.lang.Exception {
    method public int getNumericErrorCode();
    method public int getRetryPolicy();
    method public boolean isSystemError();
    method public boolean isTransientFailure();
    method public boolean requiresUserAuthentication();
    field public static final int ERROR_ATTESTATION_CHALLENGE_TOO_LARGE = 9; // 0x9
    field public static final int ERROR_ATTESTATION_KEYS_UNAVAILABLE = 16; // 0x10
    field public static final int ERROR_ID_ATTESTATION_FAILURE = 8; // 0x8
    field public static final int ERROR_INCORRECT_USAGE = 13; // 0xd
    field public static final int ERROR_INTERNAL_SYSTEM_ERROR = 4; // 0x4
@@ -37593,6 +37595,9 @@ package android.security {
    field public static final int ERROR_PERMISSION_DENIED = 5; // 0x5
    field public static final int ERROR_UNIMPLEMENTED = 12; // 0xc
    field public static final int ERROR_USER_AUTHENTICATION_REQUIRED = 2; // 0x2
    field public static final int RETRY_NEVER = 1; // 0x1
    field public static final int RETRY_WHEN_CONNECTIVITY_AVAILABLE = 3; // 0x3
    field public static final int RETRY_WITH_EXPONENTIAL_BACKOFF = 2; // 0x2
  }
  @Deprecated public final class KeyStoreParameter implements java.security.KeyStore.ProtectionParameter {
+152 −1
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.annotation.Nullable;
import android.annotation.TestApi;
import android.security.keymaster.KeymasterDefs;
import android.system.keystore2.ResponseCode;
import android.util.Log;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -36,6 +37,8 @@ import java.util.Map;
 * is likely to succeed.
 */
public class KeyStoreException extends Exception {
    private static final String TAG = "KeyStoreException";

    /**
     * This error code is for mapping errors that the caller will not know about. If the caller is
     * targeting an API level earlier than the one the error was introduced in, then the error will
@@ -114,6 +117,27 @@ public class KeyStoreException extends Exception {
     * The caller should re-create the crypto object and try again.
     */
    public static final int ERROR_KEY_OPERATION_EXPIRED = 15;
    /**
     * There are no keys available for attestation.
     * This error is returned only on devices that rely solely on remotely-provisioned keys (see
     * <a href=
     * "https://android-developers.googleblog.com/2022/03/upgrading-android-attestation-remote.html"
     * >Remote Key Provisioning</a>).
     *
     * <p>On such a device, if the caller requests key generation and includes an attestation
     * challenge (indicating key attestation is required), the error will be returned in one of
     * the following cases:
     * <ul>
     *     <li>The pool of remotely-provisioned keys has been exhausted.</li>
     *     <li>The device is not registered with the key provisioning server.</li>
     * </ul>
     * </p>
     *
     * <p>This error is a transient one if the pool of remotely-provisioned keys has been
     * exhausted. However, if the device is not registered with the server, or the key
     * provisioning server refuses key issuance, this is a permanent error.</p>
     */
    public static final int ERROR_ATTESTATION_KEYS_UNAVAILABLE = 16;

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
@@ -132,11 +156,68 @@ public class KeyStoreException extends Exception {
            ERROR_UNIMPLEMENTED,
            ERROR_INCORRECT_USAGE,
            ERROR_KEY_NOT_TEMPORALLY_VALID,
            ERROR_KEY_OPERATION_EXPIRED
            ERROR_KEY_OPERATION_EXPIRED,
            ERROR_ATTESTATION_KEYS_UNAVAILABLE
    })
    public @interface PublicErrorCode {
    }

    /**
     * Never re-try the operation that led to this error, since it's a permanent error.
     *
     * This value is always returned when {@link #isTransientFailure()} is {@code false}.
     */
    public static final int RETRY_NEVER = 1;
    /**
     * Re-try the operation that led to this error with an exponential back-off delay.
     * The first delay should be between 5 to 30 seconds, and each subsequent re-try should double
     * the delay time.
     *
     * This value is returned when {@link #isTransientFailure()} is {@code true}.
     */
    public static final int RETRY_WITH_EXPONENTIAL_BACKOFF = 2;
    /**
     * Re-try the operation that led to this error when the device regains connectivity.
     * Remote provisioning of keys requires reaching the remote server, and the device is
     * currently unable to due that due to lack of network connectivity.
     *
     * This value is returned when {@link #isTransientFailure()} is {@code true}.
     */
    public static final int RETRY_WHEN_CONNECTIVITY_AVAILABLE = 3;

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(flag = true, prefix = {"RETRY_"}, value = {
            RETRY_NEVER,
            RETRY_WITH_EXPONENTIAL_BACKOFF,
            RETRY_WHEN_CONNECTIVITY_AVAILABLE,
    })
    public @interface RetryPolicy {
    }

    // RKP-specific error information.
    /**
     * Remote provisioning of attestation keys has completed successfully.
     * @hide */
    public static final int RKP_SUCCESS = 0;
    /**
     * Remotely-provisioned keys are temporarily unavailable. This could be because of RPC
     * error when talking to the remote provisioner or keys are being currently fetched and will
     * be available soon.
     * @hide */
    public static final int RKP_TEMPORARILY_UNAVAILABLE = 1;
    /**
     * Permanent failure: The RKP server has declined issuance of keys to this device. Either
     * because the device is not registered with the server or the server considers the device
     * not to be trustworthy.
     * @hide */
    public static final int RKP_SERVER_REFUSED_ISSUANCE = 2;
    /**
     * The RKP server is unavailable due to lack of connectivity. The caller should re-try
     * when the device has connectivity again.
     * @hide */
    public static final int RKP_FETCHING_PENDING_CONNECTIVITY = 3;

    // Constants for encoding information about the error encountered:
    // Whether the error relates to the system state/implementation as a whole, or a specific key.
    private static final int IS_SYSTEM_ERROR = 1 << 1;
@@ -148,6 +229,21 @@ public class KeyStoreException extends Exception {
    // The internal error code. NOT to be returned directly to callers or made part of the
    // public API.
    private final int mErrorCode;
    // The Remote Key Provisioning status. Applicable if and only if {@link #mErrorCode} is equal
    // to {@link ResponseCode.OUT_OF_KEYS}.
    private final int mRkpStatus;

    private static int initializeRkpStatusForRegularErrors(int errorCode) {
        // Check if the system code mistakenly called a constructor of KeyStoreException with
        // the OUT_OF_KEYS error code but without RKP status.
        if (errorCode == ResponseCode.OUT_OF_KEYS) {
            Log.e(TAG, "RKP error code without RKP status");
            // Set RKP status to RKP_SERVER_REFUSED_ISSUANCE so that the caller never retries.
            return RKP_SERVER_REFUSED_ISSUANCE;
        } else {
            return RKP_SUCCESS;
        }
    }

    /**
     * @hide
@@ -155,6 +251,7 @@ public class KeyStoreException extends Exception {
    public KeyStoreException(int errorCode, @Nullable String message) {
        super(message);
        mErrorCode = errorCode;
        mRkpStatus = initializeRkpStatusForRegularErrors(errorCode);
    }

    /**
@@ -165,6 +262,19 @@ public class KeyStoreException extends Exception {
        super(message + " (internal Keystore code: " + errorCode + " message: "
                + keystoreErrorMessage + ")");
        mErrorCode = errorCode;
        mRkpStatus = initializeRkpStatusForRegularErrors(errorCode);
    }

    /**
     * @hide
     */
    public KeyStoreException(int errorCode, @Nullable String message, int rkpStatus) {
        super(message);
        mErrorCode = errorCode;
        mRkpStatus = rkpStatus;
        if (mErrorCode != ResponseCode.OUT_OF_KEYS) {
            Log.e(TAG, "Providing RKP status for error code " + errorCode + " has no effect.");
        }
    }

    /**
@@ -198,6 +308,17 @@ public class KeyStoreException extends Exception {
     */
    public boolean isTransientFailure() {
        PublicErrorInformation failureInfo = getErrorInformation(mErrorCode);
        // Special-case handling for RKP failures:
        if (mRkpStatus != RKP_SUCCESS && mErrorCode == ResponseCode.OUT_OF_KEYS) {
            switch (mRkpStatus) {
                case RKP_TEMPORARILY_UNAVAILABLE:
                case RKP_FETCHING_PENDING_CONNECTIVITY:
                    return true;
                case RKP_SERVER_REFUSED_ISSUANCE:
                default:
                    return false;
            }
        }
        return (failureInfo.indicators & IS_TRANSIENT_ERROR) != 0;
    }

@@ -225,6 +346,34 @@ public class KeyStoreException extends Exception {
        return (failureInfo.indicators & IS_SYSTEM_ERROR) != 0;
    }

    /**
     * Returns the re-try policy for transient failures. Valid only if
     * {@link #isTransientFailure()} returns {@code True}.
     */
    @RetryPolicy
    public int getRetryPolicy() {
        PublicErrorInformation failureInfo = getErrorInformation(mErrorCode);
        // Special-case handling for RKP failures:
        if (mRkpStatus != RKP_SUCCESS) {
            switch (mRkpStatus) {
                case RKP_TEMPORARILY_UNAVAILABLE:
                    return RETRY_WITH_EXPONENTIAL_BACKOFF;
                case RKP_FETCHING_PENDING_CONNECTIVITY:
                    return RETRY_WHEN_CONNECTIVITY_AVAILABLE;
                case RKP_SERVER_REFUSED_ISSUANCE:
                    return RETRY_NEVER;
                default:
                    return (failureInfo.indicators & IS_TRANSIENT_ERROR) != 0
                            ? RETRY_WITH_EXPONENTIAL_BACKOFF : RETRY_NEVER;
            }
        }
        if ((failureInfo.indicators & IS_TRANSIENT_ERROR) != 0) {
            return RETRY_WITH_EXPONENTIAL_BACKOFF;
        } else {
            return RETRY_NEVER;
        }
    }

    @Override
    public String toString() {
        String errorCodes = String.format(" (public error code: %d internal Keystore code: %d)",
@@ -469,5 +618,7 @@ public class KeyStoreException extends Exception {
                new PublicErrorInformation(0, ERROR_KEY_CORRUPTED));
        sErrorCodeToFailureInfo.put(ResponseCode.KEY_PERMANENTLY_INVALIDATED,
                new PublicErrorInformation(0, ERROR_KEY_DOES_NOT_EXIST));
        sErrorCodeToFailureInfo.put(ResponseCode.OUT_OF_KEYS,
                new PublicErrorInformation(IS_SYSTEM_ERROR, ERROR_ATTESTATION_KEYS_UNAVAILABLE));
    }
}
+46 −12
Original line number Diff line number Diff line
@@ -28,7 +28,6 @@ import android.hardware.security.keymint.Tag;
import android.os.Build;
import android.os.RemoteException;
import android.security.GenerateRkpKey;
import android.security.GenerateRkpKeyException;
import android.security.KeyPairGeneratorSpec;
import android.security.KeyStore2;
import android.security.KeyStoreException;
@@ -618,18 +617,44 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato

    @Override
    public KeyPair generateKeyPair() {
        try {
            return generateKeyPairHelper();
        } catch (GenerateRkpKeyException e) {
            try {
                return generateKeyPairHelper();
            } catch (GenerateRkpKeyException f) {
                throw new ProviderException("Failed to provision new attestation keys.");
        GenerateKeyPairHelperResult result = new GenerateKeyPairHelperResult(0, null);
        for (int i = 0; i < 2; i++) {
            /**
             * NOTE: There is no need to delay between re-tries because the call to
             * GenerateRkpKey.notifyEmpty() will delay for a while before returning.
             */
            result = generateKeyPairHelper();
            if (result.rkpStatus == KeyStoreException.RKP_SUCCESS) {
                return result.keyPair;
            }
        }

        // RKP failure
        if (result.rkpStatus != KeyStoreException.RKP_SUCCESS) {
            KeyStoreException ksException = new KeyStoreException(ResponseCode.OUT_OF_KEYS,
                    "Could not get RKP keys", result.rkpStatus);
            throw new ProviderException("Failed to provision new attestation keys.", ksException);
        }

        return result.keyPair;
    }

    private KeyPair generateKeyPairHelper() throws GenerateRkpKeyException {
    private static class GenerateKeyPairHelperResult {
        // Zero indicates success, non-zero indicates failure. Values should be
        // {@link android.security.KeyStoreException#RKP_TEMPORARILY_UNAVAILABLE},
        // {@link android.security.KeyStoreException#RKP_SERVER_REFUSED_ISSUANCE},
        // {@link android.security.KeyStoreException#RKP_FETCHING_PENDING_CONNECTIVITY}
        public final int rkpStatus;
        @Nullable
        public final KeyPair keyPair;

        private GenerateKeyPairHelperResult(int rkpStatus, KeyPair keyPair) {
            this.rkpStatus = rkpStatus;
            this.keyPair = keyPair;
        }
    }

    private GenerateKeyPairHelperResult generateKeyPairHelper() {
        if (mKeyStore == null || mSpec == null) {
            throw new IllegalStateException("Not initialized");
        }
@@ -679,7 +704,8 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato
                Log.d(TAG, "Couldn't connect to the RemoteProvisioner backend.", e);
            }
            success = true;
            return new KeyPair(publicKey, publicKey.getPrivateKey());
            KeyPair kp = new KeyPair(publicKey, publicKey.getPrivateKey());
            return new GenerateKeyPairHelperResult(0, kp);
        } catch (android.security.KeyStoreException e) {
            switch (e.getErrorCode()) {
                case KeymasterDefs.KM_ERROR_HARDWARE_TYPE_UNAVAILABLE:
@@ -688,11 +714,19 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato
                    GenerateRkpKey keyGen = new GenerateRkpKey(ActivityThread
                            .currentApplication());
                    try {
                        //TODO: When detailed error information is available from the remote
                        //provisioner, propagate it up.
                        keyGen.notifyEmpty(securityLevel);
                    } catch (RemoteException f) {
                        throw new ProviderException("Failed to talk to RemoteProvisioner", f);
                    }
                    throw new GenerateRkpKeyException();
                        KeyStoreException ksException = new KeyStoreException(
                                ResponseCode.OUT_OF_KEYS,
                                "Remote exception: " + f.getMessage(),
                                KeyStoreException.RKP_TEMPORARILY_UNAVAILABLE);
                        throw new ProviderException("Failed to talk to RemoteProvisioner",
                                ksException);
                    }
                    return new GenerateKeyPairHelperResult(
                            KeyStoreException.RKP_TEMPORARILY_UNAVAILABLE, null);
                default:
                    ProviderException p = new ProviderException("Failed to generate key pair.", e);
                    if ((mSpec.getPurposes() & KeyProperties.PURPOSE_WRAP_KEY) != 0) {