summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Eran Messeri <eranm@google.com> 2022-03-29 13:52:00 +0100
committer Eran Messeri <eranm@google.com> 2022-03-30 15:03:16 +0100
commitfda47fb1caa62e2c0340602cd0a4d2baf3d7a4ab (patch)
treeca35d02fb44b7e38e0b8bb621e10a5e4e1e7b839
parent7436573ad9e3319d7aec0438cf0326fcbbf01257 (diff)
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
-rw-r--r--core/api/current.txt5
-rw-r--r--keystore/java/android/security/KeyStoreException.java153
-rw-r--r--keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java58
3 files changed, 203 insertions, 13 deletions
diff --git a/core/api/current.txt b/core/api/current.txt
index d03ab96fbf46..dd7830996441 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -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 {
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) {