From 9375c9ce61fc4e780f98922952497c8ad9a57ad4 Mon Sep 17 00:00:00 2001 From: Nikolas Havrikov Date: Wed, 23 Feb 2022 16:18:52 +0100 Subject: [PATCH 1/3] Replace LinkedList by a more performant collection This is a semi-automatic change. See https://errorprone.info/bugpattern/JdkObsolete for the rationale. Test: make Bug: 221046110 Change-Id: I84591e3963b54e69570e77354e54027e17f17293 Merged-In: I84591e3963b54e69570e77354e54027e17f17293 --- .../security/identity/CredstoreIdentityCredential.java | 7 ++++--- .../identity/CredstoreWritableIdentityCredential.java | 5 +++-- .../android/security/identity/PersonalizationData.java | 5 +++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/identity/java/android/security/identity/CredstoreIdentityCredential.java b/identity/java/android/security/identity/CredstoreIdentityCredential.java index 8e011053d2a75..c591c879059c6 100644 --- a/identity/java/android/security/identity/CredstoreIdentityCredential.java +++ b/identity/java/android/security/identity/CredstoreIdentityCredential.java @@ -38,8 +38,9 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.time.Instant; +import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedList; +import java.util.List; import java.util.Map; import javax.crypto.BadPaddingException; @@ -227,7 +228,7 @@ class CredstoreIdentityCredential extends IdentityCredential { throw new RuntimeException("Error decoding certificates", e); } - LinkedList x509Certs = new LinkedList<>(); + ArrayList x509Certs = new ArrayList<>(); for (Certificate cert : certs) { x509Certs.add((X509Certificate) cert); } @@ -384,7 +385,7 @@ class CredstoreIdentityCredential extends IdentityCredential { public @NonNull Collection getAuthKeysNeedingCertification() { try { AuthKeyParcel[] authKeyParcels = mBinder.getAuthKeysNeedingCertification(); - LinkedList x509Certs = new LinkedList<>(); + ArrayList x509Certs = new ArrayList<>(); CertificateFactory factory = CertificateFactory.getInstance("X.509"); for (AuthKeyParcel authKeyParcel : authKeyParcels) { Collection certs = null; diff --git a/identity/java/android/security/identity/CredstoreWritableIdentityCredential.java b/identity/java/android/security/identity/CredstoreWritableIdentityCredential.java index d2e7984ce19f1..1ad70edb7c6fa 100644 --- a/identity/java/android/security/identity/CredstoreWritableIdentityCredential.java +++ b/identity/java/android/security/identity/CredstoreWritableIdentityCredential.java @@ -25,8 +25,9 @@ import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedList; +import java.util.List; class CredstoreWritableIdentityCredential extends WritableIdentityCredential { @@ -61,7 +62,7 @@ class CredstoreWritableIdentityCredential extends WritableIdentityCredential { throw new RuntimeException("Error decoding certificates", e); } - LinkedList x509Certs = new LinkedList<>(); + ArrayList x509Certs = new ArrayList<>(); for (Certificate cert : certs) { x509Certs.add((X509Certificate) cert); } diff --git a/identity/java/android/security/identity/PersonalizationData.java b/identity/java/android/security/identity/PersonalizationData.java index b34f2505a6a63..bdb00fdfb341f 100644 --- a/identity/java/android/security/identity/PersonalizationData.java +++ b/identity/java/android/security/identity/PersonalizationData.java @@ -18,10 +18,11 @@ package android.security.identity; import android.annotation.NonNull; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; -import java.util.LinkedList; +import java.util.List; /** * An object that holds personalization data. @@ -38,7 +39,7 @@ public class PersonalizationData { private PersonalizationData() { } - private LinkedList mProfiles = new LinkedList<>(); + private ArrayList mProfiles = new ArrayList<>(); private LinkedHashMap mNamespaces = new LinkedHashMap<>(); From 55f62fc1252f45803263d9d88a9fe7179d334172 Mon Sep 17 00:00:00 2001 From: David Zeuthen Date: Tue, 4 Oct 2022 13:18:12 -0400 Subject: [PATCH 2/3] identity: Add support for ECDSA auth and don't require session encryption. This adds a new method which allows applications to use mdoc ECDSA authentication instead of mdoc MAC authentication. Additionally, also relax requirements on SessionTranscript so the APIs can be used even when mdoc session encryption isn't being used. Bug: 241912421 Test: atest VtsHalIdentityTargetTest Test: atest android.security.identity.cts Change-Id: I25336f1352102208887531d066ec432a9ae3cd36 --- core/api/current.txt | 1 + .../identity/CredentialDataResult.java | 24 +++++++++++++++++ .../CredstoreCredentialDataResult.java | 5 ++++ .../identity/CredstoreIdentityCredential.java | 13 ++++++++-- .../CredstoreIdentityCredentialStore.java | 26 ++++++++++++++++--- .../CredstorePresentationSession.java | 7 +++-- .../identity/CredstoreResultData.java | 19 +++++++++++--- .../security/identity/IdentityCredential.java | 17 ++++++------ .../identity/PresentationSession.java | 3 ++- .../android/security/identity/ResultData.java | 4 +++ 10 files changed, 100 insertions(+), 19 deletions(-) diff --git a/core/api/current.txt b/core/api/current.txt index 7ef4f770fcd9f..367b88bd2806c 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -37682,6 +37682,7 @@ package android.security.identity { public abstract class CredentialDataResult { method @Nullable public abstract byte[] getDeviceMac(); method @NonNull public abstract byte[] getDeviceNameSpaces(); + method @Nullable public byte[] getDeviceSignature(); method @NonNull public abstract android.security.identity.CredentialDataResult.Entries getDeviceSignedEntries(); method @NonNull public abstract android.security.identity.CredentialDataResult.Entries getIssuerSignedEntries(); method @NonNull public abstract byte[] getStaticAuthenticationData(); diff --git a/identity/java/android/security/identity/CredentialDataResult.java b/identity/java/android/security/identity/CredentialDataResult.java index beb03af463038..dca039a06a0d1 100644 --- a/identity/java/android/security/identity/CredentialDataResult.java +++ b/identity/java/android/security/identity/CredentialDataResult.java @@ -105,6 +105,30 @@ public abstract class CredentialDataResult { */ public abstract @Nullable byte[] getDeviceMac(); + /** + * Returns a signature over the {@code DeviceAuthenticationBytes} CBOR + * specified in {@link #getDeviceNameSpaces()}, to prove to the reader that the data + * is from a trusted credential. + * + *

The signature is made using the authentication private key. See section 9.1.3.4 of + * ISO/IEC 18013-5:2021 for details of this operation. + * + *

If the session transcript or reader ephemeral key wasn't set on the {@link + * PresentationSession} used to obtain this data no signature will be produced and this method + * will return {@code null}. + * + *

This is only implemented in feature version 202301 or later. If not implemented, the call + * fails with {@link UnsupportedOperationException}. See + * {@link android.content.pm.PackageManager#FEATURE_IDENTITY_CREDENTIAL_HARDWARE} for known + * feature versions. + * + * @return A COSE_Sign1 structure as described above or {@code null} if the conditions + * specified above are not met. + */ + public @Nullable byte[] getDeviceSignature() { + throw new UnsupportedOperationException(); + } + /** * Returns the static authentication data associated with the dynamic authentication * key used to MAC the data returned by {@link #getDeviceNameSpaces()}. diff --git a/identity/java/android/security/identity/CredstoreCredentialDataResult.java b/identity/java/android/security/identity/CredstoreCredentialDataResult.java index 7afe3d448bf99..b4fd5d3fe5a7c 100644 --- a/identity/java/android/security/identity/CredstoreCredentialDataResult.java +++ b/identity/java/android/security/identity/CredstoreCredentialDataResult.java @@ -46,6 +46,11 @@ class CredstoreCredentialDataResult extends CredentialDataResult { return mDeviceSignedResult.getMessageAuthenticationCode(); } + @Override + public @Nullable byte[] getDeviceSignature() { + return mDeviceSignedResult.getSignature(); + } + @Override public @NonNull byte[] getStaticAuthenticationData() { return mDeviceSignedResult.getStaticAuthenticationData(); diff --git a/identity/java/android/security/identity/CredstoreIdentityCredential.java b/identity/java/android/security/identity/CredstoreIdentityCredential.java index c591c879059c6..06953ba832896 100644 --- a/identity/java/android/security/identity/CredstoreIdentityCredential.java +++ b/identity/java/android/security/identity/CredstoreIdentityCredential.java @@ -60,16 +60,19 @@ class CredstoreIdentityCredential extends IdentityCredential { private Context mContext; private ICredential mBinder; private CredstorePresentationSession mSession; + private int mFeatureVersion; CredstoreIdentityCredential(Context context, String credentialName, @IdentityCredentialStore.Ciphersuite int cipherSuite, ICredential binder, - @Nullable CredstorePresentationSession session) { + @Nullable CredstorePresentationSession session, + int featureVersion) { mContext = context; mCredentialName = credentialName; mCipherSuite = cipherSuite; mBinder = binder; mSession = session; + mFeatureVersion = featureVersion; } private KeyPair mEphemeralKeyPair = null; @@ -347,12 +350,18 @@ class CredstoreIdentityCredential extends IdentityCredential { } } + byte[] signature = resultParcel.signature; + if (signature != null && signature.length == 0) { + signature = null; + } + byte[] mac = resultParcel.mac; if (mac != null && mac.length == 0) { mac = null; } CredstoreResultData.Builder resultDataBuilder = new CredstoreResultData.Builder( - resultParcel.staticAuthenticationData, resultParcel.deviceNameSpaces, mac); + mFeatureVersion, resultParcel.staticAuthenticationData, + resultParcel.deviceNameSpaces, mac, signature); for (ResultNamespaceParcel resultNamespaceParcel : resultParcel.resultNamespaces) { for (ResultEntryParcel resultEntryParcel : resultNamespaceParcel.entries) { diff --git a/identity/java/android/security/identity/CredstoreIdentityCredentialStore.java b/identity/java/android/security/identity/CredstoreIdentityCredentialStore.java index bbaf0862f923f..d785c3c895b8d 100644 --- a/identity/java/android/security/identity/CredstoreIdentityCredentialStore.java +++ b/identity/java/android/security/identity/CredstoreIdentityCredentialStore.java @@ -19,6 +19,8 @@ package android.security.identity; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.content.pm.FeatureInfo; +import android.content.pm.PackageManager; import android.os.RemoteException; import android.os.ServiceManager; import android.security.GenerateRkpKey; @@ -30,10 +32,28 @@ class CredstoreIdentityCredentialStore extends IdentityCredentialStore { private Context mContext = null; private ICredentialStore mStore = null; + private int mFeatureVersion; + + static int getFeatureVersion(@NonNull Context context) { + PackageManager pm = context.getPackageManager(); + if (pm.hasSystemFeature(PackageManager.FEATURE_IDENTITY_CREDENTIAL_HARDWARE)) { + FeatureInfo[] infos = pm.getSystemAvailableFeatures(); + for (int n = 0; n < infos.length; n++) { + FeatureInfo info = infos[n]; + if (info.name.equals(PackageManager.FEATURE_IDENTITY_CREDENTIAL_HARDWARE)) { + return info.version; + } + } + } + // Use of the system feature is not required since Android 12. So for Android 11 + // return 202009 which is the feature version shipped with Android 11. + return 202009; + } private CredstoreIdentityCredentialStore(@NonNull Context context, ICredentialStore store) { mContext = context; mStore = store; + mFeatureVersion = getFeatureVersion(mContext); } static CredstoreIdentityCredentialStore getInstanceForType(@NonNull Context context, @@ -139,8 +159,7 @@ class CredstoreIdentityCredentialStore extends IdentityCredentialStore { ICredential credstoreCredential; credstoreCredential = mStore.getCredentialByName(credentialName, cipherSuite); return new CredstoreIdentityCredential(mContext, credentialName, cipherSuite, - credstoreCredential, - null); + credstoreCredential, null, mFeatureVersion); } catch (android.os.RemoteException e) { throw new RuntimeException("Unexpected RemoteException ", e); } catch (android.os.ServiceSpecificException e) { @@ -182,7 +201,8 @@ class CredstoreIdentityCredentialStore extends IdentityCredentialStore { throws CipherSuiteNotSupportedException { try { ISession credstoreSession = mStore.createPresentationSession(cipherSuite); - return new CredstorePresentationSession(mContext, cipherSuite, this, credstoreSession); + return new CredstorePresentationSession(mContext, cipherSuite, this, credstoreSession, + mFeatureVersion); } catch (android.os.RemoteException e) { throw new RuntimeException("Unexpected RemoteException ", e); } catch (android.os.ServiceSpecificException e) { diff --git a/identity/java/android/security/identity/CredstorePresentationSession.java b/identity/java/android/security/identity/CredstorePresentationSession.java index e3c6689a8914b..96bda5215c6d3 100644 --- a/identity/java/android/security/identity/CredstorePresentationSession.java +++ b/identity/java/android/security/identity/CredstorePresentationSession.java @@ -48,15 +48,18 @@ class CredstorePresentationSession extends PresentationSession { private byte[] mSessionTranscript = null; private boolean mOperationHandleSet = false; private long mOperationHandle = 0; + private int mFeatureVersion = 0; CredstorePresentationSession(Context context, @IdentityCredentialStore.Ciphersuite int cipherSuite, CredstoreIdentityCredentialStore store, - ISession binder) { + ISession binder, + int featureVersion) { mContext = context; mCipherSuite = cipherSuite; mStore = store; mBinder = binder; + mFeatureVersion = featureVersion; } private void ensureEphemeralKeyPair() { @@ -147,7 +150,7 @@ class CredstorePresentationSession extends PresentationSession { mBinder.getCredentialForPresentation(credentialName); credential = new CredstoreIdentityCredential(mContext, credentialName, mCipherSuite, credstoreCredential, - this); + this, mFeatureVersion); mCredentialCache.put(credentialName, credential); credential.setAllowUsingExhaustedKeys(request.isAllowUsingExhaustedKeys()); diff --git a/identity/java/android/security/identity/CredstoreResultData.java b/identity/java/android/security/identity/CredstoreResultData.java index 2ef735eec81d9..57c04369203c1 100644 --- a/identity/java/android/security/identity/CredstoreResultData.java +++ b/identity/java/android/security/identity/CredstoreResultData.java @@ -30,10 +30,11 @@ import java.util.Map; * data requested from a {@link IdentityCredential}. */ class CredstoreResultData extends ResultData { - + int mFeatureVersion = 0; byte[] mStaticAuthenticationData = null; byte[] mAuthenticatedData = null; byte[] mMessageAuthenticationCode = null; + byte[] mSignature = null; private Map> mData = new LinkedHashMap<>(); @@ -60,6 +61,14 @@ class CredstoreResultData extends ResultData { return mMessageAuthenticationCode; } + @Override + @Nullable byte[] getSignature() { + if (mFeatureVersion < 202301) { + throw new UnsupportedOperationException(); + } + return mSignature; + } + @Override public @NonNull byte[] getStaticAuthenticationData() { return mStaticAuthenticationData; @@ -124,13 +133,17 @@ class CredstoreResultData extends ResultData { static class Builder { private CredstoreResultData mResultData; - Builder(byte[] staticAuthenticationData, + Builder(int featureVersion, + byte[] staticAuthenticationData, byte[] authenticatedData, - byte[] messageAuthenticationCode) { + byte[] messageAuthenticationCode, + byte[] signature) { this.mResultData = new CredstoreResultData(); + this.mResultData.mFeatureVersion = featureVersion; this.mResultData.mStaticAuthenticationData = staticAuthenticationData; this.mResultData.mAuthenticatedData = authenticatedData; this.mResultData.mMessageAuthenticationCode = messageAuthenticationCode; + this.mResultData.mSignature = signature; } private Map getOrCreateInnerMap(String namespaceName) { diff --git a/identity/java/android/security/identity/IdentityCredential.java b/identity/java/android/security/identity/IdentityCredential.java index f440b693a5b37..3988c0c82b6e8 100644 --- a/identity/java/android/security/identity/IdentityCredential.java +++ b/identity/java/android/security/identity/IdentityCredential.java @@ -236,14 +236,15 @@ public abstract class IdentityCredential { * IntentToRetain = bool * * - *

If the {@code sessionTranscript} parameter is not {@code null}, the X and Y coordinates - * of the public part of the key-pair previously generated by {@link #createEphemeralKeyPair()} - * must appear somewhere in the bytes of the CBOR. Each of these coordinates must appear - * encoded with the most significant bits first and use the exact amount of bits indicated by - * the key size of the ephemeral keys. For example, if the ephemeral key is using the P-256 - * curve then the 32 bytes for the X coordinate encoded with the most significant bits first - * must appear somewhere in {@code sessionTranscript} and ditto for the 32 bytes for the Y - * coordinate. + *

If mdoc session encryption is used (e.g. if {@link #createEphemeralKeyPair()} has been + * called) and if the {@code sessionTranscript} parameter is not {@code null}, the X and Y + * coordinates of the public part of the key-pair previously generated by + * {@link #createEphemeralKeyPair()} must appear somewhere in the bytes of the CBOR. Each of + * these coordinates must appear encoded with the most significant bits first and use the + * exact amount of bits indicated by the key size of the ephemeral keys. For example, if the + * ephemeral key is using the P-256 curve then the 32 bytes for the X coordinate encoded with + * the most significant bits first must appear somewhere in {@code sessionTranscript} and + * ditto for the 32 bytes for the Y coordinate. * *

If {@code readerSignature} is not {@code null} it must be the bytes of a * {@code COSE_Sign1} structure as defined in RFC 8152. For the payload nil shall be used and diff --git a/identity/java/android/security/identity/PresentationSession.java b/identity/java/android/security/identity/PresentationSession.java index 6cde611fcd638..f392e121becab 100644 --- a/identity/java/android/security/identity/PresentationSession.java +++ b/identity/java/android/security/identity/PresentationSession.java @@ -73,7 +73,8 @@ public abstract class PresentationSession { *

If called, this must be called before any calls to * {@link #getCredentialData(String, CredentialDataRequest)}. * - *

The X and Y coordinates of the public part of the key-pair returned by {@link + *

If mdoc session encryption is used (e.g. if {@link #getEphemeralKeyPair()} has been + * called) then the X and Y coordinates of the public part of the key-pair returned by {@link * #getEphemeralKeyPair()} must appear somewhere in the bytes of the passed in CBOR. Each of * these coordinates must appear encoded with the most significant bits first and use the exact * amount of bits indicated by the key size of the ephemeral keys. For example, if the diff --git a/identity/java/android/security/identity/ResultData.java b/identity/java/android/security/identity/ResultData.java index d46f9854b278b..8a0e56ed3184f 100644 --- a/identity/java/android/security/identity/ResultData.java +++ b/identity/java/android/security/identity/ResultData.java @@ -134,6 +134,10 @@ public abstract class ResultData { */ public abstract @Nullable byte[] getMessageAuthenticationCode(); + @Nullable byte[] getSignature() { + throw new UnsupportedOperationException(); + } + /** * Returns the static authentication data associated with the dynamic authentication * key used to sign or MAC the data returned by {@link #getAuthenticatedData()}. From c505485c2dff2b91dd627b679a13ea2db55e12d3 Mon Sep 17 00:00:00 2001 From: David Zeuthen Date: Mon, 14 Nov 2022 15:17:07 -0500 Subject: [PATCH 3/3] identity: Add support for setting minimum validity period for AuthKey. This change adds support for specifying that an AuthKey should be replaced if it's going to expire within a certain amount of time configurable by the application. This also adds a way for the application to learn about the expiration time of currently configured AuthKeys. Combined these two changes allow an application to get a perfect picture of which AuthKeys are available, when they expire, and allows the application to refresh AuthKeys well ahead of expiration dates. Also remove wrong comment that storeStaticAuthenticationDate() variant taking an expiration is only available in feature version 202101. It's available on all feature versions. Bug: 241912421 Test: atest VtsHalIdentityTargetTest Test: atest android.security.identity.cts Change-Id: Ib79da64abfa25b37ed73a37ce78fedd4ef7d1ece --- core/api/current.txt | 11 +++- .../identity/AuthenticationKeyMetadata.java | 53 ++++++++++++++++ .../identity/CredstoreIdentityCredential.java | 36 ++++++++++- .../security/identity/IdentityCredential.java | 62 ++++++++++++++++--- 4 files changed, 152 insertions(+), 10 deletions(-) create mode 100644 identity/java/android/security/identity/AuthenticationKeyMetadata.java diff --git a/core/api/current.txt b/core/api/current.txt index 367b88bd2806c..579ddd8f17a78 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -37652,6 +37652,11 @@ package android.security.identity { ctor public AlreadyPersonalizedException(@NonNull String, @NonNull Throwable); } + public class AuthenticationKeyMetadata { + method @NonNull public java.time.Instant getExpirationDate(); + method @IntRange(from=0) public int getUsageCount(); + } + public class CipherSuiteNotSupportedException extends android.security.identity.IdentityCredentialException { ctor public CipherSuiteNotSupportedException(@NonNull String); ctor public CipherSuiteNotSupportedException(@NonNull String, @NonNull Throwable); @@ -37719,13 +37724,15 @@ package android.security.identity { method @NonNull public byte[] delete(@NonNull byte[]); method @Deprecated @NonNull public abstract byte[] encryptMessageToReader(@NonNull byte[]); method @NonNull public abstract java.util.Collection getAuthKeysNeedingCertification(); - method @NonNull public abstract int[] getAuthenticationDataUsageCount(); + method @Deprecated @NonNull public abstract int[] getAuthenticationDataUsageCount(); + method @NonNull public java.util.List getAuthenticationKeyMetadata(); method @NonNull public abstract java.util.Collection getCredentialKeyCertificateChain(); method @Deprecated @NonNull public abstract android.security.identity.ResultData getEntries(@Nullable byte[], @NonNull java.util.Map>, @Nullable byte[], @Nullable byte[]) throws android.security.identity.EphemeralPublicKeyNotFoundException, android.security.identity.InvalidReaderSignatureException, android.security.identity.InvalidRequestMessageException, android.security.identity.NoAuthenticationKeyAvailableException, android.security.identity.SessionTranscriptMismatchException; method @NonNull public byte[] proveOwnership(@NonNull byte[]); method @Deprecated public abstract void setAllowUsingExhaustedKeys(boolean); method @Deprecated public void setAllowUsingExpiredKeys(boolean); - method public abstract void setAvailableAuthenticationKeys(int, int); + method @Deprecated public abstract void setAvailableAuthenticationKeys(int, int); + method public void setAvailableAuthenticationKeys(@IntRange(from=0) int, @IntRange(from=1) int, @IntRange(from=0) long); method @Deprecated public abstract void setReaderEphemeralPublicKey(@NonNull java.security.PublicKey) throws java.security.InvalidKeyException; method @Deprecated public abstract void storeStaticAuthenticationData(@NonNull java.security.cert.X509Certificate, @NonNull byte[]) throws android.security.identity.UnknownAuthenticationKeyException; method public void storeStaticAuthenticationData(@NonNull java.security.cert.X509Certificate, @NonNull java.time.Instant, @NonNull byte[]) throws android.security.identity.UnknownAuthenticationKeyException; diff --git a/identity/java/android/security/identity/AuthenticationKeyMetadata.java b/identity/java/android/security/identity/AuthenticationKeyMetadata.java new file mode 100644 index 0000000000000..d4c28f8d459af --- /dev/null +++ b/identity/java/android/security/identity/AuthenticationKeyMetadata.java @@ -0,0 +1,53 @@ +/* + * Copyright 2019 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 android.security.identity; + +import android.annotation.IntRange; +import android.annotation.NonNull; + +import java.time.Instant; + +/** + * Data about authentication keys. + */ +public class AuthenticationKeyMetadata { + private int mUsageCount; + private Instant mExpirationDate; + + AuthenticationKeyMetadata(int usageCount, Instant expirationDate) { + mUsageCount = usageCount; + mExpirationDate = expirationDate; + } + + /** + * Gets usage count for the authentication key. + * + * @return the usage count + */ + public @IntRange(from = 0) int getUsageCount() { + return mUsageCount; + } + + /** + * Gets expiration date for the authentication key. + * + * @return the expiration date of the authentication key. + */ + public @NonNull Instant getExpirationDate() { + return mExpirationDate; + } +} diff --git a/identity/java/android/security/identity/CredstoreIdentityCredential.java b/identity/java/android/security/identity/CredstoreIdentityCredential.java index 06953ba832896..449c7a7067532 100644 --- a/identity/java/android/security/identity/CredstoreIdentityCredential.java +++ b/identity/java/android/security/identity/CredstoreIdentityCredential.java @@ -380,8 +380,14 @@ class CredstoreIdentityCredential extends IdentityCredential { @Override public void setAvailableAuthenticationKeys(int keyCount, int maxUsesPerKey) { + setAvailableAuthenticationKeys(keyCount, maxUsesPerKey, 0); + } + + @Override + public void setAvailableAuthenticationKeys(int keyCount, int maxUsesPerKey, + long minValidTimeMillis) { try { - mBinder.setAvailableAuthenticationKeys(keyCount, maxUsesPerKey); + mBinder.setAvailableAuthenticationKeys(keyCount, maxUsesPerKey, minValidTimeMillis); } catch (android.os.RemoteException e) { throw new RuntimeException("Unexpected RemoteException ", e); } catch (android.os.ServiceSpecificException e) { @@ -480,6 +486,34 @@ class CredstoreIdentityCredential extends IdentityCredential { } } + @Override + public @NonNull List getAuthenticationKeyMetadata() { + try { + int[] usageCount = mBinder.getAuthenticationDataUsageCount(); + long[] expirationsMillis = mBinder.getAuthenticationDataExpirations(); + if (usageCount.length != expirationsMillis.length) { + throw new IllegalStateException("Size og usageCount and expirationMillis differ"); + } + List mds = new ArrayList<>(); + for (int n = 0; n < expirationsMillis.length; n++) { + AuthenticationKeyMetadata md = null; + long expirationMillis = expirationsMillis[n]; + if (expirationMillis != Long.MAX_VALUE) { + md = new AuthenticationKeyMetadata( + usageCount[n], + Instant.ofEpochMilli(expirationMillis)); + } + mds.add(md); + } + return mds; + } catch (android.os.RemoteException e) { + throw new IllegalStateException("Unexpected RemoteException ", e); + } catch (android.os.ServiceSpecificException e) { + throw new IllegalStateException("Unexpected ServiceSpecificException with code " + + e.errorCode, e); + } + } + @Override public @NonNull byte[] proveOwnership(@NonNull byte[] challenge) { try { diff --git a/identity/java/android/security/identity/IdentityCredential.java b/identity/java/android/security/identity/IdentityCredential.java index 3988c0c82b6e8..2dd9778a6fe70 100644 --- a/identity/java/android/security/identity/IdentityCredential.java +++ b/identity/java/android/security/identity/IdentityCredential.java @@ -16,6 +16,7 @@ package android.security.identity; +import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; @@ -25,6 +26,7 @@ import java.security.PublicKey; import java.security.cert.X509Certificate; import java.time.Instant; import java.util.Collection; +import java.util.List; import java.util.Map; /** @@ -297,7 +299,7 @@ public abstract class IdentityCredential { * session transcripts. * @throws NoAuthenticationKeyAvailableException if authentication keys were never * provisioned, the method - * {@link #setAvailableAuthenticationKeys(int, int)} + * {@link #setAvailableAuthenticationKeys(int, int, long)} * was called with {@code keyCount} set to 0, * the method * {@link #setAllowUsingExhaustedKeys(boolean)} @@ -331,19 +333,25 @@ public abstract class IdentityCredential { * for which this method has not been called behave as though it had been called wit * {@code keyCount} 0 and {@code maxUsesPerKey} 1. * + *

The effect of this method is like calling + * {@link #setAvailableAuthenticationKeys(int, int, long)} with the last parameter is set to 0. + * * @param keyCount The number of active, certified dynamic authentication keys the * {@code IdentityCredential} will try to keep available. This value * must be non-negative. * @param maxUsesPerKey The maximum number of times each of the keys will be used before it's * eligible for replacement. This value must be greater than zero. + * @deprecated Use {@link #setAvailableAuthenticationKeys(int, int, long)} instead. */ + @Deprecated public abstract void setAvailableAuthenticationKeys(int keyCount, int maxUsesPerKey); /** * Gets a collection of dynamic authentication keys that need certification. * *

When there aren't enough certified dynamic authentication keys, either because the key - * count has been increased or because one or more keys have reached their usage count, this + * count has been increased or because one or more keys have reached their usage count or + * it if a key is too close to its expiration date, this * method will generate replacement keys and certificates and return them for issuer * certification. The issuer certificates and associated static authentication data must then * be provided back to the Identity Credential using @@ -401,11 +409,6 @@ public abstract class IdentityCredential { * This should only be called for an authenticated key returned by * {@link #getAuthKeysNeedingCertification()}. * - *

This is only implemented in feature version 202101 or later. If not implemented, the call - * fails with {@link UnsupportedOperationException}. See - * {@link android.content.pm.PackageManager#FEATURE_IDENTITY_CREDENTIAL_HARDWARE} for known - * feature versions. - * * @param authenticationKey The dynamic authentication key for which certification and * associated static * authentication data is being provided. @@ -427,7 +430,9 @@ public abstract class IdentityCredential { * Get the number of times the dynamic authentication keys have been used. * * @return int array of dynamic authentication key usage counts. + * @deprecated Use {@link #getAuthenticationKeyMetadata()} instead. */ + @Deprecated public @NonNull abstract int[] getAuthenticationDataUsageCount(); /** @@ -520,4 +525,47 @@ public abstract class IdentityCredential { public @NonNull byte[] update(@NonNull PersonalizationData personalizationData) { throw new UnsupportedOperationException(); } + + /** + * Sets the number of dynamic authentication keys the {@code IdentityCredential} will maintain, + * the number of times each should be used, and the minimum amount of time it's valid for. + * + *

The Identity Credential system will select the least-used dynamic authentication key each + * time {@link #getEntries(byte[], Map, byte[], byte[])} is called. Identity Credentials + * for which this method has not been called behave as though it had been called wit + * {@code keyCount} 0, {@code maxUsesPerKey} 1, and {@code minValidTimeMillis} 0. + * + *

Applications can use {@link #getAuthenticationKeyMetadata()} to get a picture of the + * usage andtime left of each configured authentication key. This can be used to determine + * how urgent it is recertify new authentication keys via the + * {@link #getAuthKeysNeedingCertification()} method. + * + * @param keyCount The number of active, certified dynamic authentication keys the + * {@code IdentityCredential} will try to keep available. This value + * must be non-negative. + * @param maxUsesPerKey The maximum number of times each of the keys will be used before it's + * eligible for replacement. This value must be greater than zero. + * @param minValidTimeMillis If a key has less time left than this value it will be eliglible + * for replacement. This value must be non-negative. + */ + public void setAvailableAuthenticationKeys( + @IntRange(from = 0) int keyCount, + @IntRange(from = 1) int maxUsesPerKey, + @IntRange(from = 0) long minValidTimeMillis) { + throw new UnsupportedOperationException(); + } + + /** + * Get information about dynamic authentication keys. + * + *

The returned list may have null values if certification for the dynamic + * authentication key is pending. + * + *

The list is always keyCount elements long. + * + * @return list of authentication key metadata objects. + */ + public @NonNull List getAuthenticationKeyMetadata() { + throw new UnsupportedOperationException(); + } }