diff options
author | 2021-09-11 13:46:26 -0400 | |
---|---|---|
committer | 2022-01-11 10:40:51 -0500 | |
commit | 6fc37331a61a6adcaf434f6eecef6b6b3fdaa1ff (patch) | |
tree | ea6eaa082311fb768c55a864e4ea3d789d51f4ad /identity/java | |
parent | 0892c27c57ddbd6076b39393e6680fd4c42c4de0 (diff) |
identity: Add multi-document presentation support.
This new PresentationSession interface enables an application to do a
multi-document presentation, something which isn't possible with the
existing API. As a practical example of this consider presenting both
your Mobile Driving License and your Vaccination Certificate in a single
transaction.
Also update the documentation for IdentityCredential to clarify that
the same AuthKey is used for multiple getEntries() calls on the same
credential.
Also deprecate existing IdentityCredential.getEntries() method and
related methods and classes.
Bug: 197965513
Test: New CTS tests and new screen in CtsVerifier
Change-Id: I74534969143882552407917a82f44d43da12711c
Diffstat (limited to 'identity/java')
10 files changed, 1062 insertions, 10 deletions
diff --git a/identity/java/android/security/identity/CredentialDataRequest.java b/identity/java/android/security/identity/CredentialDataRequest.java new file mode 100644 index 000000000000..2a47a02405e0 --- /dev/null +++ b/identity/java/android/security/identity/CredentialDataRequest.java @@ -0,0 +1,232 @@ +/* + * Copyright 2021 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.NonNull; +import android.annotation.Nullable; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * An object representing a request for credential data. + */ +public class CredentialDataRequest { + CredentialDataRequest() {} + + /** + * Gets the device-signed entries to request. + * + * @return the device-signed entries to request. + */ + public @NonNull Map<String, Collection<String>> getDeviceSignedEntriesToRequest() { + return mDeviceSignedEntriesToRequest; + } + + /** + * Gets the issuer-signed entries to request. + * + * @return the issuer-signed entries to request. + */ + public @NonNull Map<String, Collection<String>> getIssuerSignedEntriesToRequest() { + return mIssuerSignedEntriesToRequest; + } + + /** + * Gets whether to allow using an authentication key which use count has been exceeded. + * + * <p>By default this is set to true. + * + * @return whether to allow using an authentication key which use + * count has been exceeded if no other key is available. + */ + public boolean isAllowUsingExhaustedKeys() { + return mAllowUsingExhaustedKeys; + } + + /** + * Gets whether to allow using an authentication key which is expired. + * + * <p>By default this is set to false. + * + * @return whether to allow using an authentication key which is + * expired if no other key is available. + */ + public boolean isAllowUsingExpiredKeys() { + return mAllowUsingExpiredKeys; + } + + /** + * Gets whether to increment the use-count for the authentication key used. + * + * <p>By default this is set to true. + * + * @return whether to increment the use count of the authentication key used. + */ + public boolean isIncrementUseCount() { + return mIncrementUseCount; + } + + /** + * Gets the request message CBOR. + * + * <p>This data structure is described in the documentation for the + * {@link PresentationSession#getCredentialData(String, CredentialDataRequest)} method. + * + * @return the request message CBOR as described above. + */ + public @Nullable byte[] getRequestMessage() { + return mRequestMessage; + } + + /** + * Gets the reader signature. + * + * <p>This data structure is described in the documentation for the + * {@link PresentationSession#getCredentialData(String, CredentialDataRequest)} method. + * + * @return a {@code COSE_Sign1} structure as described above. + */ + public @Nullable byte[] getReaderSignature() { + return mReaderSignature; + } + + Map<String, Collection<String>> mDeviceSignedEntriesToRequest = new LinkedHashMap<>(); + Map<String, Collection<String>> mIssuerSignedEntriesToRequest = new LinkedHashMap<>(); + boolean mAllowUsingExhaustedKeys = true; + boolean mAllowUsingExpiredKeys = false; + boolean mIncrementUseCount = true; + byte[] mRequestMessage = null; + byte[] mReaderSignature = null; + + /** + * A builder for {@link CredentialDataRequest}. + */ + public static final class Builder { + private CredentialDataRequest mData; + + /** + * Creates a new builder. + */ + public Builder() { + mData = new CredentialDataRequest(); + } + + /** + * Sets the device-signed entries to request. + * + * @param entriesToRequest the device-signed entries to request. + */ + public @NonNull Builder setDeviceSignedEntriesToRequest( + @NonNull Map<String, Collection<String>> entriesToRequest) { + mData.mDeviceSignedEntriesToRequest = entriesToRequest; + return this; + } + + /** + * Sets the issuer-signed entries to request. + * + * @param entriesToRequest the issuer-signed entries to request. + * @return the builder. + */ + public @NonNull Builder setIssuerSignedEntriesToRequest( + @NonNull Map<String, Collection<String>> entriesToRequest) { + mData.mIssuerSignedEntriesToRequest = entriesToRequest; + return this; + } + + /** + * Sets whether to allow using an authentication key which use count has been exceeded. + * + * By default this is set to true. + * + * @param allowUsingExhaustedKeys whether to allow using an authentication key which use + * count has been exceeded if no other key is available. + * @return the builder. + */ + public @NonNull Builder setAllowUsingExhaustedKeys(boolean allowUsingExhaustedKeys) { + mData.mAllowUsingExhaustedKeys = allowUsingExhaustedKeys; + return this; + } + + /** + * Sets whether to allow using an authentication key which is expired. + * + * By default this is set to false. + * + * @param allowUsingExpiredKeys whether to allow using an authentication key which is + * expired if no other key is available. + * @return the builder. + */ + public @NonNull Builder setAllowUsingExpiredKeys(boolean allowUsingExpiredKeys) { + mData.mAllowUsingExpiredKeys = allowUsingExpiredKeys; + return this; + } + + /** + * Sets whether to increment the use-count for the authentication key used. + * + * By default this is set to true. + * + * @param incrementUseCount whether to increment the use count of the authentication + * key used. + * @return the builder. + */ + public @NonNull Builder setIncrementUseCount(boolean incrementUseCount) { + mData.mIncrementUseCount = incrementUseCount; + return this; + } + + /** + * Sets the request message CBOR. + * + * <p>This data structure is described in the documentation for the + * {@link PresentationSession#getCredentialData(String, CredentialDataRequest)} method. + * + * @param requestMessage the request message CBOR as described above. + * @return the builder. + */ + public @NonNull Builder setRequestMessage(@NonNull byte[] requestMessage) { + mData.mRequestMessage = requestMessage; + return this; + } + + /** + * Sets the reader signature. + * + * <p>This data structure is described in the documentation for the + * {@link PresentationSession#getCredentialData(String, CredentialDataRequest)} method. + * + * @param readerSignature a {@code COSE_Sign1} structure as described above. + * @return the builder. + */ + public @NonNull Builder setReaderSignature(@NonNull byte[] readerSignature) { + mData.mReaderSignature = readerSignature; + return this; + } + + /** + * Finishes building a {@link CredentialDataRequest}. + * + * @return the {@link CredentialDataRequest} object. + */ + public @NonNull CredentialDataRequest build() { + return mData; + } + } +} diff --git a/identity/java/android/security/identity/CredentialDataResult.java b/identity/java/android/security/identity/CredentialDataResult.java new file mode 100644 index 000000000000..beb03af46303 --- /dev/null +++ b/identity/java/android/security/identity/CredentialDataResult.java @@ -0,0 +1,232 @@ +/* + * Copyright 2021 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 static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.lang.annotation.Retention; +import java.util.Collection; + + +/** + * An object that contains the result of retrieving data from a credential. This is used to return + * data requested in a {@link PresentationSession}. + */ +public abstract class CredentialDataResult { + /** + * @hide + */ + protected CredentialDataResult() {} + + /** + * Returns a CBOR structure containing the retrieved device-signed data. + * + * <p>This structure - along with the session transcript - may be cryptographically + * authenticated to prove to the reader that the data is from a trusted credential and + * {@link #getDeviceMac()} can be used to get a MAC. + * + * <p>The CBOR structure which is cryptographically authenticated is the + * {@code DeviceAuthenticationBytes} structure according to the following + * <a href="https://tools.ietf.org/html/rfc8610">CDDL</a> schema: + * + * <pre> + * DeviceAuthentication = [ + * "DeviceAuthentication", + * SessionTranscript, + * DocType, + * DeviceNameSpacesBytes + * ] + * + * DocType = tstr + * SessionTranscript = any + * DeviceNameSpacesBytes = #6.24(bstr .cbor DeviceNameSpaces) + * DeviceAuthenticationBytes = #6.24(bstr .cbor DeviceAuthentication) + * </pre> + * + * <p>where + * + * <pre> + * DeviceNameSpaces = { + * * NameSpace => DeviceSignedItems + * } + * + * DeviceSignedItems = { + * + DataItemName => DataItemValue + * } + * + * NameSpace = tstr + * DataItemName = tstr + * DataItemValue = any + * </pre> + * + * <p>The returned data is the binary encoding of the {@code DeviceNameSpaces} structure + * as defined above. + * + * @return The bytes of the {@code DeviceNameSpaces} CBOR structure. + */ + public abstract @NonNull byte[] getDeviceNameSpaces(); + + /** + * Returns a message authentication code over the {@code DeviceAuthenticationBytes} CBOR + * specified in {@link #getDeviceNameSpaces()}, to prove to the reader that the data + * is from a trusted credential. + * + * <p>The MAC proves to the reader that the data is from a trusted credential. This code is + * produced by using the key agreement and key derivation function from the ciphersuite + * with the authentication private key and the reader ephemeral public key to compute a + * shared message authentication code (MAC) key, then using the MAC function from the + * ciphersuite to compute a MAC of the authenticated data. See section 9.2.3.5 of + * ISO/IEC 18013-5 for details of this operation. + * + * <p>If the session transcript or reader ephemeral key wasn't set on the {@link + * PresentationSession} used to obtain this data no message authencation code will be produced + * and this method will return {@code null}. + * + * @return A COSE_Mac0 structure with the message authentication code as described above + * or {@code null} if the conditions specified above are not met. + */ + public abstract @Nullable byte[] getDeviceMac(); + + /** + * Returns the static authentication data associated with the dynamic authentication + * key used to MAC the data returned by {@link #getDeviceNameSpaces()}. + * + * @return The static authentication data associated with dynamic authentication key used to + * MAC the data. + */ + public abstract @NonNull byte[] getStaticAuthenticationData(); + + /** + * Gets the device-signed entries that was returned. + * + * @return an object to examine the entries returned. + */ + public abstract @NonNull Entries getDeviceSignedEntries(); + + /** + * Gets the issuer-signed entries that was returned. + * + * @return an object to examine the entries returned. + */ + public abstract @NonNull Entries getIssuerSignedEntries(); + + /** + * A class for representing data elements returned. + */ + public interface Entries { + /** Value was successfully retrieved. */ + int STATUS_OK = 0; + + /** The entry does not exist. */ + int STATUS_NO_SUCH_ENTRY = 1; + + /** The entry was not requested. */ + int STATUS_NOT_REQUESTED = 2; + + /** The entry wasn't in the request message. */ + int STATUS_NOT_IN_REQUEST_MESSAGE = 3; + + /** The entry was not retrieved because user authentication failed. */ + int STATUS_USER_AUTHENTICATION_FAILED = 4; + + /** The entry was not retrieved because reader authentication failed. */ + int STATUS_READER_AUTHENTICATION_FAILED = 5; + + /** + * The entry was not retrieved because it was configured without any access + * control profile. + */ + int STATUS_NO_ACCESS_CONTROL_PROFILES = 6; + + /** + * Gets the names of namespaces with retrieved entries. + * + * @return collection of name of namespaces containing retrieved entries. May be empty if no + * data was retrieved. + */ + @NonNull Collection<String> getNamespaces(); + + /** + * Get the names of all requested entries in a name space. + * + * <p>This includes the name of entries that wasn't successfully retrieved. + * + * @param namespaceName the namespace name to get entries for. + * @return A collection of names for the given namespace or the empty collection if no + * entries was returned for the given name space. + */ + @NonNull Collection<String> getEntryNames(@NonNull String namespaceName); + + /** + * Get the names of all entries that was successfully retrieved from a name space. + * + * <p>This only return entries for which {@link #getStatus(String, String)} will return + * {@link #STATUS_OK}. + * + * @param namespaceName the namespace name to get entries for. + * @return The entries in the given namespace that were successfully rerieved or the + * empty collection if no entries was returned for the given name space. + */ + @NonNull Collection<String> getRetrievedEntryNames(@NonNull String namespaceName); + + /** + * Gets the status of an entry. + * + * <p>This returns {@link #STATUS_OK} if the value was retrieved, {@link + * #STATUS_NO_SUCH_ENTRY} if the given entry wasn't retrieved, {@link + * #STATUS_NOT_REQUESTED} if it wasn't requested, {@link #STATUS_NOT_IN_REQUEST_MESSAGE} if + * the request message was set but the entry wasn't present in the request message, {@link + * #STATUS_USER_AUTHENTICATION_FAILED} if the value wasn't retrieved because the necessary + * user authentication wasn't performed, {@link #STATUS_READER_AUTHENTICATION_FAILED} if + * the supplied reader certificate chain didn't match the set of certificates the entry was + * provisioned with, or {@link #STATUS_NO_ACCESS_CONTROL_PROFILES} if the entry was + * configured without any access control profiles. + * + * @param namespaceName the namespace name of the entry. + * @param name the name of the entry to get the value for. + * @return the status indicating whether the value was retrieved and if not, why. + */ + @Status int getStatus(@NonNull String namespaceName, @NonNull String name); + + /** + * Gets the raw CBOR data for the value of an entry. + * + * <p>This should only be called on an entry for which the {@link #getStatus(String, + * String)} method returns {@link #STATUS_OK}. + * + * @param namespaceName the namespace name of the entry. + * @param name the name of the entry to get the value for. + * @return the raw CBOR data or {@code null} if no entry with the given name exists. + */ + @Nullable byte[] getEntry(@NonNull String namespaceName, @NonNull String name); + + /** + * The type of the entry status. + * @hide + */ + @Retention(SOURCE) + @IntDef({STATUS_OK, STATUS_NO_SUCH_ENTRY, STATUS_NOT_REQUESTED, + STATUS_NOT_IN_REQUEST_MESSAGE, STATUS_USER_AUTHENTICATION_FAILED, + STATUS_READER_AUTHENTICATION_FAILED, STATUS_NO_ACCESS_CONTROL_PROFILES}) + @interface Status {} + } + +} diff --git a/identity/java/android/security/identity/CredstoreCredentialDataResult.java b/identity/java/android/security/identity/CredstoreCredentialDataResult.java new file mode 100644 index 000000000000..7afe3d448bf9 --- /dev/null +++ b/identity/java/android/security/identity/CredstoreCredentialDataResult.java @@ -0,0 +1,106 @@ +/* + * Copyright 2021 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.NonNull; +import android.annotation.Nullable; + +import java.util.Collection; +import java.util.LinkedList; + +class CredstoreCredentialDataResult extends CredentialDataResult { + + ResultData mDeviceSignedResult; + ResultData mIssuerSignedResult; + CredstoreEntries mDeviceSignedEntries; + CredstoreEntries mIssuerSignedEntries; + + CredstoreCredentialDataResult(ResultData deviceSignedResult, ResultData issuerSignedResult) { + mDeviceSignedResult = deviceSignedResult; + mIssuerSignedResult = issuerSignedResult; + mDeviceSignedEntries = new CredstoreEntries(deviceSignedResult); + mIssuerSignedEntries = new CredstoreEntries(issuerSignedResult); + } + + @Override + public @NonNull byte[] getDeviceNameSpaces() { + return mDeviceSignedResult.getAuthenticatedData(); + } + + @Override + public @Nullable byte[] getDeviceMac() { + return mDeviceSignedResult.getMessageAuthenticationCode(); + } + + @Override + public @NonNull byte[] getStaticAuthenticationData() { + return mDeviceSignedResult.getStaticAuthenticationData(); + } + + @Override + public @NonNull CredentialDataResult.Entries getDeviceSignedEntries() { + return mDeviceSignedEntries; + } + + @Override + public @NonNull CredentialDataResult.Entries getIssuerSignedEntries() { + return mIssuerSignedEntries; + } + + static class CredstoreEntries implements CredentialDataResult.Entries { + ResultData mResultData; + + CredstoreEntries(ResultData resultData) { + mResultData = resultData; + } + + @Override + public @NonNull Collection<String> getNamespaces() { + return mResultData.getNamespaces(); + } + + @Override + public @NonNull Collection<String> getEntryNames(@NonNull String namespaceName) { + Collection<String> ret = mResultData.getEntryNames(namespaceName); + if (ret == null) { + ret = new LinkedList<String>(); + } + return ret; + } + + @Override + public @NonNull Collection<String> getRetrievedEntryNames(@NonNull String namespaceName) { + Collection<String> ret = mResultData.getRetrievedEntryNames(namespaceName); + if (ret == null) { + ret = new LinkedList<String>(); + } + return ret; + } + + @Override + @Status + public int getStatus(@NonNull String namespaceName, @NonNull String name) { + return mResultData.getStatus(namespaceName, name); + } + + @Override + public @Nullable byte[] getEntry(@NonNull String namespaceName, @NonNull String name) { + return mResultData.getEntry(namespaceName, name); + } + } + +} diff --git a/identity/java/android/security/identity/CredstoreIdentityCredential.java b/identity/java/android/security/identity/CredstoreIdentityCredential.java index 6398cee74cba..8e011053d2a7 100644 --- a/identity/java/android/security/identity/CredstoreIdentityCredential.java +++ b/identity/java/android/security/identity/CredstoreIdentityCredential.java @@ -58,14 +58,17 @@ class CredstoreIdentityCredential extends IdentityCredential { private @IdentityCredentialStore.Ciphersuite int mCipherSuite; private Context mContext; private ICredential mBinder; + private CredstorePresentationSession mSession; CredstoreIdentityCredential(Context context, String credentialName, @IdentityCredentialStore.Ciphersuite int cipherSuite, - ICredential binder) { + ICredential binder, + @Nullable CredstorePresentationSession session) { mContext = context; mCredentialName = credentialName; mCipherSuite = cipherSuite; mBinder = binder; + mSession = session; } private KeyPair mEphemeralKeyPair = null; @@ -239,6 +242,7 @@ class CredstoreIdentityCredential extends IdentityCredential { private boolean mAllowUsingExhaustedKeys = true; private boolean mAllowUsingExpiredKeys = false; + private boolean mIncrementKeyUsageCount = true; @Override public void setAllowUsingExhaustedKeys(boolean allowUsingExhaustedKeys) { @@ -250,6 +254,11 @@ class CredstoreIdentityCredential extends IdentityCredential { mAllowUsingExpiredKeys = allowUsingExpiredKeys; } + @Override + public void setIncrementKeyUsageCount(boolean incrementKeyUsageCount) { + mIncrementKeyUsageCount = incrementKeyUsageCount; + } + private boolean mOperationHandleSet = false; private long mOperationHandle = 0; @@ -264,7 +273,8 @@ class CredstoreIdentityCredential extends IdentityCredential { if (!mOperationHandleSet) { try { mOperationHandle = mBinder.selectAuthKey(mAllowUsingExhaustedKeys, - mAllowUsingExpiredKeys); + mAllowUsingExpiredKeys, + mIncrementKeyUsageCount); mOperationHandleSet = true; } catch (android.os.RemoteException e) { throw new RuntimeException("Unexpected RemoteException ", e); @@ -315,7 +325,8 @@ class CredstoreIdentityCredential extends IdentityCredential { sessionTranscript != null ? sessionTranscript : new byte[0], readerSignature != null ? readerSignature : new byte[0], mAllowUsingExhaustedKeys, - mAllowUsingExpiredKeys); + mAllowUsingExpiredKeys, + mIncrementKeyUsageCount); } catch (android.os.RemoteException e) { throw new RuntimeException("Unexpected RemoteException ", e); } catch (android.os.ServiceSpecificException e) { diff --git a/identity/java/android/security/identity/CredstoreIdentityCredentialStore.java b/identity/java/android/security/identity/CredstoreIdentityCredentialStore.java index d8d47424e2e8..fb0880ce3521 100644 --- a/identity/java/android/security/identity/CredstoreIdentityCredentialStore.java +++ b/identity/java/android/security/identity/CredstoreIdentityCredentialStore.java @@ -126,7 +126,8 @@ class CredstoreIdentityCredentialStore extends IdentityCredentialStore { ICredential credstoreCredential; credstoreCredential = mStore.getCredentialByName(credentialName, cipherSuite); return new CredstoreIdentityCredential(mContext, credentialName, cipherSuite, - credstoreCredential); + credstoreCredential, + null); } catch (android.os.RemoteException e) { throw new RuntimeException("Unexpected RemoteException ", e); } catch (android.os.ServiceSpecificException e) { @@ -162,4 +163,23 @@ class CredstoreIdentityCredentialStore extends IdentityCredentialStore { + e.errorCode, e); } } + + @Override + public @NonNull PresentationSession createPresentationSession(@Ciphersuite int cipherSuite) + throws CipherSuiteNotSupportedException { + try { + ISession credstoreSession = mStore.createPresentationSession(cipherSuite); + return new CredstorePresentationSession(mContext, cipherSuite, this, credstoreSession); + } catch (android.os.RemoteException e) { + throw new RuntimeException("Unexpected RemoteException ", e); + } catch (android.os.ServiceSpecificException e) { + if (e.errorCode == ICredentialStore.ERROR_CIPHER_SUITE_NOT_SUPPORTED) { + throw new CipherSuiteNotSupportedException(e.getMessage(), e); + } else { + throw new RuntimeException("Unexpected ServiceSpecificException with code " + + e.errorCode, e); + } + } + } + } diff --git a/identity/java/android/security/identity/CredstorePresentationSession.java b/identity/java/android/security/identity/CredstorePresentationSession.java new file mode 100644 index 000000000000..e3c6689a8914 --- /dev/null +++ b/identity/java/android/security/identity/CredstorePresentationSession.java @@ -0,0 +1,214 @@ +/* + * Copyright 2021 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.NonNull; +import android.annotation.Nullable; +import android.content.Context; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.util.LinkedHashMap; +import java.util.Map; + +class CredstorePresentationSession extends PresentationSession { + private static final String TAG = "CredstorePresentationSession"; + + private @IdentityCredentialStore.Ciphersuite int mCipherSuite; + private Context mContext; + private CredstoreIdentityCredentialStore mStore; + private ISession mBinder; + private Map<String, CredstoreIdentityCredential> mCredentialCache = new LinkedHashMap<>(); + private KeyPair mEphemeralKeyPair = null; + private byte[] mSessionTranscript = null; + private boolean mOperationHandleSet = false; + private long mOperationHandle = 0; + + CredstorePresentationSession(Context context, + @IdentityCredentialStore.Ciphersuite int cipherSuite, + CredstoreIdentityCredentialStore store, + ISession binder) { + mContext = context; + mCipherSuite = cipherSuite; + mStore = store; + mBinder = binder; + } + + private void ensureEphemeralKeyPair() { + if (mEphemeralKeyPair != null) { + return; + } + try { + // This PKCS#12 blob is generated in credstore, using BoringSSL. + // + // The main reason for this convoluted approach and not just sending the decomposed + // key-pair is that this would require directly using (device-side) BouncyCastle which + // is tricky due to various API hiding efforts. So instead we have credstore generate + // this PKCS#12 blob. The blob is encrypted with no password (sadly, also, BoringSSL + // doesn't support not using encryption when building a PKCS#12 blob). + // + byte[] pkcs12 = mBinder.getEphemeralKeyPair(); + String alias = "ephemeralKey"; + char[] password = {}; + + KeyStore ks = KeyStore.getInstance("PKCS12"); + ByteArrayInputStream bais = new ByteArrayInputStream(pkcs12); + ks.load(bais, password); + PrivateKey privKey = (PrivateKey) ks.getKey(alias, password); + + Certificate cert = ks.getCertificate(alias); + PublicKey pubKey = cert.getPublicKey(); + + mEphemeralKeyPair = new KeyPair(pubKey, privKey); + } catch (android.os.ServiceSpecificException e) { + throw new RuntimeException("Unexpected ServiceSpecificException with code " + + e.errorCode, e); + } catch (android.os.RemoteException + | KeyStoreException + | CertificateException + | UnrecoverableKeyException + | NoSuchAlgorithmException + | IOException e) { + throw new RuntimeException("Unexpected exception ", e); + } + } + + @Override + public @NonNull KeyPair getEphemeralKeyPair() { + ensureEphemeralKeyPair(); + return mEphemeralKeyPair; + } + + @Override + public void setReaderEphemeralPublicKey(@NonNull PublicKey readerEphemeralPublicKey) + throws InvalidKeyException { + try { + byte[] uncompressedForm = + Util.publicKeyEncodeUncompressedForm(readerEphemeralPublicKey); + mBinder.setReaderEphemeralPublicKey(uncompressedForm); + } catch (android.os.RemoteException e) { + throw new RuntimeException("Unexpected RemoteException ", e); + } catch (android.os.ServiceSpecificException e) { + throw new RuntimeException("Unexpected ServiceSpecificException with code " + + e.errorCode, e); + } + } + + @Override + public void setSessionTranscript(@NonNull byte[] sessionTranscript) { + try { + mBinder.setSessionTranscript(sessionTranscript); + mSessionTranscript = sessionTranscript; + } catch (android.os.RemoteException e) { + throw new RuntimeException("Unexpected RemoteException ", e); + } catch (android.os.ServiceSpecificException e) { + throw new RuntimeException("Unexpected ServiceSpecificException with code " + + e.errorCode, e); + } + } + + @Override + public @Nullable CredentialDataResult getCredentialData(@NonNull String credentialName, + @NonNull CredentialDataRequest request) + throws NoAuthenticationKeyAvailableException, InvalidReaderSignatureException, + InvalidRequestMessageException, EphemeralPublicKeyNotFoundException { + try { + // Cache the IdentityCredential to satisfy the property that AuthKey usage counts are + // incremented on only the _first_ getCredentialData() call. + // + CredstoreIdentityCredential credential = mCredentialCache.get(credentialName); + if (credential == null) { + ICredential credstoreCredential = + mBinder.getCredentialForPresentation(credentialName); + credential = new CredstoreIdentityCredential(mContext, credentialName, + mCipherSuite, credstoreCredential, + this); + mCredentialCache.put(credentialName, credential); + + credential.setAllowUsingExhaustedKeys(request.isAllowUsingExhaustedKeys()); + credential.setAllowUsingExpiredKeys(request.isAllowUsingExpiredKeys()); + credential.setIncrementKeyUsageCount(request.isIncrementUseCount()); + } + + ResultData deviceSignedResult = credential.getEntries( + request.getRequestMessage(), + request.getDeviceSignedEntriesToRequest(), + mSessionTranscript, + request.getReaderSignature()); + + // By design this second getEntries() call consumes the same auth-key. + + ResultData issuerSignedResult = credential.getEntries( + request.getRequestMessage(), + request.getIssuerSignedEntriesToRequest(), + mSessionTranscript, + request.getReaderSignature()); + + return new CredstoreCredentialDataResult(deviceSignedResult, issuerSignedResult); + + } catch (SessionTranscriptMismatchException e) { + throw new RuntimeException("Unexpected ", e); + } catch (android.os.RemoteException e) { + throw new RuntimeException("Unexpected RemoteException ", e); + } catch (android.os.ServiceSpecificException e) { + if (e.errorCode == ICredentialStore.ERROR_NO_SUCH_CREDENTIAL) { + return null; + } else { + throw new RuntimeException("Unexpected ServiceSpecificException with code " + + e.errorCode, e); + } + } + } + + /** + * Called by android.hardware.biometrics.CryptoObject#getOpId() to get an + * operation handle. + * + * @hide + */ + @Override + public long getCredstoreOperationHandle() { + if (!mOperationHandleSet) { + try { + mOperationHandle = mBinder.getAuthChallenge(); + mOperationHandleSet = true; + } catch (android.os.RemoteException e) { + throw new RuntimeException("Unexpected RemoteException ", e); + } catch (android.os.ServiceSpecificException e) { + if (e.errorCode == ICredentialStore.ERROR_NO_AUTHENTICATION_KEY_AVAILABLE) { + // The NoAuthenticationKeyAvailableException will be thrown when + // the caller proceeds to call getEntries(). + } + throw new RuntimeException("Unexpected ServiceSpecificException with code " + + e.errorCode, e); + } + } + return mOperationHandle; + } + +} diff --git a/identity/java/android/security/identity/IdentityCredential.java b/identity/java/android/security/identity/IdentityCredential.java index 1e685856d011..cdf746fc9900 100644 --- a/identity/java/android/security/identity/IdentityCredential.java +++ b/identity/java/android/security/identity/IdentityCredential.java @@ -48,7 +48,9 @@ public abstract class IdentityCredential { * encryption". * * @return ephemeral key pair to use to establish a secure channel with a reader. + * @deprecated Use {@link PresentationSession} instead. */ + @Deprecated public @NonNull abstract KeyPair createEphemeralKeyPair(); /** @@ -58,7 +60,9 @@ public abstract class IdentityCredential { * @param readerEphemeralPublicKey The ephemeral public key provided by the reader to * establish a secure session. * @throws InvalidKeyException if the given key is invalid. + * @deprecated Use {@link PresentationSession} instead. */ + @Deprecated public abstract void setReaderEphemeralPublicKey(@NonNull PublicKey readerEphemeralPublicKey) throws InvalidKeyException; @@ -72,7 +76,10 @@ public abstract class IdentityCredential { * * @param messagePlaintext unencrypted message to encrypt. * @return encrypted message. + * @deprecated Applications should use {@link PresentationSession} and + * implement encryption/decryption themselves. */ + @Deprecated public @NonNull abstract byte[] encryptMessageToReader(@NonNull byte[] messagePlaintext); /** @@ -86,7 +93,10 @@ public abstract class IdentityCredential { * @param messageCiphertext encrypted message to decrypt. * @return decrypted message. * @throws MessageDecryptionException if the ciphertext couldn't be decrypted. + * @deprecated Applications should use {@link PresentationSession} and + * implement encryption/decryption themselves. */ + @Deprecated public @NonNull abstract byte[] decryptMessageFromReader(@NonNull byte[] messageCiphertext) throws MessageDecryptionException; @@ -111,7 +121,9 @@ public abstract class IdentityCredential { * * @param allowUsingExhaustedKeys whether to allow using an authentication key which use count * has been exceeded if no other key is available. + * @deprecated Use {@link PresentationSession} instead. */ + @Deprecated public abstract void setAllowUsingExhaustedKeys(boolean allowUsingExhaustedKeys); /** @@ -128,12 +140,36 @@ public abstract class IdentityCredential { * * @param allowUsingExpiredKeys whether to allow using an authentication key which use count * has been exceeded if no other key is available. + * @deprecated Use {@link PresentationSession} instead. */ + @Deprecated public void setAllowUsingExpiredKeys(boolean allowUsingExpiredKeys) { throw new UnsupportedOperationException(); } /** + * @hide + * + * Sets whether the usage count of an authentication key should be increased. This must be + * called prior to calling + * {@link #getEntries(byte[], Map, byte[], byte[])} or using a + * {@link android.hardware.biometrics.BiometricPrompt.CryptoObject} which references this object. + * + * <p>By default this is set to true. + * + * <p>This is only implemented in feature version 202201 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 incrementKeyUsageCount whether the usage count of the key should be increased. + * @deprecated Use {@link PresentationSession} instead. + */ + public void setIncrementKeyUsageCount(boolean incrementKeyUsageCount) { + throw new UnsupportedOperationException(); + } + + /** * Called by android.hardware.biometrics.CryptoObject#getOpId() to get an * operation handle. * @@ -149,15 +185,19 @@ public abstract class IdentityCredential { * by using the {@link ResultData#getStatus(String, String)} method on each of the requested * entries. * - * <p>It is the responsibility of the calling application to know if authentication is needed - * and use e.g. {@link android.hardware.biometrics.BiometricPrompt} to make the user - * authenticate using a {@link android.hardware.biometrics.BiometricPrompt.CryptoObject} which - * references this object. If needed, this must be done before calling - * {@link #getEntries(byte[], Map, byte[], byte[])}. - * * <p>It is permissible to call this method multiple times using the same instance but if this * is done, the {@code sessionTranscript} parameter must be identical for each call. If this is * not the case, the {@link SessionTranscriptMismatchException} exception is thrown. + * Additionally, if this is done the same auth-key will be used. + * + * <p>The application should not make any assumptions on whether user authentication is needed. + * Instead, the application should request the data elements values first and then examine + * the returned {@link ResultData}. If {@link ResultData#STATUS_USER_AUTHENTICATION_FAILED} + * is returned the application should get a + * {@link android.hardware.biometrics.BiometricPrompt.CryptoObject} which references this + * object and use it with a {@link android.hardware.biometrics.BiometricPrompt}. Upon successful + * authentication the application may call {@link #getEntries(byte[], Map, byte[], byte[])} + * again. * * <p>If not {@code null} the {@code requestMessage} parameter must contain data for the request * from the verifier. The content can be defined in the way appropriate for the credential, but @@ -269,7 +309,9 @@ public abstract class IdentityCredential { * @throws InvalidRequestMessageException if the requestMessage is malformed. * @throws EphemeralPublicKeyNotFoundException if the ephemeral public key was not found in * the session transcript. + * @deprecated Use {@link PresentationSession} instead. */ + @Deprecated public abstract @NonNull ResultData getEntries( @Nullable byte[] requestMessage, @NonNull Map<String, Collection<String>> entriesToRequest, diff --git a/identity/java/android/security/identity/IdentityCredentialStore.java b/identity/java/android/security/identity/IdentityCredentialStore.java index 6ccd0e892141..dbb8aaa7796e 100644 --- a/identity/java/android/security/identity/IdentityCredentialStore.java +++ b/identity/java/android/security/identity/IdentityCredentialStore.java @@ -209,6 +209,25 @@ public abstract class IdentityCredentialStore { @Deprecated public abstract @Nullable byte[] deleteCredentialByName(@NonNull String credentialName); + /** + * Creates a new presentation session. + * + * <p>This method gets an object to be used for interaction with a remote verifier for + * presentation of one or more credentials. + * + * <p>This is only implemented in feature version 202201 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 cipherSuite the cipher suite to use for communicating with the verifier. + * @return The presentation session. + */ + public @NonNull PresentationSession createPresentationSession(@Ciphersuite int cipherSuite) + throws CipherSuiteNotSupportedException { + throw new UnsupportedOperationException(); + } + /** @hide */ @IntDef(value = {CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256}) @Retention(RetentionPolicy.SOURCE) diff --git a/identity/java/android/security/identity/PresentationSession.java b/identity/java/android/security/identity/PresentationSession.java new file mode 100644 index 000000000000..afaafce32798 --- /dev/null +++ b/identity/java/android/security/identity/PresentationSession.java @@ -0,0 +1,173 @@ +/* + * Copyright 2021 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.NonNull; +import android.annotation.Nullable; + +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.PublicKey; + +/** + * Class for presenting multiple documents to a remote verifier. + * + * Use {@link IdentityCredentialStore#createPresentationSession(int)} to create a {@link + * PresentationSession} instance. + */ +public abstract class PresentationSession { + /** + * @hide + */ + protected PresentationSession() {} + + /** + * Gets the ephemeral key pair to use to establish a secure channel with the verifier. + * + * <p>Applications should use this key-pair for the communications channel with the verifier + * using a protocol / cipher-suite appropriate for the application. One example of such a + * protocol is the one used for Mobile Driving Licenses, see ISO 18013-5. + * + * <p>The ephemeral key pair is tied to the {@link PresentationSession} instance so subsequent + * calls to this method will return the same key-pair. + * + * @return ephemeral key pair to use to establish a secure channel with a reader. + */ + public @NonNull abstract KeyPair getEphemeralKeyPair(); + + /** + * Set the ephemeral public key provided by the verifier. + * + * <p>If called, this must be called before any calls to + * {@link #getCredentialData(String, CredentialDataRequest)}. + * + * <p>This method can only be called once per {@link PresentationSession} instance. + * + * @param readerEphemeralPublicKey The ephemeral public key provided by the reader to + * establish a secure session. + * @throws InvalidKeyException if the given key is invalid. + */ + public abstract void setReaderEphemeralPublicKey(@NonNull PublicKey readerEphemeralPublicKey) + throws InvalidKeyException; + + /** + * Set the session transcript. + * + * <p>If called, this must be called before any calls to + * {@link #getCredentialData(String, CredentialDataRequest)}. + * + * <p>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 + * 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 and ditto for the 32 bytes for the Y + * coordinate. + * + * <p>This method can only be called once per {@link PresentationSession} instance. + * + * @param sessionTranscript the session transcript. + */ + public abstract void setSessionTranscript(@NonNull byte[] sessionTranscript); + + /** + * Retrieves data from a named credential in the current presentation session. + * + * <p>If an access control check fails for one of the requested entries or if the entry + * doesn't exist, the entry is simply not returned. The application can detect this + * by using the {@link CredentialDataResult.Entries#getStatus(String, String)} method on + * each of the requested entries. + * + * <p>The application should not make any assumptions on whether user authentication is needed. + * Instead, the application should request the data elements values first and then examine + * the returned {@link CredentialDataResult.Entries}. If + * {@link CredentialDataResult.Entries#STATUS_USER_AUTHENTICATION_FAILED} is returned the + * application should get a + * {@link android.hardware.biometrics.BiometricPrompt.CryptoObject} which references this + * object and use it with a {@link android.hardware.biometrics.BiometricPrompt}. Upon successful + * authentication the application may call + * {@link #getCredentialData(String, CredentialDataRequest)} again. + * + * <p>It is permissible to call this method multiple times using the same credential name. + * If this is done the same auth-key will be used. + * + * <p>If the reader signature is set in the request parameter (via the + * {@link CredentialDataRequest.Builder#setReaderSignature(byte[])} method) it must contain + * the bytes of a {@code COSE_Sign1} structure as defined in RFC 8152. For the payload + * {@code nil} shall be used and the detached payload is the {@code ReaderAuthenticationBytes} + * CBOR described below. + * <pre> + * ReaderAuthentication = [ + * "ReaderAuthentication", + * SessionTranscript, + * ItemsRequestBytes + * ] + * + * ItemsRequestBytes = #6.24(bstr .cbor ItemsRequest) + * + * ReaderAuthenticationBytes = #6.24(bstr .cbor ReaderAuthentication) + * </pre> + * + * <p>where {@code ItemsRequestBytes} are the bytes of the request message set in + * the request parameter (via the + * {@link CredentialDataRequest.Builder#setRequestMessage(byte[])} method). + * + * <p>The public key corresponding to the key used to make the signature, can be found in the + * {@code x5chain} unprotected header element of the {@code COSE_Sign1} structure (as as + * described in + * <a href="https://tools.ietf.org/html/draft-ietf-cose-x509-08">draft-ietf-cose-x509-08</a>). + * There will be at least one certificate in said element and there may be more (and if so, + * each certificate must be signed by its successor). + * + * <p>Data elements protected by reader authentication are returned if, and only if, + * {@code requestMessage} is signed by the top-most certificate in the reader's certificate + * chain, and the data element is configured with an {@link AccessControlProfile} configured + * with an X.509 certificate for a key which appear in the certificate chain. + * + * <p>Note that the request message CBOR is used only for enforcing reader authentication, it's + * not used for determining which entries this API will return. The application is expected to + * have parsed the request message and filtered it according to user preference and/or consent. + * + * @param credentialName the name of the credential to retrieve. + * @param request the data to retrieve from the credential + * @return If the credential wasn't found, returns null. Otherwise a + * {@link CredentialDataResult} object containing entry data organized by namespace and + * a cryptographically authenticated representation of the same data, bound to the + * current session. + * @throws NoAuthenticationKeyAvailableException if authentication keys were never + * provisioned for the credential or if they + * are expired or exhausted their use-count. + * @throws InvalidRequestMessageException if the requestMessage is malformed. + * @throws InvalidReaderSignatureException if the reader signature is invalid, or it + * doesn't contain a certificate chain, or if + * the signature failed to validate. + * @throws EphemeralPublicKeyNotFoundException if the ephemeral public key was not found in + * the session transcript. + */ + public abstract @Nullable CredentialDataResult getCredentialData( + @NonNull String credentialName, @NonNull CredentialDataRequest request) + throws NoAuthenticationKeyAvailableException, InvalidReaderSignatureException, + InvalidRequestMessageException, EphemeralPublicKeyNotFoundException; + + /** + * Called by android.hardware.biometrics.CryptoObject#getOpId() to get an + * operation handle. + * + * @hide + */ + public abstract long getCredstoreOperationHandle(); +} diff --git a/identity/java/android/security/identity/ResultData.java b/identity/java/android/security/identity/ResultData.java index 71860d261285..d46f9854b278 100644 --- a/identity/java/android/security/identity/ResultData.java +++ b/identity/java/android/security/identity/ResultData.java @@ -28,7 +28,10 @@ import java.util.Collection; /** * An object that contains the result of retrieving data from a credential. This is used to return * data requested from a {@link IdentityCredential}. + * + * @deprecated Use {@link PresentationSession} instead. */ +@Deprecated public abstract class ResultData { /** Value was successfully retrieved. */ |