Port IdentityCredential HAL to AIDL.
This includes add a partial types-only HAL for KeyMaster for
HardwareAuthToken.
Bug: 111446262
Test: atest android.security.identity.cts
Test: VtsHalIdentityTargetTest
Test: android.hardware.identity-support-lib-test
Change-Id: I7a6254d33200bfd62269aed1957cbb2a84b16272
diff --git a/identity/aidl/Android.bp b/identity/aidl/Android.bp
new file mode 100644
index 0000000..72b19a1
--- /dev/null
+++ b/identity/aidl/Android.bp
@@ -0,0 +1,21 @@
+aidl_interface {
+ name: "android.hardware.identity",
+ vendor_available: true,
+ srcs: [
+ "android/hardware/identity/*.aidl",
+ ],
+ imports: [
+ "android.hardware.keymaster",
+ ],
+ stability: "vintf",
+ backend: {
+ java: {
+ platform_apis: true,
+ },
+ ndk: {
+ vndk: {
+ enabled: true,
+ },
+ },
+ },
+}
diff --git a/identity/aidl/android/hardware/identity/Certificate.aidl b/identity/aidl/android/hardware/identity/Certificate.aidl
new file mode 100644
index 0000000..5bbc17c
--- /dev/null
+++ b/identity/aidl/android/hardware/identity/Certificate.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2020 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.hardware.identity;
+
+@VintfStability
+parcelable Certificate {
+ /**
+ * encodedCertificate contains the bytes of a DER-encoded X.509 certificate.
+ *
+ * If there is no certificate, this array is empty.
+ */
+ byte[] encodedCertificate;
+}
diff --git a/identity/aidl/android/hardware/identity/CipherSuite.aidl b/identity/aidl/android/hardware/identity/CipherSuite.aidl
new file mode 100644
index 0000000..20b02a8
--- /dev/null
+++ b/identity/aidl/android/hardware/identity/CipherSuite.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020 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.hardware.identity;
+
+/**
+ * Cipher suites that can be used for communication between holder and reader devices.
+ */
+@VintfStability
+@Backing(type="int")
+enum CipherSuite {
+ /**
+ * Specifies that the cipher suite that will be used to secure communications between the reader
+ * is:
+ *
+ * - ECDHE with HKDF-SHA-256 for key agreement.
+ * - AES-256 with GCM block mode for authenticated encryption (nonces are incremented by
+ * one for every message).
+ * - ECDSA with SHA-256 for signing (used for signing session transcripts to defeat
+ * man-in-the-middle attacks), signing keys are not ephemeral.
+ *
+ * At present this is the only supported cipher suite and it is mandatory for all
+ * implementations to support it.
+ */
+ CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256 = 1,
+}
diff --git a/identity/aidl/android/hardware/identity/HardwareInformation.aidl b/identity/aidl/android/hardware/identity/HardwareInformation.aidl
new file mode 100644
index 0000000..d67739d
--- /dev/null
+++ b/identity/aidl/android/hardware/identity/HardwareInformation.aidl
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2020 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.hardware.identity;
+
+@VintfStability
+parcelable HardwareInformation {
+ /**
+ * credentialStoreName is the name of the credential store implementation.
+ */
+ @utf8InCpp String credentialStoreName;
+
+ /**
+ * credentialStoreAuthorName is the name of the credential store author.
+ */
+ @utf8InCpp String credentialStoreAuthorName;
+
+ /**
+ * dataChunkSize is the size of data chunks to be used when sending and recieving data
+ * entries. All data chunks for a data item must be this size except for the last.
+ */
+ int dataChunkSize;
+
+ /**
+ * isDirectAccess specifies whether the provisioned credential is available through
+ * direct access. Credentials provisioned in credential stores with this set
+ * to true, should use reader authentication on all data elements.
+ */
+ boolean isDirectAccess;
+
+ /**
+ * supportedDocTypes if empty, then any document type is supported, otherwise
+ * only the document types returned are supported.
+ *
+ * Document types are defined in the relevant standard for the document, for example for the
+ * for Mobile Driving License as defined by ISO 18013-5 the document type is defined to
+ * be "org.iso.18013.5.1.mDL".
+ *
+ */
+ @utf8InCpp String[] supportedDocTypes;
+}
diff --git a/identity/aidl/android/hardware/identity/IIdentityCredential.aidl b/identity/aidl/android/hardware/identity/IIdentityCredential.aidl
new file mode 100644
index 0000000..10ce4c2
--- /dev/null
+++ b/identity/aidl/android/hardware/identity/IIdentityCredential.aidl
@@ -0,0 +1,318 @@
+/*
+ * Copyright 2020 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.hardware.identity;
+
+import android.hardware.identity.Certificate;
+import android.hardware.identity.SecureAccessControlProfile;
+import android.hardware.keymaster.HardwareAuthToken;
+
+@VintfStability
+interface IIdentityCredential {
+ /**
+ * Delete a credential.
+ *
+ * This method returns a COSE_Sign1 data structure signed by CredentialKey
+ * with payload set to the ProofOfDeletion CBOR below:
+ *
+ * ProofOfDeletion = [
+ * "ProofOfDeletion", ; tstr
+ * tstr, ; DocType
+ * bool ; true if this is a test credential, should
+ * ; always be false.
+ * ]
+ *
+ * After this method has been called, the persistent storage used for credentialData should
+ * be deleted.
+ *
+ * @return a COSE_Sign1 signature described above.
+ */
+ byte[] deleteCredential();
+
+ /**
+ * Creates an ephemeral EC key pair, for use in establishing a seceure session with a reader.
+ * This method returns the private key so the caller can perform an ECDH key agreement operation
+ * with the reader. The reason for generating the key pair in the secure environment is so that
+ * the secure environment knows what public key to expect to find in the session transcript.
+ *
+ * This method may only be called once per instance. If called more than once, STATUS_FAILED
+ * will be returned.
+ *
+ * @return the unencrypted key-pair in PKCS#8 format.
+ */
+ byte[] createEphemeralKeyPair();
+
+ /**
+ * Sets the public part of the reader's ephemeral key pair.
+ *
+ * This method may only be called once per instance. If called more than once, STATUS_FAILED
+ * will be returned.
+ *
+ * @param publicKey contains the reader's ephemeral public key, in uncompressed form.
+ */
+ void setReaderEphemeralPublicKey(in byte[] publicKey);
+
+ /**
+ * Creates a challenge value to be used for proving successful user authentication. This
+ * is included in the authToken passed to the startRetrieval() method.
+ *
+ * This method may only be called once per instance. If called more than once, STATUS_FAILED
+ * will be returned.
+ *
+ * @return challenge, a non-zero number.
+ */
+ long createAuthChallenge();
+
+ /**
+ * Start an entry retrieval process.
+ *
+ * This method be called after createEphemeralKeyPair(), setReaderEphemeralPublicKey(),
+ * createAuthChallenge() and before startRetrieveEntry(). This method call is followed by
+ * multiple calls of startRetrieveEntryValue(), retrieveEntryValue(), and finally
+ * finishRetrieval().This whole process is called a "credential presentation".
+ *
+ * It is permissible to perform multiple credential presentations using the same instance (e.g.
+ * startRetrieval(), then multiple calls of startRetrieveEntryValue(), retrieveEntryValue(),
+ * then finally finishRetrieval()) but if this is done, the sessionTranscript parameter
+ * must be identical for each startRetrieval() invocation. If this is not the case, this call
+ * fails with the STATUS_SESSION_TRANSCRIPT_MISMATCH error.
+ *
+ * If the provided authToken is not valid this method fails with STATUS_INVALID_AUTH_TOKEN.
+ *
+ * Each of the provided accessControlProfiles is checked in this call. If they are not
+ * all valid, the call fails with STATUS_INVALID_DATA.
+ *
+ * For the itemsRequest parameter, the content can be defined in the way appropriate for
+ * the credential, but there are three requirements that must be met to work with this HAL:
+ *
+ * 1. The content must be a CBOR-encoded structure.
+ * 2. The CBOR structure must be a map.
+ * 3. The map must contain a tstr key "nameSpaces" whose value contains a map, as described in
+ * the example below.
+ *
+ * If these requirements are not met the startRetrieval() call fails with
+ * STATUS_INVALID_ITEMS_REQUEST_MESSAGE.
+ *
+ * Here's an example of ItemsRequest CBOR which conforms to this requirement:
+ *
+ * ItemsRequest = {
+ * ? "docType" : DocType,
+ * "nameSpaces" : NameSpaces,
+ * ? "requestInfo" : {* tstr => any} ; Additional info the reader wants to provide
+ * }
+ *
+ * DocType = tstr
+ *
+ * NameSpaces = {
+ * + NameSpace => DataElements ; Requested data elements for each NameSpace
+ * }
+ *
+ * NameSpace = tstr
+ *
+ * DataElements = {
+ * + DataElement => IntentToRetain
+ * }
+ *
+ * DataElement = tstr
+ * IntentToRetain = bool
+ *
+ * For the readerSignature parameter, this can either be empty or if non-empty it
+ * must be a COSE_Sign1 structure with an ECDSA signature over the content of the
+ * CBOR conforming to the following CDDL:
+ *
+ * ReaderAuthentication = [
+ * "ReaderAuthentication",
+ * SessionTranscript,
+ * ItemsRequestBytes
+ * ]
+ *
+ * SessionTranscript = [
+ * DeviceEngagementBytes,
+ * EReaderKeyBytes
+ * ]
+ *
+ * DeviceEngagementBytes = #6.24(bstr .cbor DeviceEngagement)
+ * EReaderKeyBytes = #6.24(bstr .cbor EReaderKey.Pub)
+ * ItemsRequestBytes = #6.24(bstr .cbor ItemsRequest)
+ *
+ * The public key corresponding to the key used to made signature, can be found in the
+ * 'x5chain' unprotected header element of the COSE_Sign1 structure (as as described
+ * in 'draft-ietf-cose-x509-04'). 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).
+ * This is checked and if the check fails the call fails with
+ * STATUS_READER_SIGNATURE_CHECK_FAILED.
+ *
+ * The SessionTranscript CBOR is conveyed in the sessionTranscript parameter. It
+ * is permissible for this to be empty in which case the readerSignature parameter
+ * must also be empty. If this is not the case, the call fails with STATUS_FAILED.
+ *
+ * If the SessionTranscript CBOR is not empty, the X and Y coordinates of the public
+ * part of the key-pair previously generated by createEphemeralKeyPair() must appear
+ * somewhere in the bytes of DeviceEngagement structure. Both X and Y should be in
+ * uncompressed form. If this is not satisfied, the call fails with
+ * STATUS_EPHEMERAL_PUBLIC_KEY_NOT_FOUND.
+ *
+ * @param accessControlProfiles
+ * Access control profiles that are required to retrieve the entries that are going to be
+ * requested with IIdentityCredential.retrieveEntryValue(). See above.
+ *
+ * @param authToken
+ * The authentication token that proves the user was authenticated, as required
+ * by one or more of the provided accessControlProfiles. See above.
+ *
+ * @param itemsRequest
+ * If non-empty, contains request data that is signed by the reader. See above.
+ *
+ * @param sessionTranscript
+ * Either empty or the CBOR of the SessionTranscript. See above.
+ *
+ * @param readerSignature
+ * readerSignature is either empty or contains a CBOR_Sign1 structure. See above.
+ *
+ * @param requestCounts
+ * requestCounts specifies the number of data items that are going to be requested, per
+ * namespace. The number of elements in the vector must be the number of namespaces for which
+ * data items will be requested in retrieveEntryValue() calls, and the values of the elments
+ * must be the number of items from each namespace, in order, that will be requested in
+ * retrieveEntryValue() calls.
+ * Note that it's the caller's responsibility to ensure that the counts correspond to the
+ * retrieveEntryValue() calls that will be made, and that every retrieveEntryValue() call
+ * will succeed (i.e. that the access control profile checks will succeed). This means that
+ * it's the responsibility of the caller to determine which access control checks will fail
+ * and remove the corresponding requests from the counts.
+ */
+ void startRetrieval(in SecureAccessControlProfile[] accessControlProfiles,
+ in HardwareAuthToken authToken,
+ in byte[] itemsRequest,
+ in byte[] sessionTranscript, in byte[] readerSignature, in int[] requestCounts);
+
+ /**
+ * Starts retrieving an entry, subject to access control requirements. Entries must be
+ * retrieved in namespace groups as specified in the requestCounts parameter.
+ *
+ * If the requestData parameter as passed to startRetrieval() was non-empty
+ * this method must only be called with entries specified in that field. If this
+ * requirement is not met, the call fails with STATUS_NOT_IN_REQUEST_MESSAGE.
+ *
+ * If nameSpace or name is empty this call fails with STATUS_INVALID_DATA.
+ *
+ * Each access control profile for the entry is checked. If user authentication
+ * is required and the supplied auth token doesn't provide it the call fails
+ * with STATUS_USER_AUTHENTICATION_FAILED. If reader authentication is required and
+ * a suitable reader certificate chain isn't presented, the call fails with
+ * STATUS_READER_AUTHENTICATION_FAILED.
+ *
+ * It is permissible to keep retrieving values if an access control check fails.
+ *
+ * @param nameSpace is the namespace of the element, e.g. "org.iso.18013"
+ *
+ * @param name is the name of the element.
+ *
+ * @param entrySize is the size of the entry value, if it's a text string or a byte string.
+ * It must be zero if the entry value is an integer or boolean. If this requirement
+ * is not met the call fails with STATUS_INVALID_DATA.
+ *
+ * @param accessControlProfileIds specifies the set of access control profiles that can
+ * authorize access to the provisioned element. If an identifier of a profile
+ * is given and this profile wasn't passed to startRetrieval() this call fails
+ * with STATUS_INVALID_DATA.
+ */
+ void startRetrieveEntryValue(in @utf8InCpp String nameSpace, in @utf8InCpp String name,
+ in int entrySize, in int[] accessControlProfileIds);
+
+ /**
+ * Retrieves an entry value, or part of one, if the entry value is larger than gcmChunkSize.
+ * May only be called after startRetrieveEntry().
+ *
+ * If the passed in data is not authentic, can't be decrypted, is of the wrong size, or can't
+ * be decoded, this call fails with STATUS_INVALID_DATA.
+ *
+ * @param encryptedContent contains the encrypted and MACed content.
+ *
+ * @return the entry value as CBOR, or part of the entry value in the case the
+ * content exceeds gcmChunkSize in length.
+ */
+ byte[] retrieveEntryValue(in byte[] encryptedContent);
+
+ /**
+ * End retrieval of data, optionally returning a message authentication code over the
+ * returned data.
+ *
+ * If signingKeyBlob or the sessionTranscript parameter passed to startRetrieval() is
+ * empty then the returned MAC will be empty.
+ *
+ * @param signingKeyBlob is either empty or a signingKeyBlob (see generateSigningKeyPair(),
+ * below) containing the signing key to use to sign the data retrieved. If this
+ * is not in the right format the call fails with STATUS_INVALID_DATA.
+ *
+ * @param out mac is empty if signingKeyBlob or the sessionTranscript passed to
+ * startRetrieval() is empty. Otherwise it is a COSE_Mac0 with empty payload
+ * and the detached content is set to DeviceAuthentication as defined below.
+ * The key used for the MAC operation is EMacKey and is derived as follows:
+ *
+ * KDF(ECDH(SDeviceKey.Priv, EReaderKey.Pub))
+ *
+ * where SDeviceKey.Priv is the key identified by signingKeyBlob. The KDF
+ * and ECDH functions shall be the same as the ciphersuite selected and
+ * passed to IIdentityStore.getCredential(). The EMacKey shall be derived
+ * using a salt of 0x00.
+ *
+ * DeviceAuthentication = [
+ * "DeviceAuthentication",
+ * SessionTranscript,
+ * DocType,
+ * DeviceNameSpaceBytes,
+ * ]
+ *
+ * DocType = tstr
+ *
+ * SessionTranscript = [
+ * DeviceEngagementBytes,
+ * EReaderKeyBytes
+ * ]
+ *
+ * DeviceEngagementBytes = #6.24(bstr .cbor DeviceEngagement)
+ * EReaderKeyBytes = #6.24(bstr .cbor EReaderKey.Pub)
+ * DeviceNameSpacesBytes = #6.24(bstr .cbor DeviceNameSpaces)
+ *
+ * where
+ *
+ * DeviceNameSpaces = {
+ * * NameSpace => DeviceSignedItems
+ * }
+ * DeviceSignedItems = {
+ * + DataItemName => DataItemValue
+ * }
+ *
+ * Namespace = tstr
+ * DataItemName = tstr
+ * DataItemValue = any
+ *
+ *
+ * @param out deviceNameSpaces the bytes of DeviceNameSpaces.
+ */
+ void finishRetrieval(in byte[] signingKeyBlob, out byte[] mac, out byte[] deviceNameSpaces);
+
+ /**
+ * Generate a key pair to be used for signing session data and retrieved data items.
+ *
+ * @param out signingKeyBlob contains an encrypted copy of the newly-generated private
+ * signing key.
+ *
+ * @return an X.509 certificate for the new signing key, signed by the credential key.
+ */
+ Certificate generateSigningKeyPair(out byte[] signingKeyBlob);
+}
diff --git a/identity/aidl/android/hardware/identity/IIdentityCredentialStore.aidl b/identity/aidl/android/hardware/identity/IIdentityCredentialStore.aidl
new file mode 100644
index 0000000..23cb1b7
--- /dev/null
+++ b/identity/aidl/android/hardware/identity/IIdentityCredentialStore.aidl
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2020 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.hardware.identity;
+
+import android.hardware.identity.IIdentityCredential;
+import android.hardware.identity.IWritableIdentityCredential;
+import android.hardware.identity.HardwareInformation;
+import android.hardware.identity.CipherSuite;
+
+/**
+ * IIdentityCredentialStore provides an interface to a secure store for user identity documents.
+ * This HAL is deliberately fairly general and abstract. To the extent possible, specification of
+ * the message formats and semantics of communication with credential verification devices and
+ * issuing authorities (IAs) is out of scope for this HAL. It provides the interface with secure
+ * storage but a credential-specific Android application will be required to implement the
+ * presentation and verification protocols and processes appropriate for the specific credential
+ * type.
+ *
+ * The design of this HAL makes few assumptions about the underlying secure hardware. In particular
+ * it does not assume that the secure hardware has any storage beyond that needed for a persistent,
+ * hardware-bound AES-128 key. However, its design allows the use of secure hardware that does have
+ * storage, specifically to enable "direct access". Most often credentials will be accessed through
+ * this HAL and through the Android layers above it but that requires that the Android device be
+ * powered up and fully functional. It is desirable to allow identity credential usage when the
+ * Android device's battery is too low to boot the Android operating system, so direct access to the
+ * secure hardware via NFC may allow data retrieval, if the secure hardware chooses to implement it.
+ * Definition of how data is retrieved in low power mode is explicitly out of scope for this HAL
+ * specification; it's up to the relevant identity credential standards to define.
+ *
+ * The 'default' HAL instance is explicitly not for direct access and the 'direct_access' HAL
+ * instance - if available - is for direct access. Applications are expected to provision their
+ * credential to both instances (and the contents may differ), not just one of them.
+ *
+ * Multiple credentials can be created. Each credential comprises:
+ *
+ * - A document type, which is a UTF-8 string of at most 256 bytes.
+ *
+ * - A set of namespaces, which serve to disambiguate value names. Namespaces are UTF-8 strings of
+ * up to 256 bytes in length (most should be much shorter). It is recommended that namespaces be
+ * structured as reverse domain names so that IANA effectively serves as the namespace registrar.
+ *
+ * - For each namespase, a set of name/value pairs, each with an associated set of access control
+ * profile IDs. Names are UTF-8 strings of up to 256 bytes in length (most should be much
+ * shorter). Values stored must be encoed as valid CBOR (https://tools.ietf.org/html/rfc7049) and
+ * the encoeded size is is limited to at most 512 KiB.
+ *
+ * - A set of access control profiles, each with a profile ID and a specification of the
+ * conditions which satisfy the profile's requirements.
+ *
+ * - An asymmetric key pair which is used to authenticate the credential to the IA, called the
+ * CredentialKey. This key is attested to by the secure hardware using Android Keystore
+ * attestation (https://source.android.com/security/keystore/attestation). See
+ * getAttestationCertificate() in the IWritableIdentityCredential for more information.
+ *
+ * - A set of zero or more named reader authentication public keys, which are used to authenticate
+ * an authorized reader to the credential.
+ *
+ * - A set of named signing keys, which are used to sign collections of values and session
+ * transcripts.
+ *
+ * Cryptographic notation:
+ *
+ * Throughout this HAL, cryptographic operations are specified in detail. To avoid repeating the
+ * definition of the notation, it's specified here. It is assumed that the reader is familiar with
+ * standard cryptographic algorithms and constructs.
+ *
+ * AES-GCM-ENC(K, N, D, A) represents AES-GCM encryption with key 'K', nonce 'N', additional
+ * authenticated data 'A' and data 'D'. The nonce is usually specified as 'R', meaning 12
+ * random bytes. The nonce is always 12 bytes and is prepended to the ciphertext. The GCM
+ * tag is 16 bytes, appended to the ciphertext. AES-GCM-DEC with the same argument notation
+ * represents the corresponding decryption operation.
+ *
+ * ECDSA(K, D) represents ECDSA signing of data 'D' with key 'K'.
+ *
+ * || represents concatenation
+ *
+ * {} represents an empty input; 0 bytes of data.
+ *
+ * HBK represents a device-unique, hardware-bound AES-128 key which exists only in secure
+ * hardware, except for "test" credential stores (see createCredential(), below). For test
+ * stores, an all-zero value is used in place of the HBK.
+ *
+ * Data encoding notation:
+ *
+ * Various fields need to be encoded as precisely-specified byte arrays. Where existing standards
+ * define appropriate encodings, those are used. For example, X.509 certificates. Where new
+ * encodings are needed, CBOR is used. CBOR maps are described in CDDL notation
+ * (https://tools.ietf.org/html/draft-ietf-cbor-cddl-06).
+ *
+ * All binder calls in the HAL may return a ServiceSpecificException with statuses from the
+ * STATUS_* integers defined in this interface. Each method states which status can be returned
+ * and under which circumstances.
+ */
+@VintfStability
+interface IIdentityCredentialStore {
+ /**
+ * Success.
+ */
+ const int STATUS_OK = 0;
+
+ /**
+ * The operation failed. This is used as a generic catch-all for errors that don't belong
+ * in other categories, including memory/resource allocation failures and I/O errors.
+ */
+ const int STATUS_FAILED = 1;
+
+ /**
+ * Unsupported cipher suite.
+ */
+ const int STATUS_CIPHER_SUITE_NOT_SUPPORTED = 2;
+
+ /**
+ * The passed data was invalid. This is a generic catch all for errors that don't belong
+ * in other categories related to parameter validation.
+ */
+ const int STATUS_INVALID_DATA = 3;
+
+ /**
+ * The authToken parameter passed to IIdentityCredential.startRetrieval() is not valid.
+ */
+ const int STATUS_INVALID_AUTH_TOKEN = 4;
+
+ /**
+ * The itemsRequest parameter passed to IIdentityCredential.startRetrieval() does not meet
+ * the requirements described in the documentation for that method.
+ */
+ const int STATUS_INVALID_ITEMS_REQUEST_MESSAGE = 5;
+
+ /**
+ * The readerSignature parameter in IIdentityCredential.startRetrieval() is invalid,
+ * doesn't contain an embedded certificate chain, or the signature failed to
+ * validate.
+ */
+ const int STATUS_READER_SIGNATURE_CHECK_FAILED = 6;
+
+ /**
+ * The sessionTranscript passed to startRetrieval() did not contain the ephmeral public
+ * key returned by createEphemeralPublicKey().
+ */
+ const int STATUS_EPHEMERAL_PUBLIC_KEY_NOT_FOUND = 7;
+
+ /**
+ * An access condition related to user authentication was not satisfied.
+ */
+ const int STATUS_USER_AUTHENTICATION_FAILED = 8;
+
+ /**
+ * An access condition related to reader authentication was not satisfied.
+ */
+ const int STATUS_READER_AUTHENTICATION_FAILED = 9;
+
+ /**
+ * The request data element has no access control profiles associated so it cannot be accessed.
+ */
+ const int STATUS_NO_ACCESS_CONTROL_PROFILES = 10;
+
+ /**
+ * The requested data element is not in the provided non-empty itemsRequest message.
+ */
+ const int STATUS_NOT_IN_REQUEST_MESSAGE = 11;
+
+ /**
+ * The passed-in sessionTranscript doesn't match the previously passed-in sessionTranscript.
+ */
+ const int STATUS_SESSION_TRANSCRIPT_MISMATCH = 12;
+
+ /**
+ * Returns information about hardware.
+ *
+ * @return a HardwareInformation with information about the hardware.
+ */
+ HardwareInformation getHardwareInformation();
+
+ /**
+ * createCredential creates a new Credential. When a Credential is created, two cryptographic
+ * keys are created: StorageKey, an AES key used to secure the externalized Credential
+ * contents, and CredentialKeyPair, an EC key pair used to authenticate the store to the IA. In
+ * addition, all of the Credential data content is imported and a certificate for the
+ * CredentialKeyPair and a signature produced with the CredentialKeyPair are created. These
+ * latter values may be checked by an issuing authority to verify that the data was imported
+ * into secure hardware and that it was imported unmodified.
+ *
+ * @param docType is an optional name (may be an empty string) that identifies the type of
+ * credential being created, e.g. "org.iso.18013-5.2019.mdl" (the doc type of the ISO
+ * driving license standard).
+ *
+ * @param testCredential indicates if this is a test store. Test credentials must use an
+ * all-zeros hardware-bound key (HBK) and must set the test bit in the
+ * personalizationReceipt (see finishAddingEntries(), in IWritableIdentityCredential).
+ *
+ * @return an IWritableIdentityCredential interface that provides operations to
+ * provision a credential.
+ */
+ IWritableIdentityCredential createCredential(in @utf8InCpp String docType,
+ in boolean testCredential);
+
+ /**
+ * getCredential retrieves an IIdentityCredential interface which allows use of a stored
+ * Credential.
+ *
+ * The cipher suite used to communicate with the remote verifier must also be specified. Currently
+ * only a single cipher-suite is supported and the details of this are as follow:
+ *
+ * - ECDHE with HKDF-SHA-256 for key agreement.
+ * - AES-256 with GCM block mode for authenticated encryption (nonces are incremented by one
+ * for every message).
+ * - ECDSA with SHA-256 for signing (used for signing session transcripts to defeat
+ * man-in-the-middle attacks), signing keys are not ephemeral.
+ *
+ * Support for other cipher suites may be added in a future version of this HAL.
+ *
+ * This method fails with STATUS_INVALID_DATA if the passed in credentialData cannot be
+ * decoded or decrypted.
+ *
+ * @param cipherSuite is the cipher suite to use.
+ *
+ * @param credentialData is a CBOR-encoded structure containing metadata about the credential
+ * and an encrypted byte array that contains data used to secure the credential. See the
+ * return argument of the same name in finishAddingEntries(), in
+ * IWritableIdentityCredential.
+ *
+ * @return an IIdentityCredential HIDL interface that provides operations on the Credential.
+ */
+ IIdentityCredential getCredential(in CipherSuite cipherSuite, in byte[] credentialData);
+}
diff --git a/identity/aidl/android/hardware/identity/IWritableIdentityCredential.aidl b/identity/aidl/android/hardware/identity/IWritableIdentityCredential.aidl
new file mode 100644
index 0000000..483b0c7
--- /dev/null
+++ b/identity/aidl/android/hardware/identity/IWritableIdentityCredential.aidl
@@ -0,0 +1,252 @@
+/*
+ * Copyright 2020 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.hardware.identity;
+
+import android.hardware.identity.Certificate;
+import android.hardware.identity.SecureAccessControlProfile;
+
+/**
+ * IWritableIdentityCredential is used to personalize a new identity credential. Credentials cannot
+ * be updated or modified after creation; any changes require deletion and re-creation.
+ */
+@VintfStability
+interface IWritableIdentityCredential {
+ /**
+ * Gets the certificate chain for credentialKey which can be used to prove the hardware
+ * characteristics to an issuing authority. Must not be called more than once.
+ *
+ * The certificate chain must be generated using Keymaster Attestation
+ * (see https://source.android.com/security/keystore/attestation) with the
+ * following additional requirements:
+ *
+ * - The attestationVersion field in the attestation extension must be at least 3.
+ *
+ * - The attestationSecurityLevel field must be set to either Software (0),
+ * TrustedEnvironment (1), or StrongBox (2) depending on how attestation is
+ * implemented. Only the default AOSP implementation of this HAL may use
+ * value 0 (additionally, this implementation must not be used on production
+ * devices).
+ *
+ * - The keymasterVersion field in the attestation extension must be set to (10*major + minor)
+ * where major and minor are the Identity Credential interface major and minor versions.
+ * Specifically for this version of the interface (1.0) this value is 10.
+ *
+ * - The keymasterSecurityLevel field in the attestation extension must be set to
+ * either Software (0), TrustedEnvironment (1), or StrongBox (2) depending on how
+ * the Trusted Application backing the HAL implementation is implemented. Only
+ * the default AOSP implementation of this HAL may use value 0 (additionally, this
+ * implementation must not be used on production devices)
+ *
+ * - The attestationChallenge field must be set to the passed-in challenge.
+ *
+ * - The uniqueId field must be empty.
+ *
+ * - The softwareEnforced field in the attestation extension must include
+ * Tag::ATTESTATION_APPLICATION_ID which must be set to the bytes of the passed-in
+ * attestationApplicationId.
+ *
+ * - The teeEnforced field in the attestation extension must include
+ * Tag::IDENTITY_CREDENTIAL_KEY. This tag indicates that the key is an Identity
+ * Credential key (which can only sign/MAC very specific messages) and not an Android
+ * Keystore key (which can be used to sign/MAC anything).
+ *
+ * Additional authorizations may be needed in the softwareEnforced and teeEnforced
+ * fields - the above is not an exhaustive list.
+ *
+ * @param attestationApplicationId is the DER encoded value to be stored
+ * in Tag::ATTESTATION_APPLICATION_ID. This schema is described in
+ * https://developer.android.com/training/articles/security-key-attestation#certificate_schema_attestationid
+ *
+ * @param attestationChallenge a challenge set by the issuer to ensure freshness.
+ *
+ * @return the X.509 certificate chain for the credentialKey
+ */
+ Certificate[] getAttestationCertificate(in byte[] attestationApplicationId, in byte[] attestationChallenge);
+
+ /**
+ * Start the personalization process.
+ *
+ * startPersonalization must not be called more than once.
+ *
+ * @param accessControlProfileCount specifies the number of access control profiles that will
+ * be provisioned with addAccessControlProfile().
+ *
+ * @param entryCounts specifies the number of data entries that will be provisioned with
+ * beginAddEntry() and addEntry(). Each item in the array specifies how many entries
+ * will be added for each name space.
+ */
+ void startPersonalization(in int accessControlProfileCount, in int[] entryCounts);
+
+ /**
+ * Add an access control profile, which defines the requirements or retrieval of one or more
+ * entries. If both readerCertificate and userAuthenticationRequired are empty/false,
+ * associated entries are open access, requiring no authentication to read (though the caller
+ * is free to require other authentication above this HAL).
+ *
+ * This method must be called exactly as many times as specified in the startPersonalization()
+ * accessControlProfileCount parameter. If this is requirement is not met, the method fails
+ * with STATUS_INVALID_DATA.
+ *
+ * @param id a numeric identifier that must be unique within the context of a Credential and may
+ * be used to reference the profile. If this is not satisfied the call fails with
+ * STATUS_INVALID_DATA.
+ *
+ * @param readerCertificate if non-empty, specifies a X.509 certificate (or chain of
+ * certificates) that must be used to authenticate requests (see the readerSignature
+ * parameter in IIdentityCredential.startRetrieval).
+ *
+ * @param userAuthenticationRequired if true, specifies that the user is required to
+ * authenticate to allow requests. Required authentication freshness is specified by
+ * timeout below.
+ *
+ * @param timeoutMillis specifies the amount of time, in milliseconds, for which a user
+ * authentication (see userAuthenticationRequired above) is valid, if
+ * userAuthenticationRequired is true. If the timout is zero then authentication is
+ * required for each reader session. If userAuthenticationRequired is false, the timeout
+ * must be zero. If this requirement is not met the call fails with STATUS_INVALID_DATA.
+ *
+ * @param secureUserId must be non-zero if userAuthenticationRequired is true. It is not
+ * related to any Android user ID or UID, but is created in the Gatekeeper application
+ * in the secure environment. If this requirement is not met the call fails with
+ * STATUS_INVALID_DATA.
+ *
+ * @return a structure with the passed-in data and MAC created with storageKey for authenticating
+ * the data at a later point in time.
+ */
+ SecureAccessControlProfile addAccessControlProfile(in int id, in Certificate readerCertificate,
+ in boolean userAuthenticationRequired, in long timeoutMillis, in long secureUserId);
+
+ /**
+ * Begins the process of adding an entry to the credential. All access control profiles must be
+ * added before calling this method. Entries must be added in namespace "groups", meaning all
+ * entries of one namespace must be added before adding entries from another namespace.
+ *
+ * This method must be called exactly as many times as the sum of the items in the entryCounts
+ * parameter specified in the startPersonalization(), and must be followed by one or more calls
+ * to addEntryValue(). If this requirement is not met the method fails with STATUS_INVALID_DATA.
+ *
+ * @param accessControlProfileIds specifies the set of access control profiles that can
+ * authorize access to the provisioned element.
+ *
+ * @param nameSpace is the namespace of the element, e.g. "org.iso.18013"
+ *
+ * @param name is the name of the element.
+ *
+ * @param entrySize is the size of the entry value. If this requirement
+ * is not met this method fails with STATUS_INVALID_DATA.
+ */
+ void beginAddEntry(in int[] accessControlProfileIds, in @utf8InCpp String nameSpace,
+ in @utf8InCpp String name, in int entrySize);
+
+ /**
+ * Continues the process of adding an entry, providing a value or part of a value.
+ *
+ * In the common case, this method will be called only once per entry added. In the case of
+ * values that are larger than the secure environment's GCM chunk size
+ * (see IIdentityCredentialStore.getHardwareInformation()), the caller must provide the
+ * value in chunks. All chunks must be exactly gcmChunkSize except the last and the sum of all
+ * chunk sizes must equal the value of the beginAddEntry() entrySize argument. If this
+ * requirement is not met the call fails with STATUS_INVALID_DATA.
+ *
+ * @param content is the entry value, encoded as CBOR. In the case the content exceeds gcmChunkSize,
+ * this may be partial content up to gcmChunkSize bytes long.
+ *
+ * @return the encrypted and MACed content. For directly-available credentials the contents are
+ * implementation-defined. For other credentials, the result contains
+ *
+ * AES-GCM-ENC(storageKey, R, Data, AdditionalData)
+ *
+ * where:
+ *
+ * Data = any ; value
+ *
+ * AdditionalData = {
+ * "Namespace" : tstr,
+ * "Name" : tstr,
+ * "AccessControlProfileIds" : [ + uint ],
+ * }
+ */
+ byte[] addEntryValue(in byte[] content);
+
+ /**
+ * Finishes adding entries and returns a signature that an issuing authority may use to
+ * validate that all data was provisioned correctly.
+ *
+ * After this method is called, the IWritableIdentityCredential is no longer usable.
+ *
+ * @param out credentialData is a CBOR-encoded structure (in CDDL notation):
+ *
+ * CredentialData = [
+ * tstr, ; docType, an optional name that identifies the type of credential
+ * bool, ; testCredential, indicates if this is a test credential
+ * bstr ; an opaque byte vector with encrypted data, see below
+ * ]
+ *
+ * The last element is an opaque byte vector which contains encrypted copies of the
+ * secrets used to secure the new credential's data and to authenticate the credential to
+ * the issuing authority. It contains:
+ *
+ * AES-GCM-ENC(HBK, R, CredentialKeys, docType)
+ *
+ * where HBK is a unique hardware-bound key that has never existed outside of the secure
+ * environment (except it's all zeroes if testCredential is True) and CredentialKeys is
+ * the CBOR-encoded structure (in CDDL notation):
+ *
+ * CredentialKeys = [
+ * bstr, ; storageKey, a 128-bit AES key
+ * bstr ; credentialPrivKey, the private key for credentialKey
+ * ]
+ *
+ * @param out proofOfProvisioningSignature proves to the IA that the credential was imported
+ * into the secure hardware without alteration or error. When the final addEntry() call is
+ * made (when the number of provisioned entries equals the sum of the items in
+ * startPersonalization() entryCounts parameter), a COSE_Sign1 structure
+ * signed by CredentialKey with payload set to the ProofOfProvisioning CBOR below:
+ *
+ * ProofOfProvisioning = [
+ * "ProofOfProvisioning",
+ * tstr, ; DocType
+ * [ * AccessControlProfile ],
+ * ProvisionedData,
+ * bool ; true if this is a test credential, should
+ * ; always be false.
+ * ]
+ *
+ * AccessControlProfile = {
+ * "id" : uint,
+ * ? "readerCertificate" : bstr,
+ * ? (
+ * "userAuthenticationRequired" : bool,
+ * "timeoutMillis" : uint,
+ * )
+ * }
+ *
+ * ProvisionedData = {
+ * * Namespace => [ + Entry ]
+ * },
+ *
+ * Namespace = tstr
+ *
+ * Entry = {
+ * "name" : tstr,
+ * "value" : any,
+ * "accessControlProfiles" : [ * uint ],
+ * }
+ */
+ void finishAddingEntries(out byte[] credentialData,
+ out byte[] proofOfProvisioningSignature);
+}
diff --git a/identity/aidl/android/hardware/identity/SecureAccessControlProfile.aidl b/identity/aidl/android/hardware/identity/SecureAccessControlProfile.aidl
new file mode 100644
index 0000000..01d312d
--- /dev/null
+++ b/identity/aidl/android/hardware/identity/SecureAccessControlProfile.aidl
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2020 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.hardware.identity;
+
+import android.hardware.identity.Certificate;
+
+@VintfStability
+parcelable SecureAccessControlProfile {
+ /**
+ * id is a numeric identifier that must be unique within the context of a Credential and may be
+ * used to reference the profile.
+ */
+ int id;
+
+ /**
+ * readerCertificate, if non-empty, specifies a single X.509 certificate (not a chain
+ * of certificates) that must be used to authenticate requests. For details about how
+ * this is done, see the readerSignature paremter of IIdentityCredential.startRetrieval.
+ */
+ Certificate readerCertificate;
+
+ /**
+ * if true, the user is required to authenticate to allow requests. Required authentication
+ * fressness is specified by timeout below.
+ *
+ */
+ boolean userAuthenticationRequired;
+
+ /**
+ * Timeout specifies the amount of time, in milliseconds, for which a user authentication (see
+ * above) is valid, if userAuthenticationRequired is set to true. If userAuthenticationRequired
+ * is true and timout is zero then authentication is required for each reader session.
+ *
+ * If userAuthenticationRequired is false, timeout must be zero.
+ */
+ long timeoutMillis;
+
+ /**
+ * secureUserId must be non-zero if userAuthenticationRequired is true.
+ * It is not related to any Android user ID or UID, but is created in the
+ * Gatekeeper application in the secure environment.
+ */
+ long secureUserId;
+
+ /**
+ * The mac is used to authenticate the access control profile. It contains:
+ *
+ * AES-GCM-ENC(storageKey, R, {}, AccessControlProfile)
+ *
+ * where AccessControlProfile is the CBOR map:
+ *
+ * AccessControlProfile = {
+ * "id": uint,
+ * ? "readerCertificate" : bstr,
+ * ? (
+ * "userAuthenticationRequired" : bool,
+ * "timeoutMillis" : uint,
+ * "secureUserId" : uint
+ * )
+ * }
+ */
+ byte[] mac;
+}
+
diff --git a/identity/aidl/default/Android.bp b/identity/aidl/default/Android.bp
new file mode 100644
index 0000000..2eb0faa
--- /dev/null
+++ b/identity/aidl/default/Android.bp
@@ -0,0 +1,29 @@
+cc_binary {
+ name: "android.hardware.identity-service.example",
+ relative_install_path: "hw",
+ init_rc: ["identity-default.rc"],
+ vintf_fragments: ["identity-default.xml"],
+ vendor: true,
+ cflags: [
+ "-Wall",
+ "-Wextra",
+ ],
+ shared_libs: [
+ "libbase",
+ "libbinder_ndk",
+ "libcppbor",
+ "libcrypto",
+ "liblog",
+ "libutils",
+ "android.hardware.identity-support-lib",
+ "android.hardware.identity-ndk_platform",
+ "android.hardware.keymaster-ndk_platform",
+ ],
+ srcs: [
+ "IdentityCredential.cpp",
+ "IdentityCredentialStore.cpp",
+ "WritableIdentityCredential.cpp",
+ "Util.cpp",
+ "service.cpp",
+ ],
+}
diff --git a/identity/aidl/default/IdentityCredential.cpp b/identity/aidl/default/IdentityCredential.cpp
new file mode 100644
index 0000000..d5b3a0f
--- /dev/null
+++ b/identity/aidl/default/IdentityCredential.cpp
@@ -0,0 +1,768 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "IdentityCredential"
+
+#include "IdentityCredential.h"
+#include "IdentityCredentialStore.h"
+#include "Util.h"
+
+#include <android/hardware/identity/support/IdentityCredentialSupport.h>
+
+#include <string.h>
+
+#include <android-base/logging.h>
+
+#include <cppbor.h>
+#include <cppbor_parse.h>
+
+namespace aidl::android::hardware::identity {
+
+using ::aidl::android::hardware::keymaster::Timestamp;
+using ::std::optional;
+
+using namespace ::android::hardware::identity;
+
+int IdentityCredential::initialize() {
+ auto [item, _, message] = cppbor::parse(credentialData_);
+ if (item == nullptr) {
+ LOG(ERROR) << "CredentialData is not valid CBOR: " << message;
+ return IIdentityCredentialStore::STATUS_INVALID_DATA;
+ }
+
+ const cppbor::Array* arrayItem = item->asArray();
+ if (arrayItem == nullptr || arrayItem->size() != 3) {
+ LOG(ERROR) << "CredentialData is not an array with three elements";
+ return IIdentityCredentialStore::STATUS_INVALID_DATA;
+ }
+
+ const cppbor::Tstr* docTypeItem = (*arrayItem)[0]->asTstr();
+ const cppbor::Bool* testCredentialItem =
+ ((*arrayItem)[1]->asSimple() != nullptr ? ((*arrayItem)[1]->asSimple()->asBool())
+ : nullptr);
+ const cppbor::Bstr* encryptedCredentialKeysItem = (*arrayItem)[2]->asBstr();
+ if (docTypeItem == nullptr || testCredentialItem == nullptr ||
+ encryptedCredentialKeysItem == nullptr) {
+ LOG(ERROR) << "CredentialData unexpected item types";
+ return IIdentityCredentialStore::STATUS_INVALID_DATA;
+ }
+
+ docType_ = docTypeItem->value();
+ testCredential_ = testCredentialItem->value();
+
+ vector<uint8_t> hardwareBoundKey;
+ if (testCredential_) {
+ hardwareBoundKey = support::getTestHardwareBoundKey();
+ } else {
+ hardwareBoundKey = getHardwareBoundKey();
+ }
+
+ const vector<uint8_t>& encryptedCredentialKeys = encryptedCredentialKeysItem->value();
+ const vector<uint8_t> docTypeVec(docType_.begin(), docType_.end());
+ optional<vector<uint8_t>> decryptedCredentialKeys =
+ support::decryptAes128Gcm(hardwareBoundKey, encryptedCredentialKeys, docTypeVec);
+ if (!decryptedCredentialKeys) {
+ LOG(ERROR) << "Error decrypting CredentialKeys";
+ return IIdentityCredentialStore::STATUS_INVALID_DATA;
+ }
+
+ auto [dckItem, dckPos, dckMessage] = cppbor::parse(decryptedCredentialKeys.value());
+ if (dckItem == nullptr) {
+ LOG(ERROR) << "Decrypted CredentialKeys is not valid CBOR: " << dckMessage;
+ return IIdentityCredentialStore::STATUS_INVALID_DATA;
+ }
+ const cppbor::Array* dckArrayItem = dckItem->asArray();
+ if (dckArrayItem == nullptr || dckArrayItem->size() != 2) {
+ LOG(ERROR) << "Decrypted CredentialKeys is not an array with two elements";
+ return IIdentityCredentialStore::STATUS_INVALID_DATA;
+ }
+ const cppbor::Bstr* storageKeyItem = (*dckArrayItem)[0]->asBstr();
+ const cppbor::Bstr* credentialPrivKeyItem = (*dckArrayItem)[1]->asBstr();
+ if (storageKeyItem == nullptr || credentialPrivKeyItem == nullptr) {
+ LOG(ERROR) << "CredentialKeys unexpected item types";
+ return IIdentityCredentialStore::STATUS_INVALID_DATA;
+ }
+ storageKey_ = storageKeyItem->value();
+ credentialPrivKey_ = credentialPrivKeyItem->value();
+
+ return IIdentityCredentialStore::STATUS_OK;
+}
+
+ndk::ScopedAStatus IdentityCredential::deleteCredential(
+ vector<int8_t>* outProofOfDeletionSignature) {
+ cppbor::Array array = {"ProofOfDeletion", docType_, testCredential_};
+ vector<uint8_t> proofOfDeletion = array.encode();
+
+ optional<vector<uint8_t>> signature = support::coseSignEcDsa(credentialPrivKey_,
+ proofOfDeletion, // payload
+ {}, // additionalData
+ {}); // certificateChain
+ if (!signature) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED, "Error signing data"));
+ }
+
+ *outProofOfDeletionSignature = byteStringToSigned(signature.value());
+ return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus IdentityCredential::createEphemeralKeyPair(vector<int8_t>* outKeyPair) {
+ optional<vector<uint8_t>> kp = support::createEcKeyPair();
+ if (!kp) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED, "Error creating ephemeral key pair"));
+ }
+
+ // Stash public key of this key-pair for later check in startRetrieval().
+ optional<vector<uint8_t>> publicKey = support::ecKeyPairGetPublicKey(kp.value());
+ if (!publicKey) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED,
+ "Error getting public part of ephemeral key pair"));
+ }
+ ephemeralPublicKey_ = publicKey.value();
+
+ *outKeyPair = byteStringToSigned(kp.value());
+ return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus IdentityCredential::setReaderEphemeralPublicKey(
+ const vector<int8_t>& publicKey) {
+ readerPublicKey_ = byteStringToUnsigned(publicKey);
+ return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus IdentityCredential::createAuthChallenge(int64_t* outChallenge) {
+ uint64_t challenge = 0;
+ while (challenge == 0) {
+ optional<vector<uint8_t>> bytes = support::getRandom(8);
+ if (!bytes) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED,
+ "Error getting random data for challenge"));
+ }
+
+ challenge = 0;
+ for (size_t n = 0; n < bytes.value().size(); n++) {
+ challenge |= ((bytes.value())[n] << (n * 8));
+ }
+ }
+
+ *outChallenge = challenge;
+ return ndk::ScopedAStatus::ok();
+}
+
+// TODO: this could be a lot faster if we did all the splitting and pubkey extraction
+// ahead of time.
+bool checkReaderAuthentication(const SecureAccessControlProfile& profile,
+ const vector<uint8_t>& readerCertificateChain) {
+ optional<vector<uint8_t>> acpPubKey = support::certificateChainGetTopMostKey(
+ byteStringToUnsigned(profile.readerCertificate.encodedCertificate));
+ if (!acpPubKey) {
+ LOG(ERROR) << "Error extracting public key from readerCertificate in profile";
+ return false;
+ }
+
+ optional<vector<vector<uint8_t>>> certificatesInChain =
+ support::certificateChainSplit(readerCertificateChain);
+ if (!certificatesInChain) {
+ LOG(ERROR) << "Error splitting readerCertificateChain";
+ return false;
+ }
+ for (const vector<uint8_t>& certInChain : certificatesInChain.value()) {
+ optional<vector<uint8_t>> certPubKey = support::certificateChainGetTopMostKey(certInChain);
+ if (!certPubKey) {
+ LOG(ERROR)
+ << "Error extracting public key from certificate in chain presented by reader";
+ return false;
+ }
+ if (acpPubKey.value() == certPubKey.value()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+Timestamp clockGetTime() {
+ struct timespec time;
+ clock_gettime(CLOCK_MONOTONIC, &time);
+ Timestamp ts;
+ ts.milliSeconds = time.tv_sec * 1000 + time.tv_nsec / 1000000;
+ return ts;
+}
+
+bool checkUserAuthentication(const SecureAccessControlProfile& profile,
+ const HardwareAuthToken& authToken, uint64_t authChallenge) {
+ if (profile.secureUserId != authToken.userId) {
+ LOG(ERROR) << "secureUserId in profile (" << profile.secureUserId
+ << ") differs from userId in authToken (" << authToken.userId << ")";
+ return false;
+ }
+
+ if (profile.timeoutMillis == 0) {
+ if (authToken.challenge == 0) {
+ LOG(ERROR) << "No challenge in authToken";
+ return false;
+ }
+
+ if (authToken.challenge != int64_t(authChallenge)) {
+ LOG(ERROR) << "Challenge in authToken doesn't match the challenge we created";
+ return false;
+ }
+ return true;
+ }
+
+ // Note that the Epoch for timestamps in HardwareAuthToken is at the
+ // discretion of the vendor:
+ //
+ // "[...] since some starting point (generally the most recent device
+ // boot) which all of the applications within one secure environment
+ // must agree upon."
+ //
+ // Therefore, if this software implementation is used on a device which isn't
+ // the emulator then the assumption that the epoch is the same as used in
+ // clockGetTime above will not hold. This is OK as this software
+ // implementation should never be used on a real device.
+ //
+ Timestamp now = clockGetTime();
+ if (authToken.timestamp.milliSeconds > now.milliSeconds) {
+ LOG(ERROR) << "Timestamp in authToken (" << authToken.timestamp.milliSeconds
+ << ") is in the future (now: " << now.milliSeconds << ")";
+ return false;
+ }
+ if (now.milliSeconds > authToken.timestamp.milliSeconds + profile.timeoutMillis) {
+ LOG(ERROR) << "Deadline for authToken (" << authToken.timestamp.milliSeconds << " + "
+ << profile.timeoutMillis << " = "
+ << (authToken.timestamp.milliSeconds + profile.timeoutMillis)
+ << ") is in the past (now: " << now.milliSeconds << ")";
+ return false;
+ }
+ return true;
+}
+
+ndk::ScopedAStatus IdentityCredential::startRetrieval(
+ const vector<SecureAccessControlProfile>& accessControlProfiles,
+ const HardwareAuthToken& authToken, const vector<int8_t>& itemsRequestS,
+ const vector<int8_t>& sessionTranscriptS, const vector<int8_t>& readerSignatureS,
+ const vector<int32_t>& requestCounts) {
+ auto sessionTranscript = byteStringToUnsigned(sessionTranscriptS);
+ auto itemsRequest = byteStringToUnsigned(itemsRequestS);
+ auto readerSignature = byteStringToUnsigned(readerSignatureS);
+
+ if (sessionTranscript.size() > 0) {
+ auto [item, _, message] = cppbor::parse(sessionTranscript);
+ if (item == nullptr) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA,
+ "SessionTranscript contains invalid CBOR"));
+ }
+ sessionTranscriptItem_ = std::move(item);
+ }
+ if (numStartRetrievalCalls_ > 0) {
+ if (sessionTranscript_ != sessionTranscript) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_SESSION_TRANSCRIPT_MISMATCH,
+ "Passed-in SessionTranscript doesn't match previously used SessionTranscript"));
+ }
+ }
+ sessionTranscript_ = sessionTranscript;
+
+ // If there is a signature, validate that it was made with the top-most key in the
+ // certificate chain embedded in the COSE_Sign1 structure.
+ optional<vector<uint8_t>> readerCertificateChain;
+ if (readerSignature.size() > 0) {
+ readerCertificateChain = support::coseSignGetX5Chain(readerSignature);
+ if (!readerCertificateChain) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_READER_SIGNATURE_CHECK_FAILED,
+ "Unable to get reader certificate chain from COSE_Sign1"));
+ }
+
+ if (!support::certificateChainValidate(readerCertificateChain.value())) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_READER_SIGNATURE_CHECK_FAILED,
+ "Error validating reader certificate chain"));
+ }
+
+ optional<vector<uint8_t>> readerPublicKey =
+ support::certificateChainGetTopMostKey(readerCertificateChain.value());
+ if (!readerPublicKey) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_READER_SIGNATURE_CHECK_FAILED,
+ "Unable to get public key from reader certificate chain"));
+ }
+
+ const vector<uint8_t>& itemsRequestBytes = itemsRequest;
+ vector<uint8_t> dataThatWasSigned = cppbor::Array()
+ .add("ReaderAuthentication")
+ .add(sessionTranscriptItem_->clone())
+ .add(cppbor::Semantic(24, itemsRequestBytes))
+ .encode();
+ if (!support::coseCheckEcDsaSignature(readerSignature,
+ dataThatWasSigned, // detached content
+ readerPublicKey.value())) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_READER_SIGNATURE_CHECK_FAILED,
+ "readerSignature check failed"));
+ }
+ }
+
+ // Here's where we would validate the passed-in |authToken| to assure ourselves
+ // that it comes from the e.g. biometric hardware and wasn't made up by an attacker.
+ //
+ // However this involves calculating the MAC. However this requires access
+ // to the key needed to a pre-shared key which we don't have...
+ //
+
+ // To prevent replay-attacks, we check that the public part of the ephemeral
+ // key we previously created, is present in the DeviceEngagement part of
+ // SessionTranscript as a COSE_Key, in uncompressed form.
+ //
+ // We do this by just searching for the X and Y coordinates.
+ if (sessionTranscript.size() > 0) {
+ const cppbor::Array* array = sessionTranscriptItem_->asArray();
+ if (array == nullptr || array->size() != 2) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_EPHEMERAL_PUBLIC_KEY_NOT_FOUND,
+ "SessionTranscript is not an array with two items"));
+ }
+ const cppbor::Semantic* taggedEncodedDE = (*array)[0]->asSemantic();
+ if (taggedEncodedDE == nullptr || taggedEncodedDE->value() != 24) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_EPHEMERAL_PUBLIC_KEY_NOT_FOUND,
+ "First item in SessionTranscript array is not a "
+ "semantic with value 24"));
+ }
+ const cppbor::Bstr* encodedDE = (taggedEncodedDE->child())->asBstr();
+ if (encodedDE == nullptr) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_EPHEMERAL_PUBLIC_KEY_NOT_FOUND,
+ "Child of semantic in first item in SessionTranscript "
+ "array is not a bstr"));
+ }
+ const vector<uint8_t>& bytesDE = encodedDE->value();
+
+ auto [getXYSuccess, ePubX, ePubY] = support::ecPublicKeyGetXandY(ephemeralPublicKey_);
+ if (!getXYSuccess) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_EPHEMERAL_PUBLIC_KEY_NOT_FOUND,
+ "Error extracting X and Y from ePub"));
+ }
+ if (sessionTranscript.size() > 0 &&
+ !(memmem(bytesDE.data(), bytesDE.size(), ePubX.data(), ePubX.size()) != nullptr &&
+ memmem(bytesDE.data(), bytesDE.size(), ePubY.data(), ePubY.size()) != nullptr)) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_EPHEMERAL_PUBLIC_KEY_NOT_FOUND,
+ "Did not find ephemeral public key's X and Y coordinates in "
+ "SessionTranscript (make sure leading zeroes are not used)"));
+ }
+ }
+
+ // itemsRequest: If non-empty, contains request data that may be signed by the
+ // reader. The content can be defined in the way appropriate for the
+ // credential, but there are three requirements that must be met to work with
+ // this HAL:
+ if (itemsRequest.size() > 0) {
+ // 1. The content must be a CBOR-encoded structure.
+ auto [item, _, message] = cppbor::parse(itemsRequest);
+ if (item == nullptr) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_ITEMS_REQUEST_MESSAGE,
+ "Error decoding CBOR in itemsRequest"));
+ }
+
+ // 2. The CBOR structure must be a map.
+ const cppbor::Map* map = item->asMap();
+ if (map == nullptr) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_ITEMS_REQUEST_MESSAGE,
+ "itemsRequest is not a CBOR map"));
+ }
+
+ // 3. The map must contain a key "nameSpaces" whose value contains a map, as described in
+ // the example below.
+ //
+ // NameSpaces = {
+ // + NameSpace => DataElements ; Requested data elements for each NameSpace
+ // }
+ //
+ // NameSpace = tstr
+ //
+ // DataElements = {
+ // + DataElement => IntentToRetain
+ // }
+ //
+ // DataElement = tstr
+ // IntentToRetain = bool
+ //
+ // Here's an example of an |itemsRequest| CBOR value satisfying above requirements 1.
+ // through 3.:
+ //
+ // {
+ // 'docType' : 'org.iso.18013-5.2019',
+ // 'nameSpaces' : {
+ // 'org.iso.18013-5.2019' : {
+ // 'Last name' : false,
+ // 'Birth date' : false,
+ // 'First name' : false,
+ // 'Home address' : true
+ // },
+ // 'org.aamva.iso.18013-5.2019' : {
+ // 'Real Id' : false
+ // }
+ // }
+ // }
+ //
+ const cppbor::Map* nsMap = nullptr;
+ for (size_t n = 0; n < map->size(); n++) {
+ const auto& [keyItem, valueItem] = (*map)[n];
+ if (keyItem->type() == cppbor::TSTR && keyItem->asTstr()->value() == "nameSpaces" &&
+ valueItem->type() == cppbor::MAP) {
+ nsMap = valueItem->asMap();
+ break;
+ }
+ }
+ if (nsMap == nullptr) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_ITEMS_REQUEST_MESSAGE,
+ "No nameSpaces map in top-most map"));
+ }
+
+ for (size_t n = 0; n < nsMap->size(); n++) {
+ auto [nsKeyItem, nsValueItem] = (*nsMap)[n];
+ const cppbor::Tstr* nsKey = nsKeyItem->asTstr();
+ const cppbor::Map* nsInnerMap = nsValueItem->asMap();
+ if (nsKey == nullptr || nsInnerMap == nullptr) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_ITEMS_REQUEST_MESSAGE,
+ "Type mismatch in nameSpaces map"));
+ }
+ string requestedNamespace = nsKey->value();
+ vector<string> requestedKeys;
+ for (size_t m = 0; m < nsInnerMap->size(); m++) {
+ const auto& [innerMapKeyItem, innerMapValueItem] = (*nsInnerMap)[m];
+ const cppbor::Tstr* nameItem = innerMapKeyItem->asTstr();
+ const cppbor::Simple* simple = innerMapValueItem->asSimple();
+ const cppbor::Bool* intentToRetainItem =
+ (simple != nullptr) ? simple->asBool() : nullptr;
+ if (nameItem == nullptr || intentToRetainItem == nullptr) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_ITEMS_REQUEST_MESSAGE,
+ "Type mismatch in value in nameSpaces map"));
+ }
+ requestedKeys.push_back(nameItem->value());
+ }
+ requestedNameSpacesAndNames_[requestedNamespace] = requestedKeys;
+ }
+ }
+
+ // Finally, validate all the access control profiles in the requestData.
+ bool haveAuthToken = (authToken.mac.size() > 0);
+ for (const auto& profile : accessControlProfiles) {
+ if (!secureAccessControlProfileCheckMac(profile, storageKey_)) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA,
+ "Error checking MAC for profile"));
+ }
+ int accessControlCheck = IIdentityCredentialStore::STATUS_OK;
+ if (profile.userAuthenticationRequired) {
+ if (!haveAuthToken || !checkUserAuthentication(profile, authToken, authChallenge_)) {
+ accessControlCheck = IIdentityCredentialStore::STATUS_USER_AUTHENTICATION_FAILED;
+ }
+ } else if (profile.readerCertificate.encodedCertificate.size() > 0) {
+ if (!readerCertificateChain ||
+ !checkReaderAuthentication(profile, readerCertificateChain.value())) {
+ accessControlCheck = IIdentityCredentialStore::STATUS_READER_AUTHENTICATION_FAILED;
+ }
+ }
+ profileIdToAccessCheckResult_[profile.id] = accessControlCheck;
+ }
+
+ deviceNameSpacesMap_ = cppbor::Map();
+ currentNameSpaceDeviceNameSpacesMap_ = cppbor::Map();
+
+ requestCountsRemaining_ = requestCounts;
+ currentNameSpace_ = "";
+
+ itemsRequest_ = itemsRequest;
+
+ numStartRetrievalCalls_ += 1;
+ return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus IdentityCredential::startRetrieveEntryValue(
+ const string& nameSpace, const string& name, int32_t entrySize,
+ const vector<int32_t>& accessControlProfileIds) {
+ if (name.empty()) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA, "Name cannot be empty"));
+ }
+ if (nameSpace.empty()) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA, "Name space cannot be empty"));
+ }
+
+ if (requestCountsRemaining_.size() == 0) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA,
+ "No more name spaces left to go through"));
+ }
+
+ if (currentNameSpace_ == "") {
+ // First call.
+ currentNameSpace_ = nameSpace;
+ }
+
+ if (nameSpace == currentNameSpace_) {
+ // Same namespace.
+ if (requestCountsRemaining_[0] == 0) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA,
+ "No more entries to be retrieved in current name space"));
+ }
+ requestCountsRemaining_[0] -= 1;
+ } else {
+ // New namespace.
+ if (requestCountsRemaining_[0] != 0) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA,
+ "Moved to new name space but one or more entries need to be retrieved "
+ "in current name space"));
+ }
+ if (currentNameSpaceDeviceNameSpacesMap_.size() > 0) {
+ deviceNameSpacesMap_.add(currentNameSpace_,
+ std::move(currentNameSpaceDeviceNameSpacesMap_));
+ }
+ currentNameSpaceDeviceNameSpacesMap_ = cppbor::Map();
+
+ requestCountsRemaining_.erase(requestCountsRemaining_.begin());
+ currentNameSpace_ = nameSpace;
+ }
+
+ // It's permissible to have an empty itemsRequest... but if non-empty you can
+ // only request what was specified in said itemsRequest. Enforce that.
+ if (itemsRequest_.size() > 0) {
+ const auto& it = requestedNameSpacesAndNames_.find(nameSpace);
+ if (it == requestedNameSpacesAndNames_.end()) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_NOT_IN_REQUEST_MESSAGE,
+ "Name space was not requested in startRetrieval"));
+ }
+ const auto& dataItemNames = it->second;
+ if (std::find(dataItemNames.begin(), dataItemNames.end(), name) == dataItemNames.end()) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_NOT_IN_REQUEST_MESSAGE,
+ "Data item name in name space was not requested in startRetrieval"));
+ }
+ }
+
+ // Enforce access control.
+ //
+ // Access is granted if at least one of the profiles grants access.
+ //
+ // If an item is configured without any profiles, access is denied.
+ //
+ int accessControl = IIdentityCredentialStore::STATUS_NO_ACCESS_CONTROL_PROFILES;
+ for (auto id : accessControlProfileIds) {
+ auto search = profileIdToAccessCheckResult_.find(id);
+ if (search == profileIdToAccessCheckResult_.end()) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA,
+ "Requested entry with unvalidated profile id"));
+ }
+ int accessControlForProfile = search->second;
+ if (accessControlForProfile == IIdentityCredentialStore::STATUS_OK) {
+ accessControl = IIdentityCredentialStore::STATUS_OK;
+ break;
+ }
+ accessControl = accessControlForProfile;
+ }
+ if (accessControl != IIdentityCredentialStore::STATUS_OK) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ int(accessControl), "Access control check failed"));
+ }
+
+ entryAdditionalData_ = entryCreateAdditionalData(nameSpace, name, accessControlProfileIds);
+
+ currentName_ = name;
+ entryRemainingBytes_ = entrySize;
+ entryValue_.resize(0);
+
+ return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus IdentityCredential::retrieveEntryValue(const vector<int8_t>& encryptedContentS,
+ vector<int8_t>* outContent) {
+ auto encryptedContent = byteStringToUnsigned(encryptedContentS);
+
+ optional<vector<uint8_t>> content =
+ support::decryptAes128Gcm(storageKey_, encryptedContent, entryAdditionalData_);
+ if (!content) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA, "Error decrypting data"));
+ }
+
+ size_t chunkSize = content.value().size();
+
+ if (chunkSize > entryRemainingBytes_) {
+ LOG(ERROR) << "Retrieved chunk of size " << chunkSize
+ << " is bigger than remaining space of size " << entryRemainingBytes_;
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA,
+ "Retrieved chunk is bigger than remaining space"));
+ }
+
+ entryRemainingBytes_ -= chunkSize;
+ if (entryRemainingBytes_ > 0) {
+ if (chunkSize != IdentityCredentialStore::kGcmChunkSize) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA,
+ "Retrieved non-final chunk of size which isn't kGcmChunkSize"));
+ }
+ }
+
+ entryValue_.insert(entryValue_.end(), content.value().begin(), content.value().end());
+
+ if (entryRemainingBytes_ == 0) {
+ auto [entryValueItem, _, message] = cppbor::parse(entryValue_);
+ if (entryValueItem == nullptr) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA,
+ "Retrieved data which is invalid CBOR"));
+ }
+ currentNameSpaceDeviceNameSpacesMap_.add(currentName_, std::move(entryValueItem));
+ }
+
+ *outContent = byteStringToSigned(content.value());
+ return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus IdentityCredential::finishRetrieval(const vector<int8_t>& signingKeyBlobS,
+ vector<int8_t>* outMac,
+ vector<int8_t>* outDeviceNameSpaces) {
+ auto signingKeyBlob = byteStringToUnsigned(signingKeyBlobS);
+
+ if (currentNameSpaceDeviceNameSpacesMap_.size() > 0) {
+ deviceNameSpacesMap_.add(currentNameSpace_,
+ std::move(currentNameSpaceDeviceNameSpacesMap_));
+ }
+ vector<uint8_t> encodedDeviceNameSpaces = deviceNameSpacesMap_.encode();
+
+ // If there's no signing key or no sessionTranscript or no reader ephemeral
+ // public key, we return the empty MAC.
+ optional<vector<uint8_t>> mac;
+ if (signingKeyBlob.size() > 0 && sessionTranscript_.size() > 0 && readerPublicKey_.size() > 0) {
+ cppbor::Array array;
+ array.add("DeviceAuthentication");
+ array.add(sessionTranscriptItem_->clone());
+ array.add(docType_);
+ array.add(cppbor::Semantic(24, encodedDeviceNameSpaces));
+ vector<uint8_t> encodedDeviceAuthentication = array.encode();
+
+ vector<uint8_t> docTypeAsBlob(docType_.begin(), docType_.end());
+ optional<vector<uint8_t>> signingKey =
+ support::decryptAes128Gcm(storageKey_, signingKeyBlob, docTypeAsBlob);
+ if (!signingKey) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA,
+ "Error decrypting signingKeyBlob"));
+ }
+
+ optional<vector<uint8_t>> sharedSecret =
+ support::ecdh(readerPublicKey_, signingKey.value());
+ if (!sharedSecret) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED, "Error doing ECDH"));
+ }
+
+ vector<uint8_t> salt = {0x00};
+ vector<uint8_t> info = {};
+ optional<vector<uint8_t>> derivedKey = support::hkdf(sharedSecret.value(), salt, info, 32);
+ if (!derivedKey) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED,
+ "Error deriving key from shared secret"));
+ }
+
+ mac = support::coseMac0(derivedKey.value(), {}, // payload
+ encodedDeviceAuthentication); // additionalData
+ if (!mac) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED, "Error MACing data"));
+ }
+ }
+
+ *outMac = byteStringToSigned(mac.value_or(vector<uint8_t>({})));
+ *outDeviceNameSpaces = byteStringToSigned(encodedDeviceNameSpaces);
+ return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus IdentityCredential::generateSigningKeyPair(
+ vector<int8_t>* outSigningKeyBlob, Certificate* outSigningKeyCertificate) {
+ string serialDecimal = "0"; // TODO: set serial to something unique
+ string issuer = "Android Open Source Project";
+ string subject = "Android IdentityCredential Reference Implementation";
+ time_t validityNotBefore = time(nullptr);
+ time_t validityNotAfter = validityNotBefore + 365 * 24 * 3600;
+
+ optional<vector<uint8_t>> signingKeyPKCS8 = support::createEcKeyPair();
+ if (!signingKeyPKCS8) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED, "Error creating signingKey"));
+ }
+
+ optional<vector<uint8_t>> signingPublicKey =
+ support::ecKeyPairGetPublicKey(signingKeyPKCS8.value());
+ if (!signingPublicKey) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED,
+ "Error getting public part of signingKey"));
+ }
+
+ optional<vector<uint8_t>> signingKey = support::ecKeyPairGetPrivateKey(signingKeyPKCS8.value());
+ if (!signingKey) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED,
+ "Error getting private part of signingKey"));
+ }
+
+ optional<vector<uint8_t>> certificate = support::ecPublicKeyGenerateCertificate(
+ signingPublicKey.value(), credentialPrivKey_, serialDecimal, issuer, subject,
+ validityNotBefore, validityNotAfter);
+ if (!certificate) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED, "Error creating signingKey"));
+ }
+
+ optional<vector<uint8_t>> nonce = support::getRandom(12);
+ if (!nonce) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED, "Error getting random"));
+ }
+ vector<uint8_t> docTypeAsBlob(docType_.begin(), docType_.end());
+ optional<vector<uint8_t>> encryptedSigningKey = support::encryptAes128Gcm(
+ storageKey_, nonce.value(), signingKey.value(), docTypeAsBlob);
+ if (!encryptedSigningKey) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED, "Error encrypting signingKey"));
+ }
+ *outSigningKeyBlob = byteStringToSigned(encryptedSigningKey.value());
+ *outSigningKeyCertificate = Certificate();
+ outSigningKeyCertificate->encodedCertificate = byteStringToSigned(certificate.value());
+ return ndk::ScopedAStatus::ok();
+}
+
+} // namespace aidl::android::hardware::identity
diff --git a/identity/aidl/default/IdentityCredential.h b/identity/aidl/default/IdentityCredential.h
new file mode 100644
index 0000000..49ed0d4
--- /dev/null
+++ b/identity/aidl/default/IdentityCredential.h
@@ -0,0 +1,109 @@
+/*
+ * 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.
+ */
+
+#ifndef ANDROID_HARDWARE_IDENTITY_IDENTITYCREDENTIAL_H
+#define ANDROID_HARDWARE_IDENTITY_IDENTITYCREDENTIAL_H
+
+#include <aidl/android/hardware/identity/BnIdentityCredential.h>
+#include <aidl/android/hardware/keymaster/HardwareAuthToken.h>
+#include <android/hardware/identity/support/IdentityCredentialSupport.h>
+
+#include <map>
+#include <set>
+#include <string>
+#include <vector>
+
+#include <cppbor/cppbor.h>
+
+namespace aidl::android::hardware::identity {
+
+using ::aidl::android::hardware::keymaster::HardwareAuthToken;
+using ::std::map;
+using ::std::string;
+using ::std::vector;
+
+using MapStringToVectorOfStrings = map<string, vector<string>>;
+
+class IdentityCredential : public BnIdentityCredential {
+ public:
+ IdentityCredential(const vector<uint8_t>& credentialData)
+ : credentialData_(credentialData), numStartRetrievalCalls_(0), authChallenge_(0) {}
+
+ // Parses and decrypts credentialData_, return a status code from
+ // IIdentityCredentialStore. Must be called right after construction.
+ int initialize();
+
+ // Methods from IIdentityCredential follow.
+ ndk::ScopedAStatus deleteCredential(vector<int8_t>* outProofOfDeletionSignature) override;
+ ndk::ScopedAStatus createEphemeralKeyPair(vector<int8_t>* outKeyPair) override;
+ ndk::ScopedAStatus setReaderEphemeralPublicKey(const vector<int8_t>& publicKey) override;
+ ndk::ScopedAStatus createAuthChallenge(int64_t* outChallenge) override;
+ ndk::ScopedAStatus startRetrieval(
+ const vector<SecureAccessControlProfile>& accessControlProfiles,
+ const HardwareAuthToken& authToken, const vector<int8_t>& itemsRequest,
+ const vector<int8_t>& sessionTranscript, const vector<int8_t>& readerSignature,
+ const vector<int32_t>& requestCounts) override;
+ ndk::ScopedAStatus startRetrieveEntryValue(
+ const string& nameSpace, const string& name, int32_t entrySize,
+ const vector<int32_t>& accessControlProfileIds) override;
+ ndk::ScopedAStatus retrieveEntryValue(const vector<int8_t>& encryptedContent,
+ vector<int8_t>* outContent) override;
+ ndk::ScopedAStatus finishRetrieval(const vector<int8_t>& signingKeyBlob, vector<int8_t>* outMac,
+ vector<int8_t>* outDeviceNameSpaces) override;
+ ndk::ScopedAStatus generateSigningKeyPair(vector<int8_t>* outSigningKeyBlob,
+ Certificate* outSigningKeyCertificate) override;
+
+ private:
+ // Set by constructor
+ vector<uint8_t> credentialData_;
+ int numStartRetrievalCalls_;
+
+ // Set by initialize()
+ string docType_;
+ bool testCredential_;
+ vector<uint8_t> storageKey_;
+ vector<uint8_t> credentialPrivKey_;
+
+ // Set by createEphemeralKeyPair()
+ vector<uint8_t> ephemeralPublicKey_;
+
+ // Set by setReaderEphemeralPublicKey()
+ vector<uint8_t> readerPublicKey_;
+
+ // Set by createAuthChallenge()
+ uint64_t authChallenge_;
+
+ // Set at startRetrieval() time.
+ map<int32_t, int> profileIdToAccessCheckResult_;
+ vector<uint8_t> sessionTranscript_;
+ std::unique_ptr<cppbor::Item> sessionTranscriptItem_;
+ vector<uint8_t> itemsRequest_;
+ vector<int32_t> requestCountsRemaining_;
+ MapStringToVectorOfStrings requestedNameSpacesAndNames_;
+ cppbor::Map deviceNameSpacesMap_;
+ cppbor::Map currentNameSpaceDeviceNameSpacesMap_;
+
+ // Set at startRetrieveEntryValue() time.
+ string currentNameSpace_;
+ string currentName_;
+ size_t entryRemainingBytes_;
+ vector<uint8_t> entryValue_;
+ vector<uint8_t> entryAdditionalData_;
+};
+
+} // namespace aidl::android::hardware::identity
+
+#endif // ANDROID_HARDWARE_IDENTITY_IDENTITYCREDENTIAL_H
diff --git a/identity/aidl/default/IdentityCredentialStore.cpp b/identity/aidl/default/IdentityCredentialStore.cpp
new file mode 100644
index 0000000..1efb4b4
--- /dev/null
+++ b/identity/aidl/default/IdentityCredentialStore.cpp
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "IdentityCredentialStore"
+
+#include <android-base/logging.h>
+
+#include "IdentityCredential.h"
+#include "IdentityCredentialStore.h"
+#include "WritableIdentityCredential.h"
+
+namespace aidl::android::hardware::identity {
+
+ndk::ScopedAStatus IdentityCredentialStore::getHardwareInformation(
+ HardwareInformation* hardwareInformation) {
+ HardwareInformation hw;
+ hw.credentialStoreName = "Identity Credential Reference Implementation";
+ hw.credentialStoreAuthorName = "Google";
+ hw.dataChunkSize = kGcmChunkSize;
+ hw.isDirectAccess = false;
+ hw.supportedDocTypes = {};
+ *hardwareInformation = hw;
+ return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus IdentityCredentialStore::createCredential(
+ const string& docType, bool testCredential,
+ shared_ptr<IWritableIdentityCredential>* outWritableCredential) {
+ shared_ptr<WritableIdentityCredential> wc =
+ ndk::SharedRefBase::make<WritableIdentityCredential>(docType, testCredential);
+ if (!wc->initialize()) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED,
+ "Error initializing WritableIdentityCredential"));
+ }
+ *outWritableCredential = wc;
+ return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus IdentityCredentialStore::getCredential(
+ CipherSuite cipherSuite, const vector<int8_t>& credentialData,
+ shared_ptr<IIdentityCredential>* outCredential) {
+ // We only support CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256 right now.
+ if (cipherSuite != CipherSuite::CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_CIPHER_SUITE_NOT_SUPPORTED,
+ "Unsupported cipher suite"));
+ }
+
+ vector<uint8_t> data = vector<uint8_t>(credentialData.begin(), credentialData.end());
+ shared_ptr<IdentityCredential> credential = ndk::SharedRefBase::make<IdentityCredential>(data);
+ auto ret = credential->initialize();
+ if (ret != IIdentityCredentialStore::STATUS_OK) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ int(ret), "Error initializing IdentityCredential"));
+ }
+ *outCredential = credential;
+ return ndk::ScopedAStatus::ok();
+}
+
+} // namespace aidl::android::hardware::identity
diff --git a/identity/aidl/default/IdentityCredentialStore.h b/identity/aidl/default/IdentityCredentialStore.h
new file mode 100644
index 0000000..a205113
--- /dev/null
+++ b/identity/aidl/default/IdentityCredentialStore.h
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+#ifndef ANDROID_HARDWARE_IDENTITY_IDENTITYCREDENTIALSTORE_H
+#define ANDROID_HARDWARE_IDENTITY_IDENTITYCREDENTIALSTORE_H
+
+#include <aidl/android/hardware/identity/BnIdentityCredentialStore.h>
+
+namespace aidl::android::hardware::identity {
+
+using ::std::shared_ptr;
+using ::std::string;
+using ::std::vector;
+
+class IdentityCredentialStore : public BnIdentityCredentialStore {
+ public:
+ IdentityCredentialStore() {}
+
+ // The GCM chunk size used by this implementation is 64 KiB.
+ static constexpr size_t kGcmChunkSize = 64 * 1024;
+
+ // Methods from IIdentityCredentialStore follow.
+ ndk::ScopedAStatus getHardwareInformation(HardwareInformation* hardwareInformation) override;
+
+ ndk::ScopedAStatus createCredential(
+ const string& docType, bool testCredential,
+ shared_ptr<IWritableIdentityCredential>* outWritableCredential) override;
+
+ ndk::ScopedAStatus getCredential(CipherSuite cipherSuite, const vector<int8_t>& credentialData,
+ shared_ptr<IIdentityCredential>* outCredential) override;
+};
+
+} // namespace aidl::android::hardware::identity
+
+#endif // ANDROID_HARDWARE_IDENTITY_IDENTITYCREDENTIALSTORE_H
diff --git a/identity/aidl/default/Util.cpp b/identity/aidl/default/Util.cpp
new file mode 100644
index 0000000..a0f86be
--- /dev/null
+++ b/identity/aidl/default/Util.cpp
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "Util"
+
+#include "Util.h"
+
+#include <android/hardware/identity/support/IdentityCredentialSupport.h>
+
+#include <string.h>
+
+#include <android-base/logging.h>
+
+#include <cppbor.h>
+#include <cppbor_parse.h>
+
+namespace aidl::android::hardware::identity {
+
+using namespace ::android::hardware::identity;
+
+// This is not a very random HBK but that's OK because this is the SW
+// implementation where it can't be kept secret.
+vector<uint8_t> hardwareBoundKey = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
+
+const vector<uint8_t>& getHardwareBoundKey() {
+ return hardwareBoundKey;
+}
+
+vector<uint8_t> byteStringToUnsigned(const vector<int8_t>& value) {
+ return vector<uint8_t>(value.begin(), value.end());
+}
+
+vector<int8_t> byteStringToSigned(const vector<uint8_t>& value) {
+ return vector<int8_t>(value.begin(), value.end());
+}
+
+vector<uint8_t> secureAccessControlProfileEncodeCbor(const SecureAccessControlProfile& profile) {
+ cppbor::Map map;
+ map.add("id", profile.id);
+
+ if (profile.readerCertificate.encodedCertificate.size() > 0) {
+ map.add("readerCertificate",
+ cppbor::Bstr(byteStringToUnsigned(profile.readerCertificate.encodedCertificate)));
+ }
+
+ if (profile.userAuthenticationRequired) {
+ map.add("userAuthenticationRequired", profile.userAuthenticationRequired);
+ map.add("timeoutMillis", profile.timeoutMillis);
+ map.add("secureUserId", profile.secureUserId);
+ }
+
+ return map.encode();
+}
+
+optional<vector<uint8_t>> secureAccessControlProfileCalcMac(
+ const SecureAccessControlProfile& profile, const vector<uint8_t>& storageKey) {
+ vector<uint8_t> cborData = secureAccessControlProfileEncodeCbor(profile);
+
+ optional<vector<uint8_t>> nonce = support::getRandom(12);
+ if (!nonce) {
+ return {};
+ }
+ optional<vector<uint8_t>> macO =
+ support::encryptAes128Gcm(storageKey, nonce.value(), {}, cborData);
+ if (!macO) {
+ return {};
+ }
+ return macO.value();
+}
+
+bool secureAccessControlProfileCheckMac(const SecureAccessControlProfile& profile,
+ const vector<uint8_t>& storageKey) {
+ vector<uint8_t> cborData = secureAccessControlProfileEncodeCbor(profile);
+
+ if (profile.mac.size() < support::kAesGcmIvSize) {
+ return false;
+ }
+ vector<uint8_t> nonce =
+ vector<uint8_t>(profile.mac.begin(), profile.mac.begin() + support::kAesGcmIvSize);
+ optional<vector<uint8_t>> mac = support::encryptAes128Gcm(storageKey, nonce, {}, cborData);
+ if (!mac) {
+ return false;
+ }
+ if (mac.value() != byteStringToUnsigned(profile.mac)) {
+ return false;
+ }
+ return true;
+}
+
+vector<uint8_t> entryCreateAdditionalData(const string& nameSpace, const string& name,
+ const vector<int32_t> accessControlProfileIds) {
+ cppbor::Map map;
+ map.add("Namespace", nameSpace);
+ map.add("Name", name);
+
+ cppbor::Array acpIds;
+ for (auto id : accessControlProfileIds) {
+ acpIds.add(id);
+ }
+ map.add("AccessControlProfileIds", std::move(acpIds));
+ return map.encode();
+}
+
+} // namespace aidl::android::hardware::identity
diff --git a/identity/aidl/default/Util.h b/identity/aidl/default/Util.h
new file mode 100644
index 0000000..ee41ad1
--- /dev/null
+++ b/identity/aidl/default/Util.h
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+#ifndef ANDROID_HARDWARE_IDENTITY_UTIL_H
+#define ANDROID_HARDWARE_IDENTITY_UTIL_H
+
+#include <aidl/android/hardware/identity/BnIdentityCredential.h>
+#include <android/hardware/identity/support/IdentityCredentialSupport.h>
+
+#include <map>
+#include <optional>
+#include <set>
+#include <string>
+#include <vector>
+
+#include <cppbor/cppbor.h>
+
+namespace aidl::android::hardware::identity {
+
+using ::std::optional;
+using ::std::string;
+using ::std::vector;
+
+// Returns the hardware-bound AES-128 key.
+const vector<uint8_t>& getHardwareBoundKey();
+
+// Calculates the MAC for |profile| using |storageKey|.
+optional<vector<uint8_t>> secureAccessControlProfileCalcMac(
+ const SecureAccessControlProfile& profile, const vector<uint8_t>& storageKey);
+
+// Checks authenticity of the MAC in |profile| using |storageKey|.
+bool secureAccessControlProfileCheckMac(const SecureAccessControlProfile& profile,
+ const vector<uint8_t>& storageKey);
+
+// Creates the AdditionalData CBOR used in the addEntryValue() HIDL method.
+vector<uint8_t> entryCreateAdditionalData(const string& nameSpace, const string& name,
+ const vector<int32_t> accessControlProfileIds);
+
+vector<uint8_t> byteStringToUnsigned(const vector<int8_t>& value);
+
+vector<int8_t> byteStringToSigned(const vector<uint8_t>& value);
+
+} // namespace aidl::android::hardware::identity
+
+#endif // ANDROID_HARDWARE_IDENTITY_UTIL_H
diff --git a/identity/aidl/default/WritableIdentityCredential.cpp b/identity/aidl/default/WritableIdentityCredential.cpp
new file mode 100644
index 0000000..ba2062d
--- /dev/null
+++ b/identity/aidl/default/WritableIdentityCredential.cpp
@@ -0,0 +1,421 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "WritableIdentityCredential"
+
+#include <android/hardware/identity/support/IdentityCredentialSupport.h>
+
+#include <android-base/logging.h>
+
+#include <cppbor/cppbor.h>
+#include <cppbor/cppbor_parse.h>
+
+#include "IdentityCredentialStore.h"
+#include "Util.h"
+#include "WritableIdentityCredential.h"
+
+namespace aidl::android::hardware::identity {
+
+using ::std::optional;
+using namespace ::android::hardware::identity;
+
+bool WritableIdentityCredential::initialize() {
+ optional<vector<uint8_t>> keyPair = support::createEcKeyPair();
+ if (!keyPair) {
+ LOG(ERROR) << "Error creating credentialKey";
+ return false;
+ }
+
+ optional<vector<uint8_t>> pubKey = support::ecKeyPairGetPublicKey(keyPair.value());
+ if (!pubKey) {
+ LOG(ERROR) << "Error getting public part of credentialKey";
+ return false;
+ }
+ credentialPubKey_ = pubKey.value();
+
+ optional<vector<uint8_t>> privKey = support::ecKeyPairGetPrivateKey(keyPair.value());
+ if (!privKey) {
+ LOG(ERROR) << "Error getting private part of credentialKey";
+ return false;
+ }
+ credentialPrivKey_ = privKey.value();
+
+ optional<vector<uint8_t>> random = support::getRandom(16);
+ if (!random) {
+ LOG(ERROR) << "Error creating storageKey";
+ return false;
+ }
+ storageKey_ = random.value();
+
+ return true;
+}
+
+// TODO: use |attestationApplicationId| and |attestationChallenge| and also
+// ensure the returned certificate chain satisfy the requirements listed in
+// the docs for IWritableIdentityCredential::getAttestationCertificate()
+//
+ndk::ScopedAStatus WritableIdentityCredential::getAttestationCertificate(
+ const vector<int8_t>& /*attestationApplicationId*/,
+ const vector<int8_t>& /*attestationChallenge*/, vector<Certificate>* outCertificateChain) {
+ // For now, we dynamically generate an attestion key on each and every
+ // request and use that to sign CredentialKey. In a real implementation this
+ // would look very differently.
+ optional<vector<uint8_t>> attestationKeyPair = support::createEcKeyPair();
+ if (!attestationKeyPair) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED, "Error creating attestationKey"));
+ }
+
+ optional<vector<uint8_t>> attestationPubKey =
+ support::ecKeyPairGetPublicKey(attestationKeyPair.value());
+ if (!attestationPubKey) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED,
+ "Error getting public part of attestationKey"));
+ }
+
+ optional<vector<uint8_t>> attestationPrivKey =
+ support::ecKeyPairGetPrivateKey(attestationKeyPair.value());
+ if (!attestationPrivKey) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED,
+ "Error getting private part of attestationKey"));
+ }
+
+ string serialDecimal;
+ string issuer;
+ string subject;
+ time_t validityNotBefore = time(nullptr);
+ time_t validityNotAfter = validityNotBefore + 365 * 24 * 3600;
+
+ // First create a certificate for |credentialPubKey| which is signed by
+ // |attestationPrivKey|.
+ //
+ serialDecimal = "0"; // TODO: set serial to |attestationChallenge|
+ issuer = "Android Open Source Project";
+ subject = "Android IdentityCredential CredentialKey";
+ optional<vector<uint8_t>> credentialPubKeyCertificate = support::ecPublicKeyGenerateCertificate(
+ credentialPubKey_, attestationPrivKey.value(), serialDecimal, issuer, subject,
+ validityNotBefore, validityNotAfter);
+ if (!credentialPubKeyCertificate) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED,
+ "Error creating certificate for credentialPubKey"));
+ }
+
+ // This is followed by a certificate for |attestationPubKey| self-signed by
+ // |attestationPrivKey|.
+ serialDecimal = "0"; // TODO: set serial
+ issuer = "Android Open Source Project";
+ subject = "Android IdentityCredential AttestationKey";
+ optional<vector<uint8_t>> attestationKeyCertificate = support::ecPublicKeyGenerateCertificate(
+ attestationPubKey.value(), attestationPrivKey.value(), serialDecimal, issuer, subject,
+ validityNotBefore, validityNotAfter);
+ if (!attestationKeyCertificate) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED,
+ "Error creating certificate for attestationPubKey"));
+ }
+
+ // Concatenate the certificates to form the chain.
+ vector<uint8_t> certificateChain;
+ certificateChain.insert(certificateChain.end(), credentialPubKeyCertificate.value().begin(),
+ credentialPubKeyCertificate.value().end());
+ certificateChain.insert(certificateChain.end(), attestationKeyCertificate.value().begin(),
+ attestationKeyCertificate.value().end());
+
+ optional<vector<vector<uint8_t>>> splitCertChain =
+ support::certificateChainSplit(certificateChain);
+ if (!splitCertChain) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED, "Error splitting certificate chain"));
+ }
+ *outCertificateChain = vector<Certificate>();
+ for (const vector<uint8_t>& cert : splitCertChain.value()) {
+ Certificate c = Certificate();
+ c.encodedCertificate = byteStringToSigned(cert);
+ outCertificateChain->push_back(std::move(c));
+ }
+ return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus WritableIdentityCredential::startPersonalization(
+ int32_t accessControlProfileCount, const vector<int32_t>& entryCounts) {
+ numAccessControlProfileRemaining_ = accessControlProfileCount;
+ remainingEntryCounts_ = entryCounts;
+ entryNameSpace_ = "";
+
+ signedDataAccessControlProfiles_ = cppbor::Array();
+ signedDataNamespaces_ = cppbor::Map();
+ signedDataCurrentNamespace_ = cppbor::Array();
+
+ return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus WritableIdentityCredential::addAccessControlProfile(
+ int32_t id, const Certificate& readerCertificate, bool userAuthenticationRequired,
+ int64_t timeoutMillis, int64_t secureUserId,
+ SecureAccessControlProfile* outSecureAccessControlProfile) {
+ SecureAccessControlProfile profile;
+
+ if (numAccessControlProfileRemaining_ == 0) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA,
+ "numAccessControlProfileRemaining_ is 0 and expected non-zero"));
+ }
+
+ // Spec requires if |userAuthenticationRequired| is false, then |timeoutMillis| must also
+ // be zero.
+ if (!userAuthenticationRequired && timeoutMillis != 0) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA,
+ "userAuthenticationRequired is false but timeout is non-zero"));
+ }
+
+ profile.id = id;
+ profile.readerCertificate = readerCertificate;
+ profile.userAuthenticationRequired = userAuthenticationRequired;
+ profile.timeoutMillis = timeoutMillis;
+ profile.secureUserId = secureUserId;
+ optional<vector<uint8_t>> mac = secureAccessControlProfileCalcMac(profile, storageKey_);
+ if (!mac) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED, "Error calculating MAC for profile"));
+ }
+ profile.mac = byteStringToSigned(mac.value());
+
+ cppbor::Map profileMap;
+ profileMap.add("id", profile.id);
+ if (profile.readerCertificate.encodedCertificate.size() > 0) {
+ profileMap.add(
+ "readerCertificate",
+ cppbor::Bstr(byteStringToUnsigned(profile.readerCertificate.encodedCertificate)));
+ }
+ if (profile.userAuthenticationRequired) {
+ profileMap.add("userAuthenticationRequired", profile.userAuthenticationRequired);
+ profileMap.add("timeoutMillis", profile.timeoutMillis);
+ }
+ signedDataAccessControlProfiles_.add(std::move(profileMap));
+
+ numAccessControlProfileRemaining_--;
+
+ *outSecureAccessControlProfile = profile;
+ return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus WritableIdentityCredential::beginAddEntry(
+ const vector<int32_t>& accessControlProfileIds, const string& nameSpace, const string& name,
+ int32_t entrySize) {
+ if (numAccessControlProfileRemaining_ != 0) {
+ LOG(ERROR) << "numAccessControlProfileRemaining_ is " << numAccessControlProfileRemaining_
+ << " and expected zero";
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA,
+ "numAccessControlProfileRemaining_ is not zero"));
+ }
+
+ if (remainingEntryCounts_.size() == 0) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA, "No more namespaces to add to"));
+ }
+
+ // Handle initial beginEntry() call.
+ if (entryNameSpace_ == "") {
+ entryNameSpace_ = nameSpace;
+ }
+
+ // If the namespace changed...
+ if (nameSpace != entryNameSpace_) {
+ // Then check that all entries in the previous namespace have been added..
+ if (remainingEntryCounts_[0] != 0) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA,
+ "New namespace but a non-zero number of entries remain to be added"));
+ }
+ remainingEntryCounts_.erase(remainingEntryCounts_.begin());
+
+ if (signedDataCurrentNamespace_.size() > 0) {
+ signedDataNamespaces_.add(entryNameSpace_, std::move(signedDataCurrentNamespace_));
+ signedDataCurrentNamespace_ = cppbor::Array();
+ }
+ } else {
+ // Same namespace...
+ if (remainingEntryCounts_[0] == 0) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA,
+ "Same namespace but no entries remain to be added"));
+ }
+ remainingEntryCounts_[0] -= 1;
+ }
+
+ entryAdditionalData_ = entryCreateAdditionalData(nameSpace, name, accessControlProfileIds);
+
+ entryRemainingBytes_ = entrySize;
+ entryNameSpace_ = nameSpace;
+ entryName_ = name;
+ entryAccessControlProfileIds_ = accessControlProfileIds;
+ entryBytes_.resize(0);
+ // LOG(INFO) << "name=" << name << " entrySize=" << entrySize;
+ return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus WritableIdentityCredential::addEntryValue(const vector<int8_t>& contentS,
+ vector<int8_t>* outEncryptedContent) {
+ auto content = byteStringToUnsigned(contentS);
+ size_t contentSize = content.size();
+
+ if (contentSize > IdentityCredentialStore::kGcmChunkSize) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA,
+ "Passed in chunk of is bigger than kGcmChunkSize"));
+ }
+ if (contentSize > entryRemainingBytes_) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA,
+ "Passed in chunk is bigger than remaining space"));
+ }
+
+ entryBytes_.insert(entryBytes_.end(), content.begin(), content.end());
+ entryRemainingBytes_ -= contentSize;
+ if (entryRemainingBytes_ > 0) {
+ if (contentSize != IdentityCredentialStore::kGcmChunkSize) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA,
+ "Retrieved non-final chunk which isn't kGcmChunkSize"));
+ }
+ }
+
+ optional<vector<uint8_t>> nonce = support::getRandom(12);
+ if (!nonce) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED, "Error getting nonce"));
+ }
+ optional<vector<uint8_t>> encryptedContent =
+ support::encryptAes128Gcm(storageKey_, nonce.value(), content, entryAdditionalData_);
+ if (!encryptedContent) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED, "Error encrypting content"));
+ }
+
+ if (entryRemainingBytes_ == 0) {
+ // TODO: ideally do do this without parsing the data (but still validate data is valid
+ // CBOR).
+ auto [item, _, message] = cppbor::parse(entryBytes_);
+ if (item == nullptr) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_INVALID_DATA, "Data is not valid CBOR"));
+ }
+ cppbor::Map entryMap;
+ entryMap.add("name", entryName_);
+ entryMap.add("value", std::move(item));
+ cppbor::Array profileIdArray;
+ for (auto id : entryAccessControlProfileIds_) {
+ profileIdArray.add(id);
+ }
+ entryMap.add("accessControlProfiles", std::move(profileIdArray));
+ signedDataCurrentNamespace_.add(std::move(entryMap));
+ }
+
+ *outEncryptedContent = byteStringToSigned(encryptedContent.value());
+ return ndk::ScopedAStatus::ok();
+}
+
+// Writes CBOR-encoded structure to |credentialKeys| containing |storageKey| and
+// |credentialPrivKey|.
+static bool generateCredentialKeys(const vector<uint8_t>& storageKey,
+ const vector<uint8_t>& credentialPrivKey,
+ vector<uint8_t>& credentialKeys) {
+ if (storageKey.size() != 16) {
+ LOG(ERROR) << "Size of storageKey is not 16";
+ return false;
+ }
+
+ cppbor::Array array;
+ array.add(cppbor::Bstr(storageKey));
+ array.add(cppbor::Bstr(credentialPrivKey));
+ credentialKeys = array.encode();
+ return true;
+}
+
+// Writes CBOR-encoded structure to |credentialData| containing |docType|,
+// |testCredential| and |credentialKeys|. The latter element will be stored in
+// encrypted form, using |hardwareBoundKey| as the encryption key.
+bool generateCredentialData(const vector<uint8_t>& hardwareBoundKey, const string& docType,
+ bool testCredential, const vector<uint8_t>& credentialKeys,
+ vector<uint8_t>& credentialData) {
+ optional<vector<uint8_t>> nonce = support::getRandom(12);
+ if (!nonce) {
+ LOG(ERROR) << "Error getting random";
+ return false;
+ }
+ vector<uint8_t> docTypeAsVec(docType.begin(), docType.end());
+ optional<vector<uint8_t>> credentialBlob = support::encryptAes128Gcm(
+ hardwareBoundKey, nonce.value(), credentialKeys, docTypeAsVec);
+ if (!credentialBlob) {
+ LOG(ERROR) << "Error encrypting CredentialKeys blob";
+ return false;
+ }
+
+ cppbor::Array array;
+ array.add(docType);
+ array.add(testCredential);
+ array.add(cppbor::Bstr(credentialBlob.value()));
+ credentialData = array.encode();
+ return true;
+}
+
+ndk::ScopedAStatus WritableIdentityCredential::finishAddingEntries(
+ vector<int8_t>* outCredentialData, vector<int8_t>* outProofOfProvisioningSignature) {
+ if (signedDataCurrentNamespace_.size() > 0) {
+ signedDataNamespaces_.add(entryNameSpace_, std::move(signedDataCurrentNamespace_));
+ }
+ cppbor::Array popArray;
+ popArray.add("ProofOfProvisioning")
+ .add(docType_)
+ .add(std::move(signedDataAccessControlProfiles_))
+ .add(std::move(signedDataNamespaces_))
+ .add(testCredential_);
+ vector<uint8_t> encodedCbor = popArray.encode();
+
+ optional<vector<uint8_t>> signature = support::coseSignEcDsa(credentialPrivKey_,
+ encodedCbor, // payload
+ {}, // additionalData
+ {}); // certificateChain
+ if (!signature) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED, "Error signing data"));
+ }
+
+ vector<uint8_t> credentialKeys;
+ if (!generateCredentialKeys(storageKey_, credentialPrivKey_, credentialKeys)) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED, "Error generating CredentialKeys"));
+ }
+
+ vector<uint8_t> credentialData;
+ if (!generateCredentialData(
+ testCredential_ ? support::getTestHardwareBoundKey() : getHardwareBoundKey(),
+ docType_, testCredential_, credentialKeys, credentialData)) {
+ return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
+ IIdentityCredentialStore::STATUS_FAILED, "Error generating CredentialData"));
+ }
+
+ *outCredentialData = byteStringToSigned(credentialData);
+ *outProofOfProvisioningSignature = byteStringToSigned(signature.value());
+ return ndk::ScopedAStatus::ok();
+}
+
+} // namespace aidl::android::hardware::identity
diff --git a/identity/aidl/default/WritableIdentityCredential.h b/identity/aidl/default/WritableIdentityCredential.h
new file mode 100644
index 0000000..b380f89
--- /dev/null
+++ b/identity/aidl/default/WritableIdentityCredential.h
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+#ifndef ANDROID_HARDWARE_IDENTITY_WRITABLEIDENTITYCREDENTIAL_H
+#define ANDROID_HARDWARE_IDENTITY_WRITABLEIDENTITYCREDENTIAL_H
+
+#include <aidl/android/hardware/identity/BnWritableIdentityCredential.h>
+#include <android/hardware/identity/support/IdentityCredentialSupport.h>
+
+#include <cppbor.h>
+
+namespace aidl::android::hardware::identity {
+
+using ::std::string;
+using ::std::vector;
+
+class WritableIdentityCredential : public BnWritableIdentityCredential {
+ public:
+ WritableIdentityCredential(const string& docType, bool testCredential)
+ : docType_(docType), testCredential_(testCredential) {}
+
+ // Creates the Credential Key. Returns false on failure. Must be called
+ // right after construction.
+ bool initialize();
+
+ // Methods from IWritableIdentityCredential follow.
+ ndk::ScopedAStatus getAttestationCertificate(const vector<int8_t>& attestationApplicationId,
+ const vector<int8_t>& attestationChallenge,
+ vector<Certificate>* outCertificateChain) override;
+
+ ndk::ScopedAStatus startPersonalization(int32_t accessControlProfileCount,
+ const vector<int32_t>& entryCounts) override;
+
+ ndk::ScopedAStatus addAccessControlProfile(
+ int32_t id, const Certificate& readerCertificate, bool userAuthenticationRequired,
+ int64_t timeoutMillis, int64_t secureUserId,
+ SecureAccessControlProfile* outSecureAccessControlProfile) override;
+
+ ndk::ScopedAStatus beginAddEntry(const vector<int32_t>& accessControlProfileIds,
+ const string& nameSpace, const string& name,
+ int32_t entrySize) override;
+
+ ndk::ScopedAStatus addEntryValue(const vector<int8_t>& content,
+ vector<int8_t>* outEncryptedContent) override;
+
+ ndk::ScopedAStatus finishAddingEntries(
+ vector<int8_t>* outCredentialData,
+ vector<int8_t>* outProofOfProvisioningSignature) override;
+
+ // private:
+ string docType_;
+ bool testCredential_;
+
+ // These are set in initialize().
+ vector<uint8_t> storageKey_;
+ vector<uint8_t> credentialPrivKey_;
+ vector<uint8_t> credentialPubKey_;
+
+ // These fields are initialized during startPersonalization()
+ size_t numAccessControlProfileRemaining_;
+ vector<int32_t> remainingEntryCounts_;
+ cppbor::Array signedDataAccessControlProfiles_;
+ cppbor::Map signedDataNamespaces_;
+ cppbor::Array signedDataCurrentNamespace_;
+
+ // These fields are initialized during beginAddEntry()
+ size_t entryRemainingBytes_;
+ vector<uint8_t> entryAdditionalData_;
+ string entryNameSpace_;
+ string entryName_;
+ vector<int32_t> entryAccessControlProfileIds_;
+ vector<uint8_t> entryBytes_;
+};
+
+} // namespace aidl::android::hardware::identity
+
+#endif // ANDROID_HARDWARE_IDENTITY_WRITABLEIDENTITYCREDENTIAL_H
diff --git a/identity/aidl/default/identity-default.rc b/identity/aidl/default/identity-default.rc
new file mode 100644
index 0000000..d3b62c1
--- /dev/null
+++ b/identity/aidl/default/identity-default.rc
@@ -0,0 +1,3 @@
+service vendor.identity-default /vendor/bin/hw/android.hardware.identity-service.example
+ class hal
+ user nobody
diff --git a/identity/aidl/default/identity-default.xml b/identity/aidl/default/identity-default.xml
new file mode 100644
index 0000000..a47d354
--- /dev/null
+++ b/identity/aidl/default/identity-default.xml
@@ -0,0 +1,9 @@
+<manifest version="1.0" type="device">
+ <hal format="aidl">
+ <name>android.hardware.identity</name>
+ <interface>
+ <name>IIdentityCredentialStore</name>
+ <instance>default</instance>
+ </interface>
+ </hal>
+</manifest>
diff --git a/identity/aidl/default/service.cpp b/identity/aidl/default/service.cpp
new file mode 100644
index 0000000..f05c615
--- /dev/null
+++ b/identity/aidl/default/service.cpp
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "android.hardware.identity-service"
+
+#include <android-base/logging.h>
+#include <android/binder_manager.h>
+#include <android/binder_process.h>
+
+#include "IdentityCredentialStore.h"
+
+using aidl::android::hardware::identity::IdentityCredentialStore;
+
+int main() {
+ ABinderProcess_setThreadPoolMaxThreadCount(0);
+ std::shared_ptr<IdentityCredentialStore> store =
+ ndk::SharedRefBase::make<IdentityCredentialStore>();
+
+ const std::string instance = std::string() + IdentityCredentialStore::descriptor + "/default";
+ LOG(INFO) << "instance: " << instance;
+ binder_status_t status = AServiceManager_addService(store->asBinder().get(), instance.c_str());
+ CHECK(status == STATUS_OK);
+
+ ABinderProcess_joinThreadPool();
+ return EXIT_FAILURE; // should not reach
+}
diff --git a/identity/aidl/vts/Android.bp b/identity/aidl/vts/Android.bp
new file mode 100644
index 0000000..21ff440
--- /dev/null
+++ b/identity/aidl/vts/Android.bp
@@ -0,0 +1,21 @@
+cc_test {
+ name: "VtsHalIdentityTargetTest",
+ defaults: [
+ "VtsHalTargetTestDefaults",
+ "use_libaidlvintf_gtest_helper_static",
+ ],
+ srcs: ["VtsHalIdentityTargetTest.cpp"],
+ shared_libs: [
+ "libbinder",
+ "libcppbor",
+ "android.hardware.identity-support-lib",
+ ],
+ static_libs: [
+ "android.hardware.identity-cpp",
+ "android.hardware.keymaster-cpp",
+ ],
+ test_suites: [
+ "general-tests",
+ "vts-core",
+ ],
+}
diff --git a/identity/aidl/vts/VtsHalIdentityTargetTest.cpp b/identity/aidl/vts/VtsHalIdentityTargetTest.cpp
new file mode 100644
index 0000000..5abe5a2
--- /dev/null
+++ b/identity/aidl/vts/VtsHalIdentityTargetTest.cpp
@@ -0,0 +1,446 @@
+/*
+ * Copyright (C) 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.
+ */
+#define LOG_TAG "VtsHalIdentityTargetTest"
+
+#include <aidl/Gtest.h>
+#include <aidl/Vintf.h>
+#include <android-base/logging.h>
+#include <android/hardware/identity/IIdentityCredentialStore.h>
+#include <android/hardware/identity/support/IdentityCredentialSupport.h>
+#include <binder/IServiceManager.h>
+#include <binder/ProcessState.h>
+#include <cppbor.h>
+#include <cppbor_parse.h>
+#include <gtest/gtest.h>
+#include <future>
+#include <map>
+
+namespace android::hardware::identity {
+
+using std::map;
+using std::optional;
+using std::string;
+using std::vector;
+
+using ::android::sp;
+using ::android::String16;
+using ::android::binder::Status;
+
+using ::android::hardware::keymaster::HardwareAuthToken;
+
+// ---------------------------------------------------------------------------
+// Test Data.
+// ---------------------------------------------------------------------------
+
+struct TestEntryData {
+ TestEntryData(string nameSpace, string name, vector<int32_t> profileIds)
+ : nameSpace(nameSpace), name(name), profileIds(profileIds) {}
+
+ TestEntryData(string nameSpace, string name, const string& value, vector<int32_t> profileIds)
+ : TestEntryData(nameSpace, name, profileIds) {
+ valueCbor = cppbor::Tstr(((const char*)value.data())).encode();
+ }
+ TestEntryData(string nameSpace, string name, const vector<uint8_t>& value,
+ vector<int32_t> profileIds)
+ : TestEntryData(nameSpace, name, profileIds) {
+ valueCbor = cppbor::Bstr(value).encode();
+ }
+ TestEntryData(string nameSpace, string name, bool value, vector<int32_t> profileIds)
+ : TestEntryData(nameSpace, name, profileIds) {
+ valueCbor = cppbor::Bool(value).encode();
+ }
+ TestEntryData(string nameSpace, string name, int64_t value, vector<int32_t> profileIds)
+ : TestEntryData(nameSpace, name, profileIds) {
+ if (value >= 0) {
+ valueCbor = cppbor::Uint(value).encode();
+ } else {
+ valueCbor = cppbor::Nint(-value).encode();
+ }
+ }
+
+ string nameSpace;
+ string name;
+ vector<uint8_t> valueCbor;
+ vector<int32_t> profileIds;
+};
+
+struct TestProfile {
+ uint16_t id;
+ vector<uint8_t> readerCertificate;
+ bool userAuthenticationRequired;
+ uint64_t timeoutMillis;
+};
+
+// ----------------------------------------------------------------
+
+class IdentityAidl : public testing::TestWithParam<std::string> {
+ public:
+ virtual void SetUp() override {
+ credentialStore_ = android::waitForDeclaredService<IIdentityCredentialStore>(
+ String16(GetParam().c_str()));
+ ASSERT_NE(credentialStore_, nullptr);
+ }
+
+ sp<IIdentityCredentialStore> credentialStore_;
+};
+
+TEST_P(IdentityAidl, hardwareInformation) {
+ HardwareInformation info;
+ ASSERT_TRUE(credentialStore_->getHardwareInformation(&info).isOk());
+ ASSERT_GT(info.credentialStoreName.size(), 0);
+ ASSERT_GT(info.credentialStoreAuthorName.size(), 0);
+ ASSERT_GE(info.dataChunkSize, 256);
+}
+
+TEST_P(IdentityAidl, createAndRetrieveCredential) {
+ // First, generate a key-pair for the reader since its public key will be
+ // part of the request data.
+ optional<vector<uint8_t>> readerKeyPKCS8 = support::createEcKeyPair();
+ ASSERT_TRUE(readerKeyPKCS8);
+ optional<vector<uint8_t>> readerPublicKey =
+ support::ecKeyPairGetPublicKey(readerKeyPKCS8.value());
+ optional<vector<uint8_t>> readerKey = support::ecKeyPairGetPrivateKey(readerKeyPKCS8.value());
+ string serialDecimal = "1234";
+ string issuer = "Android Open Source Project";
+ string subject = "Android IdentityCredential VTS Test";
+ time_t validityNotBefore = time(nullptr);
+ time_t validityNotAfter = validityNotBefore + 365 * 24 * 3600;
+ optional<vector<uint8_t>> readerCertificate = support::ecPublicKeyGenerateCertificate(
+ readerPublicKey.value(), readerKey.value(), serialDecimal, issuer, subject,
+ validityNotBefore, validityNotAfter);
+ ASSERT_TRUE(readerCertificate);
+
+ // Make the portrait image really big (just shy of 256 KiB) to ensure that
+ // the chunking code gets exercised.
+ vector<uint8_t> portraitImage;
+ portraitImage.resize(256 * 1024 - 10);
+ for (size_t n = 0; n < portraitImage.size(); n++) {
+ portraitImage[n] = (uint8_t)n;
+ }
+
+ // Access control profiles:
+ const vector<TestProfile> testProfiles = {// Profile 0 (reader authentication)
+ {0, readerCertificate.value(), false, 0},
+ // Profile 1 (no authentication)
+ {1, {}, false, 0}};
+
+ HardwareAuthToken authToken;
+
+ // Here's the actual test data:
+ const vector<TestEntryData> testEntries = {
+ {"PersonalData", "Last name", string("Turing"), vector<int32_t>{0, 1}},
+ {"PersonalData", "Birth date", string("19120623"), vector<int32_t>{0, 1}},
+ {"PersonalData", "First name", string("Alan"), vector<int32_t>{0, 1}},
+ {"PersonalData", "Home address", string("Maida Vale, London, England"),
+ vector<int32_t>{0}},
+ {"Image", "Portrait image", portraitImage, vector<int32_t>{0, 1}},
+ };
+ const vector<int32_t> testEntriesEntryCounts = {static_cast<int32_t>(testEntries.size() - 1),
+ 1u};
+ HardwareInformation hwInfo;
+ ASSERT_TRUE(credentialStore_->getHardwareInformation(&hwInfo).isOk());
+
+ string cborPretty;
+ sp<IWritableIdentityCredential> writableCredential;
+ string docType = "org.iso.18013-5.2019.mdl";
+ bool testCredential = true;
+ ASSERT_TRUE(credentialStore_->createCredential(docType, testCredential, &writableCredential)
+ .isOk());
+ ASSERT_NE(writableCredential, nullptr);
+
+ string challenge = "attestationChallenge";
+ // TODO: set it to something random and check it's in the cert chain
+ vector<uint8_t> attestationApplicationId = {};
+ vector<uint8_t> attestationChallenge(challenge.begin(), challenge.end());
+ vector<Certificate> attestationCertificates;
+ ASSERT_TRUE(writableCredential
+ ->getAttestationCertificate(attestationApplicationId, attestationChallenge,
+ &attestationCertificates)
+ .isOk());
+ ASSERT_GE(attestationCertificates.size(), 2);
+
+ ASSERT_TRUE(
+ writableCredential->startPersonalization(testProfiles.size(), testEntriesEntryCounts)
+ .isOk());
+
+ vector<SecureAccessControlProfile> returnedSecureProfiles;
+ for (const auto& testProfile : testProfiles) {
+ SecureAccessControlProfile profile;
+ Certificate cert;
+ cert.encodedCertificate = testProfile.readerCertificate;
+ ASSERT_TRUE(writableCredential
+ ->addAccessControlProfile(testProfile.id, cert,
+ testProfile.userAuthenticationRequired,
+ testProfile.timeoutMillis,
+ 0, // secureUserId
+ &profile)
+ .isOk());
+ ASSERT_EQ(testProfile.id, profile.id);
+ ASSERT_EQ(testProfile.readerCertificate, profile.readerCertificate.encodedCertificate);
+ ASSERT_EQ(testProfile.userAuthenticationRequired, profile.userAuthenticationRequired);
+ ASSERT_EQ(testProfile.timeoutMillis, profile.timeoutMillis);
+ ASSERT_EQ(support::kAesGcmTagSize + support::kAesGcmIvSize, profile.mac.size());
+ returnedSecureProfiles.push_back(profile);
+ }
+
+ // Uses TestEntryData* pointer as key and values are the encrypted blobs. This
+ // is a little hacky but it works well enough.
+ map<const TestEntryData*, vector<vector<uint8_t>>> encryptedBlobs;
+
+ for (const auto& entry : testEntries) {
+ vector<vector<uint8_t>> chunks =
+ support::chunkVector(entry.valueCbor, hwInfo.dataChunkSize);
+
+ ASSERT_TRUE(writableCredential
+ ->beginAddEntry(entry.profileIds, entry.nameSpace, entry.name,
+ entry.valueCbor.size())
+ .isOk());
+
+ vector<vector<uint8_t>> encryptedChunks;
+ for (const auto& chunk : chunks) {
+ vector<uint8_t> encryptedChunk;
+ ASSERT_TRUE(writableCredential->addEntryValue(chunk, &encryptedChunk).isOk());
+ encryptedChunks.push_back(encryptedChunk);
+ }
+ encryptedBlobs[&entry] = encryptedChunks;
+ }
+
+ vector<uint8_t> credentialData;
+ vector<uint8_t> proofOfProvisioningSignature;
+ ASSERT_TRUE(
+ writableCredential->finishAddingEntries(&credentialData, &proofOfProvisioningSignature)
+ .isOk());
+
+ optional<vector<uint8_t>> proofOfProvisioning =
+ support::coseSignGetPayload(proofOfProvisioningSignature);
+ ASSERT_TRUE(proofOfProvisioning);
+ cborPretty = support::cborPrettyPrint(proofOfProvisioning.value(), 32, {"readerCertificate"});
+ EXPECT_EQ(
+ "[\n"
+ " 'ProofOfProvisioning',\n"
+ " 'org.iso.18013-5.2019.mdl',\n"
+ " [\n"
+ " {\n"
+ " 'id' : 0,\n"
+ " 'readerCertificate' : <not printed>,\n"
+ " },\n"
+ " {\n"
+ " 'id' : 1,\n"
+ " },\n"
+ " ],\n"
+ " {\n"
+ " 'PersonalData' : [\n"
+ " {\n"
+ " 'name' : 'Last name',\n"
+ " 'value' : 'Turing',\n"
+ " 'accessControlProfiles' : [0, 1, ],\n"
+ " },\n"
+ " {\n"
+ " 'name' : 'Birth date',\n"
+ " 'value' : '19120623',\n"
+ " 'accessControlProfiles' : [0, 1, ],\n"
+ " },\n"
+ " {\n"
+ " 'name' : 'First name',\n"
+ " 'value' : 'Alan',\n"
+ " 'accessControlProfiles' : [0, 1, ],\n"
+ " },\n"
+ " {\n"
+ " 'name' : 'Home address',\n"
+ " 'value' : 'Maida Vale, London, England',\n"
+ " 'accessControlProfiles' : [0, ],\n"
+ " },\n"
+ " ],\n"
+ " 'Image' : [\n"
+ " {\n"
+ " 'name' : 'Portrait image',\n"
+ " 'value' : <bstr size=262134 sha1=941e372f654d86c32d88fae9e41b706afbfd02bb>,\n"
+ " 'accessControlProfiles' : [0, 1, ],\n"
+ " },\n"
+ " ],\n"
+ " },\n"
+ " true,\n"
+ "]",
+ cborPretty);
+
+ optional<vector<uint8_t>> credentialPubKey =
+ support::certificateChainGetTopMostKey(attestationCertificates[0].encodedCertificate);
+ ASSERT_TRUE(credentialPubKey);
+ EXPECT_TRUE(support::coseCheckEcDsaSignature(proofOfProvisioningSignature,
+ {}, // Additional data
+ credentialPubKey.value()));
+ writableCredential = nullptr;
+
+ // Now that the credential has been provisioned, read it back and check the
+ // correct data is returned.
+ sp<IIdentityCredential> credential;
+ ASSERT_TRUE(credentialStore_
+ ->getCredential(
+ CipherSuite::CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256,
+ credentialData, &credential)
+ .isOk());
+ ASSERT_NE(credential, nullptr);
+
+ optional<vector<uint8_t>> readerEphemeralKeyPair = support::createEcKeyPair();
+ ASSERT_TRUE(readerEphemeralKeyPair);
+ optional<vector<uint8_t>> readerEphemeralPublicKey =
+ support::ecKeyPairGetPublicKey(readerEphemeralKeyPair.value());
+ ASSERT_TRUE(credential->setReaderEphemeralPublicKey(readerEphemeralPublicKey.value()).isOk());
+
+ vector<uint8_t> ephemeralKeyPair;
+ ASSERT_TRUE(credential->createEphemeralKeyPair(&ephemeralKeyPair).isOk());
+ optional<vector<uint8_t>> ephemeralPublicKey = support::ecKeyPairGetPublicKey(ephemeralKeyPair);
+
+ // Calculate requestData field and sign it with the reader key.
+ auto [getXYSuccess, ephX, ephY] = support::ecPublicKeyGetXandY(ephemeralPublicKey.value());
+ ASSERT_TRUE(getXYSuccess);
+ cppbor::Map deviceEngagement = cppbor::Map().add("ephX", ephX).add("ephY", ephY);
+ vector<uint8_t> deviceEngagementBytes = deviceEngagement.encode();
+ vector<uint8_t> eReaderPubBytes = cppbor::Tstr("ignored").encode();
+ cppbor::Array sessionTranscript = cppbor::Array()
+ .add(cppbor::Semantic(24, deviceEngagementBytes))
+ .add(cppbor::Semantic(24, eReaderPubBytes));
+ vector<uint8_t> sessionTranscriptBytes = sessionTranscript.encode();
+
+ vector<uint8_t> itemsRequestBytes =
+ cppbor::Map("nameSpaces",
+ cppbor::Map()
+ .add("PersonalData", cppbor::Map()
+ .add("Last name", false)
+ .add("Birth date", false)
+ .add("First name", false)
+ .add("Home address", true))
+ .add("Image", cppbor::Map().add("Portrait image", false)))
+ .encode();
+ cborPretty = support::cborPrettyPrint(itemsRequestBytes, 32, {"EphemeralPublicKey"});
+ EXPECT_EQ(
+ "{\n"
+ " 'nameSpaces' : {\n"
+ " 'PersonalData' : {\n"
+ " 'Last name' : false,\n"
+ " 'Birth date' : false,\n"
+ " 'First name' : false,\n"
+ " 'Home address' : true,\n"
+ " },\n"
+ " 'Image' : {\n"
+ " 'Portrait image' : false,\n"
+ " },\n"
+ " },\n"
+ "}",
+ cborPretty);
+ vector<uint8_t> dataToSign = cppbor::Array()
+ .add("ReaderAuthentication")
+ .add(sessionTranscript.clone())
+ .add(cppbor::Semantic(24, itemsRequestBytes))
+ .encode();
+ optional<vector<uint8_t>> readerSignature =
+ support::coseSignEcDsa(readerKey.value(), {}, // content
+ dataToSign, // detached content
+ readerCertificate.value());
+ ASSERT_TRUE(readerSignature);
+
+ ASSERT_TRUE(credential
+ ->startRetrieval(returnedSecureProfiles, authToken, itemsRequestBytes,
+ sessionTranscriptBytes, readerSignature.value(),
+ testEntriesEntryCounts)
+ .isOk());
+
+ for (const auto& entry : testEntries) {
+ ASSERT_TRUE(credential
+ ->startRetrieveEntryValue(entry.nameSpace, entry.name,
+ entry.valueCbor.size(), entry.profileIds)
+ .isOk());
+
+ auto it = encryptedBlobs.find(&entry);
+ ASSERT_NE(it, encryptedBlobs.end());
+ const vector<vector<uint8_t>>& encryptedChunks = it->second;
+
+ vector<uint8_t> content;
+ for (const auto& encryptedChunk : encryptedChunks) {
+ vector<uint8_t> chunk;
+ ASSERT_TRUE(credential->retrieveEntryValue(encryptedChunk, &chunk).isOk());
+ content.insert(content.end(), chunk.begin(), chunk.end());
+ }
+ EXPECT_EQ(content, entry.valueCbor);
+ }
+
+ // Generate the key that will be used to sign AuthenticatedData.
+ vector<uint8_t> signingKeyBlob;
+ Certificate signingKeyCertificate;
+ ASSERT_TRUE(credential->generateSigningKeyPair(&signingKeyBlob, &signingKeyCertificate).isOk());
+
+ vector<uint8_t> mac;
+ vector<uint8_t> deviceNameSpacesBytes;
+ ASSERT_TRUE(credential->finishRetrieval(signingKeyBlob, &mac, &deviceNameSpacesBytes).isOk());
+ cborPretty = support::cborPrettyPrint(deviceNameSpacesBytes, 32, {});
+ ASSERT_EQ(
+ "{\n"
+ " 'PersonalData' : {\n"
+ " 'Last name' : 'Turing',\n"
+ " 'Birth date' : '19120623',\n"
+ " 'First name' : 'Alan',\n"
+ " 'Home address' : 'Maida Vale, London, England',\n"
+ " },\n"
+ " 'Image' : {\n"
+ " 'Portrait image' : <bstr size=262134 "
+ "sha1=941e372f654d86c32d88fae9e41b706afbfd02bb>,\n"
+ " },\n"
+ "}",
+ cborPretty);
+ // The data that is MACed is ["DeviceAuthentication", sessionTranscriptBytes, docType,
+ // deviceNameSpacesBytes] so build up that structure
+ cppbor::Array deviceAuthentication;
+ deviceAuthentication.add("DeviceAuthentication");
+ deviceAuthentication.add(sessionTranscript.clone());
+ deviceAuthentication.add(docType);
+ deviceAuthentication.add(cppbor::Semantic(24, deviceNameSpacesBytes));
+ vector<uint8_t> encodedDeviceAuthentication = deviceAuthentication.encode();
+ optional<vector<uint8_t>> signingPublicKey =
+ support::certificateChainGetTopMostKey(signingKeyCertificate.encodedCertificate);
+ EXPECT_TRUE(signingPublicKey);
+
+ // Derive the key used for MACing.
+ optional<vector<uint8_t>> readerEphemeralPrivateKey =
+ support::ecKeyPairGetPrivateKey(readerEphemeralKeyPair.value());
+ optional<vector<uint8_t>> sharedSecret =
+ support::ecdh(signingPublicKey.value(), readerEphemeralPrivateKey.value());
+ ASSERT_TRUE(sharedSecret);
+ vector<uint8_t> salt = {0x00};
+ vector<uint8_t> info = {};
+ optional<vector<uint8_t>> derivedKey = support::hkdf(sharedSecret.value(), salt, info, 32);
+ ASSERT_TRUE(derivedKey);
+ optional<vector<uint8_t>> calculatedMac =
+ support::coseMac0(derivedKey.value(), {}, // payload
+ encodedDeviceAuthentication); // detached content
+ ASSERT_TRUE(calculatedMac);
+ EXPECT_EQ(mac, calculatedMac);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ Identity, IdentityAidl,
+ testing::ValuesIn(android::getAidlHalInstanceNames(IIdentityCredentialStore::descriptor)),
+ android::PrintInstanceNameToString);
+// INSTANTIATE_TEST_SUITE_P(Identity, IdentityAidl,
+// testing::Values("android.hardware.identity.IIdentityCredentialStore/default"));
+
+} // namespace android::hardware::identity
+
+int main(int argc, char** argv) {
+ ::testing::InitGoogleTest(&argc, argv);
+ ::android::ProcessState::self()->setThreadPoolMaxThreadCount(1);
+ ::android::ProcessState::self()->startThreadPool();
+ return RUN_ALL_TESTS();
+}