diff options
| author | 2022-04-04 10:32:11 +0000 | |
|---|---|---|
| committer | 2022-04-04 10:32:11 +0000 | |
| commit | a0e091c6f3cb56b29f4fb6026d43ba59ebfabe1c (patch) | |
| tree | 8b9e664282e05a73588fcdb99574e16bff439a20 | |
| parent | d7659e60143ee8073f75092d84bc395bc9efd53a (diff) | |
| parent | 6c82325675fc139788cd59822d16ab872718c170 (diff) | |
Merge "KeyStore: Surface RKP failures" am: 6c82325675
Original change: https://android-review.googlesource.com/c/platform/frameworks/base/+/2049729
Change-Id: Ia726d3005e6971b4cf34c05b7cb70f7511263fd3
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
| -rw-r--r-- | core/api/current.txt | 5 | ||||
| -rw-r--r-- | keystore/java/android/security/KeyStoreException.java | 153 | ||||
| -rw-r--r-- | keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java | 58 |
3 files changed, 203 insertions, 13 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 8f74ae197b73..641b1a3697e2 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -36138,10 +36138,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 @@ -36156,6 +36158,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 { diff --git a/keystore/java/android/security/KeyStoreException.java b/keystore/java/android/security/KeyStoreException.java index 54184dbf6e08..1a81dda8d56c 100644 --- a/keystore/java/android/security/KeyStoreException.java +++ b/keystore/java/android/security/KeyStoreException.java @@ -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)); } } diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java index e7961c94928c..5950b5bc7231 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java @@ -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 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 KeyPair generateKeyPairHelper() throws GenerateRkpKeyException { + 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); + 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); } - throw new GenerateRkpKeyException(); + 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) { |