From c75ac31ec97c9a6116403355bc61e3976fe44074 Mon Sep 17 00:00:00 2001 From: David Zeuthen Date: Mon, 28 Oct 2019 13:16:45 -0400 Subject: [PATCH] Add Identity Credential HAL, default implementation, and VTS tests. 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. Bug: 111446262 Test: VtsHalIdentityCredentialTargetTest Test: android.hardware.identity-support-lib-test Test: CtsIdentityTestCases Change-Id: I64eb50114d645dd475012ad1b889d2177aaf1d37 --- current.txt | 4 + identity/1.0/Android.bp | 25 + identity/1.0/IIdentityCredential.hal | 343 ++++ identity/1.0/IIdentityCredentialStore.hal | 185 ++ identity/1.0/IWritableIdentityCredential.hal | 236 +++ identity/1.0/default/Android.bp | 43 + identity/1.0/default/IdentityCredential.cpp | 773 +++++++ identity/1.0/default/IdentityCredential.h | 132 ++ .../1.0/default/IdentityCredentialStore.cpp | 68 + .../1.0/default/IdentityCredentialStore.h | 55 + identity/1.0/default/OWNERS | 2 + .../default/WritableIdentityCredential.cpp | 426 ++++ .../1.0/default/WritableIdentityCredential.h | 105 + ...d.hardware.identity@1.0-service.example.rc | 3 + identity/1.0/default/service.cpp | 40 + identity/1.0/types.hal | 164 ++ identity/1.0/vts/OWNERS | 2 + identity/1.0/vts/functional/Android.bp | 36 + .../VtsHalIdentityCredentialTargetTest.cpp | 527 +++++ identity/support/Android.bp | 103 + .../support/IdentityCredentialSupport.h | 303 +++ identity/support/include/cppbor/README.md | 216 ++ identity/support/include/cppbor/cppbor.h | 827 ++++++++ .../support/include/cppbor/cppbor_parse.h | 133 ++ .../support/src/IdentityCredentialSupport.cpp | 1815 +++++++++++++++++ identity/support/src/cppbor.cpp | 225 ++ identity/support/src/cppbor_parse.cpp | 351 ++++ .../tests/IdentityCredentialSupportTest.cpp | 446 ++++ identity/support/tests/cppbor_test.cpp | 1013 +++++++++ 29 files changed, 8601 insertions(+) create mode 100644 identity/1.0/Android.bp create mode 100644 identity/1.0/IIdentityCredential.hal create mode 100644 identity/1.0/IIdentityCredentialStore.hal create mode 100644 identity/1.0/IWritableIdentityCredential.hal create mode 100644 identity/1.0/default/Android.bp create mode 100644 identity/1.0/default/IdentityCredential.cpp create mode 100644 identity/1.0/default/IdentityCredential.h create mode 100644 identity/1.0/default/IdentityCredentialStore.cpp create mode 100644 identity/1.0/default/IdentityCredentialStore.h create mode 100644 identity/1.0/default/OWNERS create mode 100644 identity/1.0/default/WritableIdentityCredential.cpp create mode 100644 identity/1.0/default/WritableIdentityCredential.h create mode 100644 identity/1.0/default/android.hardware.identity@1.0-service.example.rc create mode 100644 identity/1.0/default/service.cpp create mode 100644 identity/1.0/types.hal create mode 100644 identity/1.0/vts/OWNERS create mode 100644 identity/1.0/vts/functional/Android.bp create mode 100644 identity/1.0/vts/functional/VtsHalIdentityCredentialTargetTest.cpp create mode 100644 identity/support/Android.bp create mode 100644 identity/support/include/android/hardware/identity/support/IdentityCredentialSupport.h create mode 100644 identity/support/include/cppbor/README.md create mode 100644 identity/support/include/cppbor/cppbor.h create mode 100644 identity/support/include/cppbor/cppbor_parse.h create mode 100644 identity/support/src/IdentityCredentialSupport.cpp create mode 100644 identity/support/src/cppbor.cpp create mode 100644 identity/support/src/cppbor_parse.cpp create mode 100644 identity/support/tests/IdentityCredentialSupportTest.cpp create mode 100644 identity/support/tests/cppbor_test.cpp diff --git a/current.txt b/current.txt index ab49dc4948..8f28ad30fe 100644 --- a/current.txt +++ b/current.txt @@ -615,6 +615,10 @@ dd377f404a8e71f6191d295e10067db629b0f0c28e594af906f2bea5d87fe2cc android.hardwar ce8dbe76eb9ee94b46ef98f725be992e760a5751073d4f4912484026541371f3 android.hardware.health@2.1::IHealth 26f04510a0b57aba5167c5c0a7c2f077c2acbb98b81902a072517829fd9fd67f android.hardware.health@2.1::IHealthInfoCallback db47f4ceceb1f06c656f39caa70c557b0f8471ef59fd58611bea667ffca20101 android.hardware.health@2.1::types +0589e410f519e36514e7ece18f283f022df0f70efd2c12821d822f67f74aba98 android.hardware.identity@1.0::types +bbeee9604128ede83ee755b67e73b5ad29e6e1dbac9ec41fea6ffe2745b0c50a android.hardware.identity@1.0::IIdentityCredential +96ce8aad80f4c476f25261f790d357c117e79e18474c7dadd850dac704bbe65e android.hardware.identity@1.0::IIdentityCredentialStore +6e1e28a96c90ba78d47257faea3f3bb4e6360affbbfa5822f0dc31211f9266ff android.hardware.identity@1.0::IWritableIdentityCredential c228aaa27f66c48e147159a4f4996c5273191fece1b08de31bd171c61334855e android.hardware.keymaster@4.1::IKeymasterDevice adb0efdf1462e9b2e742c0dcadd598666aac551f178be06e755bfcdf5797abd0 android.hardware.keymaster@4.1::IOperation 7a04ea5595ed418ca3e91c28b8bd7353dd988be9be7b0c8c9e64fb4b77bd4523 android.hardware.keymaster@4.1::types diff --git a/identity/1.0/Android.bp b/identity/1.0/Android.bp new file mode 100644 index 0000000000..a5cea90656 --- /dev/null +++ b/identity/1.0/Android.bp @@ -0,0 +1,25 @@ +// This file is autogenerated by hidl-gen -Landroidbp. + +hidl_interface { + name: "android.hardware.identity@1.0", + root: "android.hardware", + vndk: { + enabled: true, + }, + srcs: [ + "types.hal", + "IIdentityCredential.hal", + "IIdentityCredentialStore.hal", + "IWritableIdentityCredential.hal", + ], + interfaces: [ + "android.hidl.base@1.0", + "android.hardware.keymaster@4.0", + ], + types: [ + "AuditLogEntry", + "ResultCode", + "SecureAccessControlProfile", + ], + gen_java: false, +} diff --git a/identity/1.0/IIdentityCredential.hal b/identity/1.0/IIdentityCredential.hal new file mode 100644 index 0000000000..75f6e1843d --- /dev/null +++ b/identity/1.0/IIdentityCredential.hal @@ -0,0 +1,343 @@ +/* + * 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@1.0; + +import android.hardware.keymaster@4.0::HardwareAuthToken; + +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 proofOfDeletionSignature is a COSE_Sign1 signature described above. + */ + deleteCredential() + generates(Result result, vec proofOfDeletionSignature); + + /** + * 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, FAILED + * will be returned. + * + * @return result is OK on success or FAILED if an error occurred. + * + * @return keyPair contains the unencrypted key-pair in PKCS#8 format. + */ + createEphemeralKeyPair() generates (Result result, vec keyPair); + + /** + * 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, FAILED + * will be returned. + * + * @param publicKey contains the reader's ephemeral public key, in uncompressed form. + * + * @return result is OK on success or FAILED if an error occurred. + */ + setReaderEphemeralPublicKey(vec publicKey) generates (Result result); + + /** + * 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, FAILED + * will be returned. + * + * @return result is OK on success or FAILED if an error occurred. + * + * @return challenge on success, is a non-zero number. + */ + createAuthChallenge() generates (Result result, uint64_t challenge); + + /** + * 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 SESSION_TRANSCRIPT_MISMATCH error. + * + * If the provided authToken is not valid this method fails with INVALID_AUTH_TOKEN. + * + * Each of the provided accessControlProfiles is checked in this call. If they are not + * all valid, the call fails with 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 + * 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 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 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 + * 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. + * + * @return result is OK on success. If an error occurs one of the values described above + * will be returned. + */ + startRetrieval(vec accessControlProfiles, + HardwareAuthToken authToken, + vec itemsRequest, + vec sessionTranscript, + vec readerSignature, + vec requestCounts) generates(Result result); + + /** + * 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 NOT_IN_REQUEST_MESSAGE. + * + * If nameSpace or name is empty this call fails with 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 USER_AUTHENTICATION_FAILED. If reader authentication is required and + * a suitable reader certificate chain isn't presented, the call fails with + * 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 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 INVALID_DATA. + * + * @return result is OK on success. Otherwise one of INVALID_DATA, FAILED, + * USER_AUTHENTICATION_FAILED, READER_AUTHENTICATION_FAILED. + */ + startRetrieveEntryValue(string nameSpace, string name, uint32_t entrySize, + vec accessControlProfileIds) + generates (Result result); + + + /** + * 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 INVALID_DATA. + * + * @param encryptedContent contains the encrypted and MACed content. + * + * @return result is OK on success, INVALID_DATA, or FAILED if an error occurred. + * + * @return content is the entry value as CBOR, or part of the entry value in the case the + * content exceeds gcmChunkSize in length. + */ + retrieveEntryValue(vec encryptedContent) + generates (Result result, vec content); + + + /** + * 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 INVALID_DATA. + * + * @return result is OK on success, INVALID_DATA or FAILED if an error occurred. + * + * @return 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 + * + * + * @return deviceNameSpaces the bytes of DeviceNameSpaces. + */ + finishRetrieval(vec signingKeyBlob) + generates(Result result, vec mac, vec deviceNameSpaces); + + + /** + * Generate a key pair to be used for signing session data and retrieved data items. + * + * @return result is OK on success or FAILED if an error occurred. + * + * @return signingKeyBlob contains an encrypted copy of the newly-generated private signing key. + * + * @return signingKeyCertificate contains an X.509 certificate for the new signing key, signed + * by the credential key. + */ + generateSigningKeyPair() + generates(Result result, vec signingKeyBlob, + vec signingKeyCertificate); +}; diff --git a/identity/1.0/IIdentityCredentialStore.hal b/identity/1.0/IIdentityCredentialStore.hal new file mode 100644 index 0000000000..118ca6fa9a --- /dev/null +++ b/identity/1.0/IIdentityCredentialStore.hal @@ -0,0 +1,185 @@ +/* + * 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@1.0; + +import IWritableIdentityCredential; +import IIdentityCredential; + +/** + * 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). + */ +interface IIdentityCredentialStore { + + /** + * Returns information about hardware. + * + * The isDirectAccess output parameter indicates whether this credential store + * implementation is for direct access. Credentials provisioned in credential + * stores with this set to true, should use reader authentication on all data elements. + * + * @return result is OK on success, FAILED if an error occurred. + * + * @return credentialStoreName the name of the credential store implementation. + * + * @return credentialStoreAuthorName the name of the credential store author. + * + * @return dataChunkSize the maximum size of data chunks. + * + * @return isDirectAccess whether the provisioned credential is available through + * direct access. + * + * @return supportedDocTypes if empty, then any document type is supported, otherwise + * only the document types returned are supported. + */ + getHardwareInformation() + generates(Result result, + string credentialStoreName, + string credentialStoreAuthorName, + uint32_t dataChunkSize, + bool isDirectAccess, + vec supportedDocTypes); + + /** + * 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 result is OK on success, FAILED if an error occurred. + * + * @return writableCredential is an IWritableIdentityCredential HIDL interface that provides + * operations to provision a credential. + */ + createCredential(string docType, bool testCredential) + generates(Result result, IWritableIdentityCredential writableCredential); + + /** + * getCredential retrieves an IIdentityCredential HIDL 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. + * + * @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 result is OK on success or INVALID_DATA if the passed in credentialData + * cannot be decoded or decrypted. + * + * @return credential is an IIdentityCredential HIDL interface that provides operations on the + * Credential. + */ + getCredential(vec credentialData) + generates (Result result, IIdentityCredential credential); +}; diff --git a/identity/1.0/IWritableIdentityCredential.hal b/identity/1.0/IWritableIdentityCredential.hal new file mode 100644 index 0000000000..b1ce00de4f --- /dev/null +++ b/identity/1.0/IWritableIdentityCredential.hal @@ -0,0 +1,236 @@ +/* + * 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@1.0; + +/** + * IWritableIdentityCredential is used to personalize a new identity credential. Credentials cannot + * be updated or modified after creation; any changes require deletion and re-creation. + */ +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) and must also + * have the Tag::IDENTITY_CREDENTIAL_KEY tag from KeyMaster 4.1 set. This tag indicates + * that this 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). + * + * @param attestationChallenge a challenge set by the issuer to ensure freshness. + * + * @return result is OK on success, FAILED if an error occurred. + * + * @return certificate is the X.509 certificate chain for the credentialKey + */ + getAttestationCertificate(vec attestationChallenge) + generates(Result result, vec certificate); + + /** + * 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. + * + * @return result is OK on success, FAILED if an error occurred. + * + */ + startPersonalization(uint16_t accessControlProfileCount, vec entryCounts) + generates(Result result); + + /** + * 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 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 + * 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 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 + * INVALID_DATA. + * + * @return result is OK on success, INVALID_DATA or FAILED if an error occurred. + * + * @return secureAccessControlProfile is a structure with the passed-in data and MAC created + * with storageKey for authenticating the data at a later point in time. + */ + addAccessControlProfile(uint16_t id, vec readerCertificate, + bool userAuthenticationRequired, uint64_t timeoutMillis, + uint64_t secureUserId) + generates(Result result, SecureAccessControlProfile secureAccessControlProfile); + + /** + * 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 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 INVALID_DATA. + * + * @return result is OK on success, INVALID_DATA or FAILED if an error occurred. + */ + beginAddEntry(vec accessControlProfileIds, string nameSpace, + string name, uint32_t entrySize) + generates(Result result); + + /** + * 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 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 result is OK on success, INVALID_DATA or FAILED if an error occurred. + * + * @return encryptedContent contains the encrypted and MACed content. For directly-available + * credentials the contents are implementation-defined but must not exceed 32 bytes in + * length. + * + * For other credentials, encryptedContent contains: + * + * AES-GCM-ENC(storageKey, R, Data, AdditionalData) + * + * where: + * + * Data = any ; value + * + * AdditionalData = { + * "Namespace" : tstr, + * "Name" : tstr, + * "AccessControlProfileIds" : [ + uint ], + * } + */ + addEntryValue(vec content) + generates(Result result, vec encryptedContent); + + /** + * 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. + * + * @return result is OK on success or FAILED if an error occurred. + * + * @return 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 + * ] + * + * @return 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), it 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 ], + * } + */ + finishAddingEntries() + generates(Result result, vec credentialData, + vec proofOfProvisioningSignature); +}; diff --git a/identity/1.0/default/Android.bp b/identity/1.0/default/Android.bp new file mode 100644 index 0000000000..d2b29660d3 --- /dev/null +++ b/identity/1.0/default/Android.bp @@ -0,0 +1,43 @@ +// +// 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. +// + +cc_binary { + name: "android.hardware.identity@1.0-service.example", + init_rc: ["android.hardware.identity@1.0-service.example.rc"], + vendor: true, + relative_install_path: "hw", + cflags: [ + "-Wall", + "-Wextra", + ], + srcs: [ + "service.cpp", + "IdentityCredential.cpp", + "IdentityCredentialStore.cpp", + "WritableIdentityCredential.cpp", + ], + shared_libs: [ + "android.hardware.identity@1.0", + "android.hardware.identity-support-lib", + "android.hardware.keymaster@4.0", + "libcppbor", + "libcrypto", + "libbase", + "libhidlbase", + "liblog", + "libutils", + ], +} diff --git a/identity/1.0/default/IdentityCredential.cpp b/identity/1.0/default/IdentityCredential.cpp new file mode 100644 index 0000000000..b0a5e568f2 --- /dev/null +++ b/identity/1.0/default/IdentityCredential.cpp @@ -0,0 +1,773 @@ +/* + * 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 + +#include + +#include + +#include +#include + +namespace android { +namespace hardware { +namespace identity { +namespace implementation { + +using ::android::hardware::keymaster::V4_0::Timestamp; +using ::std::optional; + +Return IdentityCredential::deleteCredential(deleteCredential_cb _hidl_cb) { + cppbor::Array array = {"ProofOfDeletion", docType_, testCredential_}; + vector proofOfDeletion = array.encode(); + + optional> proofOfDeletionSignature = + support::coseSignEcDsa(credentialPrivKey_, + proofOfDeletion, // payload + {}, // additionalData + {}); // certificateChain + if (!proofOfDeletionSignature) { + _hidl_cb(support::result(ResultCode::FAILED, "Error signing data"), {}); + return Void(); + } + + _hidl_cb(support::resultOK(), proofOfDeletionSignature.value()); + return Void(); +} + +Return IdentityCredential::createEphemeralKeyPair(createEphemeralKeyPair_cb _hidl_cb) { + optional> keyPair = support::createEcKeyPair(); + if (!keyPair) { + _hidl_cb(support::result(ResultCode::FAILED, "Error creating ephemeral key pair"), {}); + return Void(); + } + + // Stash public key of this key-pair for later check in startRetrieval(). + optional> publicKey = support::ecKeyPairGetPublicKey(keyPair.value()); + if (!publicKey) { + _hidl_cb(support::result(ResultCode::FAILED, + "Error getting public part of ephemeral key pair"), + {}); + return Void(); + } + ephemeralPublicKey_ = publicKey.value(); + + _hidl_cb(support::resultOK(), keyPair.value()); + return Void(); +} + +Return IdentityCredential::setReaderEphemeralPublicKey( + const hidl_vec& publicKey, setReaderEphemeralPublicKey_cb _hidl_cb) { + readerPublicKey_ = publicKey; + _hidl_cb(support::resultOK()); + return Void(); +} + +ResultCode IdentityCredential::initialize() { + auto [item, _, message] = cppbor::parse(credentialData_); + if (item == nullptr) { + LOG(ERROR) << "CredentialData is not valid CBOR: " << message; + return ResultCode::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 ResultCode::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 ResultCode::INVALID_DATA; + } + + docType_ = docTypeItem->value(); + testCredential_ = testCredentialItem->value(); + + vector hardwareBoundKey; + if (testCredential_) { + hardwareBoundKey = support::getTestHardwareBoundKey(); + } else { + hardwareBoundKey = support::getHardwareBoundKey(); + } + + const vector& encryptedCredentialKeys = encryptedCredentialKeysItem->value(); + const vector docTypeVec(docType_.begin(), docType_.end()); + optional> decryptedCredentialKeys = + support::decryptAes128Gcm(hardwareBoundKey, encryptedCredentialKeys, docTypeVec); + if (!decryptedCredentialKeys) { + LOG(ERROR) << "Error decrypting CredentialKeys"; + return ResultCode::INVALID_DATA; + } + + auto [dckItem, dckPos, dckMessage] = cppbor::parse(decryptedCredentialKeys.value()); + if (dckItem == nullptr) { + LOG(ERROR) << "Decrypted CredentialKeys is not valid CBOR: " << dckMessage; + return ResultCode::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 ResultCode::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 ResultCode::INVALID_DATA; + } + storageKey_ = storageKeyItem->value(); + credentialPrivKey_ = credentialPrivKeyItem->value(); + + return ResultCode::OK; +} + +Return IdentityCredential::createAuthChallenge(createAuthChallenge_cb _hidl_cb) { + uint64_t challenge = 0; + while (challenge == 0) { + optional> bytes = support::getRandom(8); + if (!bytes) { + _hidl_cb(support::result(ResultCode::FAILED, "Error getting random data for challenge"), + 0); + return Void(); + } + + challenge = 0; + for (size_t n = 0; n < bytes.value().size(); n++) { + challenge |= ((bytes.value())[n] << (n * 8)); + } + } + + authChallenge_ = challenge; + _hidl_cb(support::resultOK(), challenge); + return Void(); +} + +// 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& readerCertificateChain) { + optional> acpPubKey = + support::certificateChainGetTopMostKey(profile.readerCertificate); + if (!acpPubKey) { + LOG(ERROR) << "Error extracting public key from readerCertificate in profile"; + return false; + } + + optional>> certificatesInChain = + support::certificateChainSplit(readerCertificateChain); + if (!certificatesInChain) { + LOG(ERROR) << "Error splitting readerCertificateChain"; + return false; + } + for (const vector& certInChain : certificatesInChain.value()) { + optional> 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); + return time.tv_sec * 1000 + time.tv_nsec / 1000000; +} + +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 != 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 > now) { + LOG(ERROR) << "Timestamp in authToken (" << authToken.timestamp + << ") is in the future (now: " << now << ")"; + return false; + } + if (now > authToken.timestamp + profile.timeoutMillis) { + LOG(ERROR) << "Deadline for authToken (" << authToken.timestamp << " + " + << profile.timeoutMillis << " = " + << (authToken.timestamp + profile.timeoutMillis) + << ") is in the past (now: " << now << ")"; + return false; + } + + return true; +} + +Return IdentityCredential::startRetrieval( + const hidl_vec& accessControlProfiles, + const HardwareAuthToken& authToken, const hidl_vec& itemsRequest, + const hidl_vec& sessionTranscript, const hidl_vec& readerSignature, + const hidl_vec& requestCounts, startRetrieval_cb _hidl_cb) { + if (sessionTranscript.size() > 0) { + auto [item, _, message] = cppbor::parse(sessionTranscript); + if (item == nullptr) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, + "SessionTranscript contains invalid CBOR")); + return Void(); + } + sessionTranscriptItem_ = std::move(item); + } + if (numStartRetrievalCalls_ > 0) { + if (sessionTranscript_ != vector(sessionTranscript)) { + _hidl_cb(support::result( + ResultCode::SESSION_TRANSCRIPT_MISMATCH, + "Passed-in SessionTranscript doesn't match previously used SessionTranscript")); + return Void(); + } + } + 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> readerCertificateChain; + if (readerSignature.size() > 0) { + readerCertificateChain = support::coseSignGetX5Chain(readerSignature); + if (!readerCertificateChain) { + _hidl_cb(support::result(ResultCode::READER_SIGNATURE_CHECK_FAILED, + "Unable to get reader certificate chain from COSE_Sign1")); + return Void(); + } + + if (!support::certificateChainValidate(readerCertificateChain.value())) { + _hidl_cb(support::result(ResultCode::READER_SIGNATURE_CHECK_FAILED, + "Error validating reader certificate chain")); + return Void(); + } + + optional> readerPublicKey = + support::certificateChainGetTopMostKey(readerCertificateChain.value()); + if (!readerPublicKey) { + _hidl_cb(support::result(ResultCode::READER_SIGNATURE_CHECK_FAILED, + "Unable to get public key from reader certificate chain")); + return Void(); + } + + const vector& itemsRequestBytes = itemsRequest; + vector dataThatWasSigned = cppbor::Array() + .add("ReaderAuthentication") + .add(sessionTranscriptItem_->clone()) + .add(cppbor::Semantic(24, itemsRequestBytes)) + .encode(); + if (!support::coseCheckEcDsaSignature(readerSignature, + dataThatWasSigned, // detached content + readerPublicKey.value())) { + _hidl_cb(support::result(ResultCode::READER_SIGNATURE_CHECK_FAILED, + "readerSignature check failed")); + return Void(); + } + } + + // 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) { + _hidl_cb(support::result(ResultCode::EPHEMERAL_PUBLIC_KEY_NOT_FOUND, + "SessionTranscript is not an array with two items")); + return Void(); + } + const cppbor::Semantic* taggedEncodedDE = (*array)[0]->asSemantic(); + if (taggedEncodedDE == nullptr || taggedEncodedDE->value() != 24) { + _hidl_cb(support::result(ResultCode::EPHEMERAL_PUBLIC_KEY_NOT_FOUND, + "First item in SessionTranscript array is not a " + "semantic with value 24")); + return Void(); + } + const cppbor::Bstr* encodedDE = (taggedEncodedDE->child())->asBstr(); + if (encodedDE == nullptr) { + _hidl_cb(support::result(ResultCode::EPHEMERAL_PUBLIC_KEY_NOT_FOUND, + "Child of semantic in first item in SessionTranscript " + "array is not a bstr")); + return Void(); + } + const vector& bytesDE = encodedDE->value(); + + auto [getXYSuccess, ePubX, ePubY] = support::ecPublicKeyGetXandY(ephemeralPublicKey_); + if (!getXYSuccess) { + _hidl_cb(support::result(ResultCode::EPHEMERAL_PUBLIC_KEY_NOT_FOUND, + "Error extracting X and Y from ePub")); + return Void(); + } + if (sessionTranscript.size() > 0 && + !(memmem(bytesDE.data(), bytesDE.size(), ePubX.data(), ePubX.size()) != nullptr && + memmem(bytesDE.data(), bytesDE.size(), ePubY.data(), ePubY.size()) != nullptr)) { + _hidl_cb(support::result(ResultCode::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)")); + return Void(); + } + } + + // 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) { + _hidl_cb(support::result(ResultCode::INVALID_ITEMS_REQUEST_MESSAGE, + "Error decoding CBOR in itemsRequest: %s", message.c_str())); + return Void(); + } + + // 2. The CBOR structure must be a map. + const cppbor::Map* map = item->asMap(); + if (map == nullptr) { + _hidl_cb(support::result(ResultCode::INVALID_ITEMS_REQUEST_MESSAGE, + "itemsRequest is not a CBOR map")); + return Void(); + } + + // 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) { + _hidl_cb(support::result(ResultCode::INVALID_ITEMS_REQUEST_MESSAGE, + "No nameSpaces map in top-most map")); + return Void(); + } + + 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) { + _hidl_cb(support::result(ResultCode::INVALID_ITEMS_REQUEST_MESSAGE, + "Type mismatch in nameSpaces map")); + return Void(); + } + string requestedNamespace = nsKey->value(); + vector 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) { + _hidl_cb(support::result(ResultCode::INVALID_ITEMS_REQUEST_MESSAGE, + "Type mismatch in value in nameSpaces map")); + return Void(); + } + 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 (!support::secureAccessControlProfileCheckMac(profile, storageKey_)) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, + "Error checking MAC for profile with id %d", int(profile.id))); + return Void(); + } + ResultCode accessControlCheck = ResultCode::OK; + if (profile.userAuthenticationRequired) { + if (!haveAuthToken || !checkUserAuthentication(profile, authToken, authChallenge_)) { + accessControlCheck = ResultCode::USER_AUTHENTICATION_FAILED; + } + } else if (profile.readerCertificate.size() > 0) { + if (!readerCertificateChain || + !checkReaderAuthentication(profile, readerCertificateChain.value())) { + accessControlCheck = ResultCode::READER_AUTHENTICATION_FAILED; + } + } + profileIdToAccessCheckResult_[profile.id] = accessControlCheck; + } + + deviceNameSpacesMap_ = cppbor::Map(); + currentNameSpaceDeviceNameSpacesMap_ = cppbor::Map(); + + requestCountsRemaining_ = requestCounts; + currentNameSpace_ = ""; + + itemsRequest_ = itemsRequest; + + numStartRetrievalCalls_ += 1; + _hidl_cb(support::resultOK()); + return Void(); +} + +Return IdentityCredential::startRetrieveEntryValue( + const hidl_string& nameSpace, const hidl_string& name, uint32_t entrySize, + const hidl_vec& accessControlProfileIds, startRetrieveEntryValue_cb _hidl_cb) { + if (name.empty()) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, "Name cannot be empty")); + return Void(); + } + if (nameSpace.empty()) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, "Name space cannot be empty")); + return Void(); + } + + if (requestCountsRemaining_.size() == 0) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, + "No more name spaces left to go through")); + return Void(); + } + + if (currentNameSpace_ == "") { + // First call. + currentNameSpace_ = nameSpace; + } + + if (nameSpace == currentNameSpace_) { + // Same namespace. + if (requestCountsRemaining_[0] == 0) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, + "No more entries to be retrieved in current name space")); + return Void(); + } + requestCountsRemaining_[0] -= 1; + } else { + // New namespace. + if (requestCountsRemaining_[0] != 0) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, + "Moved to new name space but %d entries need to be retrieved " + "in current name space", + int(requestCountsRemaining_[0]))); + return Void(); + } + 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()) { + _hidl_cb(support::result(ResultCode::NOT_IN_REQUEST_MESSAGE, + "Name space '%s' was not requested in startRetrieval", + nameSpace.c_str())); + return Void(); + } + const auto& dataItemNames = it->second; + if (std::find(dataItemNames.begin(), dataItemNames.end(), name) == dataItemNames.end()) { + _hidl_cb(support::result( + ResultCode::NOT_IN_REQUEST_MESSAGE, + "Data item name '%s' in name space '%s' was not requested in startRetrieval", + name.c_str(), nameSpace.c_str())); + return Void(); + } + } + + // 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. + // + ResultCode accessControl = ResultCode::NO_ACCESS_CONTROL_PROFILES; + for (auto id : accessControlProfileIds) { + auto search = profileIdToAccessCheckResult_.find(id); + if (search == profileIdToAccessCheckResult_.end()) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, + "Requested entry with unvalidated profile id %d", (int(id)))); + return Void(); + } + ResultCode accessControlForProfile = search->second; + if (accessControlForProfile == ResultCode::OK) { + accessControl = ResultCode::OK; + break; + } + accessControl = accessControlForProfile; + } + if (accessControl != ResultCode::OK) { + _hidl_cb(support::result(accessControl, "Access control check failed")); + return Void(); + } + + entryAdditionalData_ = + support::entryCreateAdditionalData(nameSpace, name, accessControlProfileIds); + + currentName_ = name; + entryRemainingBytes_ = entrySize; + entryValue_.resize(0); + + _hidl_cb(support::resultOK()); + return Void(); +} + +Return IdentityCredential::retrieveEntryValue(const hidl_vec& encryptedContent, + retrieveEntryValue_cb _hidl_cb) { + optional> content = + support::decryptAes128Gcm(storageKey_, encryptedContent, entryAdditionalData_); + if (!content) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, "Error decrypting data"), {}); + return Void(); + } + + size_t chunkSize = content.value().size(); + + if (chunkSize > entryRemainingBytes_) { + LOG(ERROR) << "Retrieved chunk of size " << chunkSize + << " is bigger than remaining space of size " << entryRemainingBytes_; + _hidl_cb(support::result(ResultCode::INVALID_DATA, + "Retrieved chunk of size %zd is bigger than remaining space " + "of size %zd", + chunkSize, entryRemainingBytes_), + {}); + return Void(); + } + + entryRemainingBytes_ -= chunkSize; + if (entryRemainingBytes_ > 0) { + if (chunkSize != IdentityCredentialStore::kGcmChunkSize) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, + "Retrieved non-final chunk of size %zd but expected " + "kGcmChunkSize which is %zd", + chunkSize, IdentityCredentialStore::kGcmChunkSize), + {}); + return Void(); + } + } + + entryValue_.insert(entryValue_.end(), content.value().begin(), content.value().end()); + + if (entryRemainingBytes_ == 0) { + auto [entryValueItem, _, message] = cppbor::parse(entryValue_); + if (entryValueItem == nullptr) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, "Retrieved data invalid CBOR"), {}); + return Void(); + } + currentNameSpaceDeviceNameSpacesMap_.add(currentName_, std::move(entryValueItem)); + } + + _hidl_cb(support::resultOK(), content.value()); + return Void(); +} + +Return IdentityCredential::finishRetrieval(const hidl_vec& signingKeyBlob, + finishRetrieval_cb _hidl_cb) { + if (currentNameSpaceDeviceNameSpacesMap_.size() > 0) { + deviceNameSpacesMap_.add(currentNameSpace_, + std::move(currentNameSpaceDeviceNameSpacesMap_)); + } + vector encodedDeviceNameSpaces = deviceNameSpacesMap_.encode(); + + // If there's no signing key or no sessionTranscript or no reader ephemeral + // public key, we return the empty MAC. + optional> 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 encodedDeviceAuthentication = array.encode(); + + vector docTypeAsBlob(docType_.begin(), docType_.end()); + optional> signingKey = + support::decryptAes128Gcm(storageKey_, signingKeyBlob, docTypeAsBlob); + if (!signingKey) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, "Error decrypting signingKeyBlob"), + {}, {}); + return Void(); + } + + optional> sharedSecret = + support::ecdh(readerPublicKey_, signingKey.value()); + if (!sharedSecret) { + _hidl_cb(support::result(ResultCode::FAILED, "Error doing ECDH"), {}, {}); + return Void(); + } + + vector salt = {0x00}; + vector info = {}; + optional> derivedKey = support::hkdf(sharedSecret.value(), salt, info, 32); + if (!derivedKey) { + _hidl_cb(support::result(ResultCode::FAILED, "Error deriving key from shared secret"), + {}, {}); + return Void(); + } + + mac = support::coseMac0(derivedKey.value(), {}, // payload + encodedDeviceAuthentication); // additionalData + if (!mac) { + _hidl_cb(support::result(ResultCode::FAILED, "Error MACing data"), {}, {}); + return Void(); + } + } + + _hidl_cb(support::resultOK(), mac.value_or(vector({})), encodedDeviceNameSpaces); + return Void(); +} + +Return IdentityCredential::generateSigningKeyPair(generateSigningKeyPair_cb _hidl_cb) { + 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> signingKeyPKCS8 = support::createEcKeyPair(); + if (!signingKeyPKCS8) { + _hidl_cb(support::result(ResultCode::FAILED, "Error creating signingKey"), {}, {}); + return Void(); + } + + optional> signingPublicKey = + support::ecKeyPairGetPublicKey(signingKeyPKCS8.value()); + if (!signingPublicKey) { + _hidl_cb(support::result(ResultCode::FAILED, "Error getting public part of signingKey"), {}, + {}); + return Void(); + } + + optional> signingKey = support::ecKeyPairGetPrivateKey(signingKeyPKCS8.value()); + if (!signingKey) { + _hidl_cb(support::result(ResultCode::FAILED, "Error getting private part of signingKey"), + {}, {}); + return Void(); + } + + optional> certificate = support::ecPublicKeyGenerateCertificate( + signingPublicKey.value(), credentialPrivKey_, serialDecimal, issuer, subject, + validityNotBefore, validityNotAfter); + if (!certificate) { + _hidl_cb(support::result(ResultCode::FAILED, "Error creating signingKey"), {}, {}); + return Void(); + } + + optional> nonce = support::getRandom(12); + if (!nonce) { + _hidl_cb(support::result(ResultCode::FAILED, "Error getting random"), {}, {}); + return Void(); + } + vector docTypeAsBlob(docType_.begin(), docType_.end()); + optional> encryptedSigningKey = support::encryptAes128Gcm( + storageKey_, nonce.value(), signingKey.value(), docTypeAsBlob); + if (!encryptedSigningKey) { + _hidl_cb(support::result(ResultCode::FAILED, "Error encrypting signingKey"), {}, {}); + return Void(); + } + _hidl_cb(support::resultOK(), encryptedSigningKey.value(), certificate.value()); + return Void(); +} + +} // namespace implementation +} // namespace identity +} // namespace hardware +} // namespace android diff --git a/identity/1.0/default/IdentityCredential.h b/identity/1.0/default/IdentityCredential.h new file mode 100644 index 0000000000..eb8787b872 --- /dev/null +++ b/identity/1.0/default/IdentityCredential.h @@ -0,0 +1,132 @@ +/* + * 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 + +#include + +#include +#include +#include +#include + +#include + +namespace android { +namespace hardware { +namespace identity { +namespace implementation { + +using ::std::map; +using ::std::string; +using ::std::vector; + +using ::android::hardware::hidl_string; +using ::android::hardware::hidl_vec; +using ::android::hardware::Return; +using ::android::hardware::Void; +using ::android::hardware::identity::V1_0::IIdentityCredential; +using ::android::hardware::identity::V1_0::Result; +using ::android::hardware::identity::V1_0::ResultCode; +using ::android::hardware::identity::V1_0::SecureAccessControlProfile; +using ::android::hardware::keymaster::V4_0::HardwareAuthToken; + +using MapStringToVectorOfStrings = map>; + +class IdentityCredential : public IIdentityCredential { + public: + IdentityCredential(const hidl_vec& credentialData) + : credentialData_(credentialData), numStartRetrievalCalls_(0), authChallenge_(0) {} + + // Parses and decrypts credentialData_, return false on failure. Must be + // called right after construction. + ResultCode initialize(); + + // Methods from ::android::hardware::identity::IIdentityCredential follow. + + Return deleteCredential(deleteCredential_cb _hidl_cb) override; + Return createEphemeralKeyPair(createEphemeralKeyPair_cb _hidl_cb) override; + + Return setReaderEphemeralPublicKey(const hidl_vec& publicKey, + setReaderEphemeralPublicKey_cb _hidl_cb) override; + + Return createAuthChallenge(createAuthChallenge_cb _hidl_cb) override; + + Return startRetrieval(const hidl_vec& accessControlProfiles, + const HardwareAuthToken& authToken, + const hidl_vec& itemsRequest, + const hidl_vec& sessionTranscript, + const hidl_vec& readerSignature, + const hidl_vec& requestCounts, + startRetrieval_cb _hidl_cb) override; + Return startRetrieveEntryValue(const hidl_string& nameSpace, const hidl_string& name, + uint32_t entrySize, + const hidl_vec& accessControlProfileIds, + startRetrieveEntryValue_cb _hidl_cb) override; + Return retrieveEntryValue(const hidl_vec& encryptedContent, + retrieveEntryValue_cb _hidl_cb) override; + Return finishRetrieval(const hidl_vec& signingKeyBlob, + finishRetrieval_cb _hidl_cb) override; + + Return generateSigningKeyPair(generateSigningKeyPair_cb _hidl_cb) override; + + private: + // Set by constructor + vector credentialData_; + int numStartRetrievalCalls_; + + // Set by initialize() + string docType_; + bool testCredential_; + vector storageKey_; + vector credentialPrivKey_; + + // Set by createEphemeralKeyPair() + vector ephemeralPublicKey_; + + // Set by setReaderEphemeralPublicKey() + vector readerPublicKey_; + + // Set by createAuthChallenge() + uint64_t authChallenge_; + + // Set at startRetrieval() time. + map profileIdToAccessCheckResult_; + vector sessionTranscript_; + std::unique_ptr sessionTranscriptItem_; + vector itemsRequest_; + vector requestCountsRemaining_; + MapStringToVectorOfStrings requestedNameSpacesAndNames_; + cppbor::Map deviceNameSpacesMap_; + cppbor::Map currentNameSpaceDeviceNameSpacesMap_; + + // Set at startRetrieveEntryValue() time. + string currentNameSpace_; + string currentName_; + size_t entryRemainingBytes_; + vector entryValue_; + vector entryAdditionalData_; +}; + +} // namespace implementation +} // namespace identity +} // namespace hardware +} // namespace android + +#endif // ANDROID_HARDWARE_IDENTITY_IDENTITYCREDENTIAL_H diff --git a/identity/1.0/default/IdentityCredentialStore.cpp b/identity/1.0/default/IdentityCredentialStore.cpp new file mode 100644 index 0000000000..9eb1e70751 --- /dev/null +++ b/identity/1.0/default/IdentityCredentialStore.cpp @@ -0,0 +1,68 @@ +/* + * 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 "IdentityCredentialStore.h" +#include "IdentityCredential.h" +#include "WritableIdentityCredential.h" + +#include + +namespace android { +namespace hardware { +namespace identity { +namespace implementation { + +// Methods from ::android::hardware::identity::IIdentityCredentialStore follow. + +Return IdentityCredentialStore::getHardwareInformation(getHardwareInformation_cb _hidl_cb) { + _hidl_cb(support::resultOK(), "IdentityCredential Reference Implementation", "Google", + kGcmChunkSize, false /* isDirectAccess */, {} /* supportedDocTypes */); + return Void(); +} + +Return IdentityCredentialStore::createCredential(const hidl_string& docType, + bool testCredential, + createCredential_cb _hidl_cb) { + auto writable_credential = new WritableIdentityCredential(docType, testCredential); + if (!writable_credential->initialize()) { + _hidl_cb(support::result(ResultCode::FAILED, + "Error initializing WritableIdentityCredential"), + writable_credential); + return Void(); + } + _hidl_cb(support::resultOK(), writable_credential); + return Void(); +} + +Return IdentityCredentialStore::getCredential(const hidl_vec& credentialData, + getCredential_cb _hidl_cb) { + auto credential = new IdentityCredential(credentialData); + // We only support CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256 right now. + auto ret = credential->initialize(); + if (ret != ResultCode::OK) { + _hidl_cb(support::result(ret, "Error initializing IdentityCredential"), credential); + return Void(); + } + _hidl_cb(support::resultOK(), credential); + return Void(); +} + +} // namespace implementation +} // namespace identity +} // namespace hardware +} // namespace android diff --git a/identity/1.0/default/IdentityCredentialStore.h b/identity/1.0/default/IdentityCredentialStore.h new file mode 100644 index 0000000000..ad7536043f --- /dev/null +++ b/identity/1.0/default/IdentityCredentialStore.h @@ -0,0 +1,55 @@ +/* + * 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 + +namespace android { +namespace hardware { +namespace identity { +namespace implementation { + +using ::android::hardware::hidl_string; +using ::android::hardware::hidl_vec; +using ::android::hardware::Return; +using ::android::hardware::Void; +using ::android::hardware::identity::V1_0::IIdentityCredentialStore; +using ::android::hardware::identity::V1_0::Result; +using ::android::hardware::identity::V1_0::ResultCode; + +class IdentityCredentialStore : public IIdentityCredentialStore { + public: + IdentityCredentialStore() {} + + // The GCM chunk size used by this implementation is 64 KiB. + static constexpr size_t kGcmChunkSize = 64 * 1024; + + // Methods from ::android::hardware::identity::IIdentityCredentialStore follow. + Return getHardwareInformation(getHardwareInformation_cb _hidl_cb) override; + Return createCredential(const hidl_string& docType, bool testCredential, + createCredential_cb _hidl_cb) override; + Return getCredential(const hidl_vec& credentialData, + getCredential_cb _hidl_cb) override; +}; + +} // namespace implementation +} // namespace identity +} // namespace hardware +} // namespace android + +#endif // ANDROID_HARDWARE_IDENTITY_IDENTITYCREDENTIALSTORE_H diff --git a/identity/1.0/default/OWNERS b/identity/1.0/default/OWNERS new file mode 100644 index 0000000000..6969910ce5 --- /dev/null +++ b/identity/1.0/default/OWNERS @@ -0,0 +1,2 @@ +swillden@google.com +zeuthen@google.com diff --git a/identity/1.0/default/WritableIdentityCredential.cpp b/identity/1.0/default/WritableIdentityCredential.cpp new file mode 100644 index 0000000000..548b4c071a --- /dev/null +++ b/identity/1.0/default/WritableIdentityCredential.cpp @@ -0,0 +1,426 @@ +/* + * 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 "WritableIdentityCredential.h" +#include "IdentityCredentialStore.h" + +#include + +#include + +#include +#include + +namespace android { +namespace hardware { +namespace identity { +namespace implementation { + +using ::std::optional; + +// Writes CBOR-encoded structure to |credentialKeys| containing |storageKey| and +// |credentialPrivKey|. +static bool generateCredentialKeys(const vector& storageKey, + const vector& credentialPrivKey, + vector& 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& hardwareBoundKey, const string& docType, + bool testCredential, const vector& credentialKeys, + vector& credentialData) { + optional> nonce = support::getRandom(12); + if (!nonce) { + LOG(ERROR) << "Error getting random"; + return false; + } + vector docTypeAsVec(docType.begin(), docType.end()); + optional> 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; +} + +bool WritableIdentityCredential::initialize() { + optional> keyPair = support::createEcKeyPair(); + if (!keyPair) { + LOG(ERROR) << "Error creating credentialKey"; + return false; + } + + optional> pubKey = support::ecKeyPairGetPublicKey(keyPair.value()); + if (!pubKey) { + LOG(ERROR) << "Error getting public part of credentialKey"; + return false; + } + credentialPubKey_ = pubKey.value(); + + optional> privKey = support::ecKeyPairGetPrivateKey(keyPair.value()); + if (!privKey) { + LOG(ERROR) << "Error getting private part of credentialKey"; + return false; + } + credentialPrivKey_ = privKey.value(); + + optional> random = support::getRandom(16); + if (!random) { + LOG(ERROR) << "Error creating storageKey"; + return false; + } + storageKey_ = random.value(); + + return true; +} + +Return WritableIdentityCredential::getAttestationCertificate( + const hidl_vec& /* attestationChallenge */, + getAttestationCertificate_cb _hidl_cb) { + // 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> attestationKeyPair = support::createEcKeyPair(); + if (!attestationKeyPair) { + _hidl_cb(support::result(ResultCode::FAILED, "Error creating attestationKey"), {}); + return Void(); + } + + optional> attestationPubKey = + support::ecKeyPairGetPublicKey(attestationKeyPair.value()); + if (!attestationPubKey) { + _hidl_cb(support::result(ResultCode::FAILED, "Error getting public part of attestationKey"), + {}); + return Void(); + } + + optional> attestationPrivKey = + support::ecKeyPairGetPrivateKey(attestationKeyPair.value()); + if (!attestationPrivKey) { + _hidl_cb( + support::result(ResultCode::FAILED, "Error getting private part of attestationKey"), + {}); + return Void(); + } + + 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> credentialPubKeyCertificate = support::ecPublicKeyGenerateCertificate( + credentialPubKey_, attestationPrivKey.value(), serialDecimal, issuer, subject, + validityNotBefore, validityNotAfter); + if (!credentialPubKeyCertificate) { + _hidl_cb(support::result(ResultCode::FAILED, + "Error creating certificate for credentialPubKey"), + {}); + return Void(); + } + + // 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> attestationKeyCertificate = support::ecPublicKeyGenerateCertificate( + attestationPubKey.value(), attestationPrivKey.value(), serialDecimal, issuer, subject, + validityNotBefore, validityNotAfter); + if (!attestationKeyCertificate) { + _hidl_cb(support::result(ResultCode::FAILED, + "Error creating certificate for attestationPubKey"), + {}); + return Void(); + } + + // Concatenate the certificates to form the chain. + vector certificateChain; + certificateChain.insert(certificateChain.end(), credentialPubKeyCertificate.value().begin(), + credentialPubKeyCertificate.value().end()); + certificateChain.insert(certificateChain.end(), attestationKeyCertificate.value().begin(), + attestationKeyCertificate.value().end()); + + _hidl_cb(support::resultOK(), certificateChain); + return Void(); +} + +Return WritableIdentityCredential::startPersonalization(uint16_t accessControlProfileCount, + const hidl_vec& entryCounts, + startPersonalization_cb _hidl_cb) { + numAccessControlProfileRemaining_ = accessControlProfileCount; + remainingEntryCounts_ = entryCounts; + entryNameSpace_ = ""; + + signedDataAccessControlProfiles_ = cppbor::Array(); + signedDataNamespaces_ = cppbor::Map(); + signedDataCurrentNamespace_ = cppbor::Array(); + + _hidl_cb(support::resultOK()); + return Void(); +} + +Return WritableIdentityCredential::addAccessControlProfile( + uint16_t id, const hidl_vec& readerCertificate, bool userAuthenticationRequired, + uint64_t timeoutMillis, uint64_t secureUserId, addAccessControlProfile_cb _hidl_cb) { + SecureAccessControlProfile profile; + + if (numAccessControlProfileRemaining_ == 0) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, + "numAccessControlProfileRemaining_ is 0 and expected non-zero"), + profile); + return Void(); + } + + // Spec requires if |userAuthenticationRequired| is false, then |timeoutMillis| must also + // be zero. + if (!userAuthenticationRequired && timeoutMillis != 0) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, + "userAuthenticationRequired is false but timeout is non-zero"), + profile); + return Void(); + } + + profile.id = id; + profile.readerCertificate = readerCertificate; + profile.userAuthenticationRequired = userAuthenticationRequired; + profile.timeoutMillis = timeoutMillis; + profile.secureUserId = secureUserId; + optional> mac = + support::secureAccessControlProfileCalcMac(profile, storageKey_); + if (!mac) { + _hidl_cb(support::result(ResultCode::FAILED, "Error calculating MAC for profile"), profile); + return Void(); + } + profile.mac = mac.value(); + + cppbor::Map profileMap; + profileMap.add("id", profile.id); + if (profile.readerCertificate.size() > 0) { + profileMap.add("readerCertificate", cppbor::Bstr(profile.readerCertificate)); + } + if (profile.userAuthenticationRequired) { + profileMap.add("userAuthenticationRequired", profile.userAuthenticationRequired); + profileMap.add("timeoutMillis", profile.timeoutMillis); + } + signedDataAccessControlProfiles_.add(std::move(profileMap)); + + numAccessControlProfileRemaining_--; + + _hidl_cb(support::resultOK(), profile); + return Void(); +} + +Return WritableIdentityCredential::beginAddEntry( + const hidl_vec& accessControlProfileIds, const hidl_string& nameSpace, + const hidl_string& name, uint32_t entrySize, beginAddEntry_cb _hidl_cb) { + if (numAccessControlProfileRemaining_ != 0) { + LOG(ERROR) << "numAccessControlProfileRemaining_ is " << numAccessControlProfileRemaining_ + << " and expected zero"; + _hidl_cb(support::result(ResultCode::INVALID_DATA, + "numAccessControlProfileRemaining_ is %zd and expected zero", + numAccessControlProfileRemaining_)); + return Void(); + } + + if (remainingEntryCounts_.size() == 0) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, "No more namespaces to add to")); + return Void(); + } + + // 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) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, + "New namespace but %d entries remain to be added", + int(remainingEntryCounts_[0]))); + return Void(); + } + remainingEntryCounts_.erase(remainingEntryCounts_.begin()); + + if (signedDataCurrentNamespace_.size() > 0) { + signedDataNamespaces_.add(entryNameSpace_, std::move(signedDataCurrentNamespace_)); + signedDataCurrentNamespace_ = cppbor::Array(); + } + } else { + // Same namespace... + if (remainingEntryCounts_[0] == 0) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, + "Same namespace but no entries remain to be added")); + return Void(); + } + remainingEntryCounts_[0] -= 1; + } + + entryAdditionalData_ = + support::entryCreateAdditionalData(nameSpace, name, accessControlProfileIds); + + entryRemainingBytes_ = entrySize; + entryNameSpace_ = nameSpace; + entryName_ = name; + entryAccessControlProfileIds_ = accessControlProfileIds; + entryBytes_.resize(0); + // LOG(INFO) << "name=" << name << " entrySize=" << entrySize; + + _hidl_cb(support::resultOK()); + return Void(); +} + +Return WritableIdentityCredential::addEntryValue(const hidl_vec& content, + addEntryValue_cb _hidl_cb) { + size_t contentSize = content.size(); + + if (contentSize > IdentityCredentialStore::kGcmChunkSize) { + _hidl_cb(support::result( + ResultCode::INVALID_DATA, + "Passed in chunk of size %zd is bigger than kGcmChunkSize which is %zd", + contentSize, IdentityCredentialStore::kGcmChunkSize), + {}); + return Void(); + } + if (contentSize > entryRemainingBytes_) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, + "Passed in chunk of size %zd is bigger than remaining space " + "of size %zd", + contentSize, entryRemainingBytes_), + {}); + return Void(); + } + + entryBytes_.insert(entryBytes_.end(), content.begin(), content.end()); + entryRemainingBytes_ -= contentSize; + if (entryRemainingBytes_ > 0) { + if (contentSize != IdentityCredentialStore::kGcmChunkSize) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, + "Retrieved non-final chunk of size %zd but expected " + "kGcmChunkSize which is %zd", + contentSize, IdentityCredentialStore::kGcmChunkSize), + {}); + return Void(); + } + } + + optional> nonce = support::getRandom(12); + if (!nonce) { + _hidl_cb(support::result(ResultCode::FAILED, "Error getting nonce"), {}); + return Void(); + } + optional> encryptedContent = + support::encryptAes128Gcm(storageKey_, nonce.value(), content, entryAdditionalData_); + if (!encryptedContent) { + _hidl_cb(support::result(ResultCode::FAILED, "Error encrypting content"), {}); + return Void(); + } + + 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) { + _hidl_cb(support::result(ResultCode::INVALID_DATA, "Data is not valid CBOR"), {}); + return Void(); + } + 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)); + } + + _hidl_cb(support::resultOK(), encryptedContent.value()); + return Void(); +} + +Return WritableIdentityCredential::finishAddingEntries(finishAddingEntries_cb _hidl_cb) { + 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 encodedCbor = popArray.encode(); + + optional> signature = support::coseSignEcDsa(credentialPrivKey_, + encodedCbor, // payload + {}, // additionalData + {}); // certificateChain + if (!signature) { + _hidl_cb(support::result(ResultCode::FAILED, "Error signing data"), {}, {}); + return Void(); + } + + vector credentialKeys; + if (!generateCredentialKeys(storageKey_, credentialPrivKey_, credentialKeys)) { + _hidl_cb(support::result(ResultCode::FAILED, "Error generating CredentialKeys"), {}, {}); + return Void(); + } + + vector credentialData; + if (!generateCredentialData(testCredential_ ? support::getTestHardwareBoundKey() + : support::getHardwareBoundKey(), + docType_, testCredential_, credentialKeys, credentialData)) { + _hidl_cb(support::result(ResultCode::FAILED, "Error generating CredentialData"), {}, {}); + return Void(); + } + + _hidl_cb(support::resultOK(), credentialData, signature.value()); + return Void(); +} + +} // namespace implementation +} // namespace identity +} // namespace hardware +} // namespace android diff --git a/identity/1.0/default/WritableIdentityCredential.h b/identity/1.0/default/WritableIdentityCredential.h new file mode 100644 index 0000000000..9f4e303ac4 --- /dev/null +++ b/identity/1.0/default/WritableIdentityCredential.h @@ -0,0 +1,105 @@ +/* + * 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 + +#include + +#include + +namespace android { +namespace hardware { +namespace identity { +namespace implementation { + +using ::std::string; +using ::std::vector; + +using ::android::hardware::hidl_string; +using ::android::hardware::hidl_vec; +using ::android::hardware::Return; +using ::android::hardware::Void; +using ::android::hardware::identity::V1_0::IWritableIdentityCredential; +using ::android::hardware::identity::V1_0::Result; +using ::android::hardware::identity::V1_0::ResultCode; +using ::android::hardware::identity::V1_0::SecureAccessControlProfile; + +class WritableIdentityCredential : public IWritableIdentityCredential { + public: + WritableIdentityCredential(const hidl_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 ::android::hardware::identity::IWritableIdentityCredential + // follow. + Return getAttestationCertificate(const hidl_vec& attestationChallenge, + getAttestationCertificate_cb _hidl_cb) override; + + Return startPersonalization(uint16_t accessControlProfileCount, + const hidl_vec& entryCounts, + startPersonalization_cb _hidl_cb) override; + + Return addAccessControlProfile(uint16_t id, const hidl_vec& readerCertificate, + bool userAuthenticationRequired, uint64_t timeoutMillis, + uint64_t secureUserId, + addAccessControlProfile_cb _hidl_cb) override; + + Return beginAddEntry(const hidl_vec& accessControlProfileIds, + const hidl_string& nameSpace, const hidl_string& name, + uint32_t entrySize, beginAddEntry_cb _hidl_cb) override; + + Return addEntryValue(const hidl_vec& content, + addEntryValue_cb _hidl_cb) override; + + Return finishAddingEntries(finishAddingEntries_cb _hidl_cb) override; + + private: + string docType_; + bool testCredential_; + + // These are set in initialize(). + vector storageKey_; + vector credentialPrivKey_; + vector credentialPubKey_; + + // These fields are initialized during startPersonalization() + size_t numAccessControlProfileRemaining_; + vector remainingEntryCounts_; + cppbor::Array signedDataAccessControlProfiles_; + cppbor::Map signedDataNamespaces_; + cppbor::Array signedDataCurrentNamespace_; + + // These fields are initialized during beginAddEntry() + size_t entryRemainingBytes_; + vector entryAdditionalData_; + string entryNameSpace_; + string entryName_; + vector entryAccessControlProfileIds_; + vector entryBytes_; +}; + +} // namespace implementation +} // namespace identity +} // namespace hardware +} // namespace android + +#endif // ANDROID_HARDWARE_IDENTITY_WRITABLEIDENTITYCREDENTIAL_H diff --git a/identity/1.0/default/android.hardware.identity@1.0-service.example.rc b/identity/1.0/default/android.hardware.identity@1.0-service.example.rc new file mode 100644 index 0000000000..1eb7319e14 --- /dev/null +++ b/identity/1.0/default/android.hardware.identity@1.0-service.example.rc @@ -0,0 +1,3 @@ +service vendor.identity-1-0 /vendor/bin/hw/android.hardware.identity@1.0-service.example + class hal + user nobody diff --git a/identity/1.0/default/service.cpp b/identity/1.0/default/service.cpp new file mode 100644 index 0000000000..839e8030ea --- /dev/null +++ b/identity/1.0/default/service.cpp @@ -0,0 +1,40 @@ +/* + * 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@1.0-service" + +#include +#include + +#include "IdentityCredentialStore.h" + +using android::hardware::joinRpcThreadpool; +using android::hardware::identity::implementation::IdentityCredentialStore; + +int main(int /* argc */, char* argv[]) { + ::android::hardware::configureRpcThreadpool(1, true /*willJoinThreadpool*/); + + ::android::base::InitLogging(argv, &android::base::StderrLogger); + + auto identity_store = new IdentityCredentialStore(); + auto status = identity_store->registerAsService(); + if (status != android::OK) { + LOG(FATAL) << "Could not register service for IdentityCredentialStore 1.0 (" << status + << ")"; + } + joinRpcThreadpool(); + return -1; // Should never get here. +} diff --git a/identity/1.0/types.hal b/identity/1.0/types.hal new file mode 100644 index 0000000000..5aedfea4dc --- /dev/null +++ b/identity/1.0/types.hal @@ -0,0 +1,164 @@ +/* + * Copyright 2018 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@1.0; + +/** + * The ResultCode enumeration is used to convey the status of an operation. + */ +enum ResultCode : int32_t { + /** + * Success. + */ + 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. + */ + FAILED = 1, + + /** + * The passed data was invalid. This is a generic catch all for errors that don't belong + * in other categories related to parameter validation. + */ + INVALID_DATA = 2, + + /** + * The authToken parameter passed to IIdentityCredential.startRetrieval() is not valid. + */ + INVALID_AUTH_TOKEN = 3, + + /** + * The itemsRequest parameter passed to IIdentityCredential.startRetrieval() does not meet + * the requirements described in the documentation for that method. + */ + INVALID_ITEMS_REQUEST_MESSAGE = 4, + + /** + * The readerSignature parameter in IIdentityCredential.startRetrieval() is invalid, + * doesn't contain an embedded certificate chain, or the signature failed to + * validate. + */ + READER_SIGNATURE_CHECK_FAILED = 5, + + /** + * The sessionTranscript passed to startRetrieval() did not contain the ephmeral public + * key returned by createEphemeralPublicKey(). + */ + EPHEMERAL_PUBLIC_KEY_NOT_FOUND = 6, + + /** + * An access condition related to user authentication was not satisfied. + */ + USER_AUTHENTICATION_FAILED = 7, + + /** + * An access condition related to reader authentication was not satisfied. + */ + READER_AUTHENTICATION_FAILED = 8, + + /** + * The request data element has no access control profiles associated so it cannot be accessed. + */ + NO_ACCESS_CONTROL_PROFILES = 9, + + /** + * The requested data element is not in the provided non-empty itemsRequest message. + */ + NOT_IN_REQUEST_MESSAGE = 10, + + /** + * The passed-in sessionTranscript doesn't match the previously passed-in sessionTranscript. + */ + SESSION_TRANSCRIPT_MISMATCH = 11, +}; + +/** + * A result has a ResultCode and corresponding textual message. + */ +struct Result { + /** + * The result code. + * + * Implementations must not use values not defined in the ResultCode enumeration. + */ + ResultCode code; + + /** + * A human-readable message in English conveying more detail about a failure. + * + * If code is ResultCode::OK this field must be set to the empty string. + */ + string message; +}; + +struct SecureAccessControlProfile { + /** + * id is a numeric identifier that must be unique within the context of a Credential and may be + * used to reference the profile. + */ + uint16_t 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. + */ + vec readerCertificate; + + /** + * if true, the user is required to authenticate to allow requests. Required authentication + * fressness is specified by timeout below. + * + */ + bool 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. + */ + uint64_t 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. + */ + uint64_t 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 + * ) + * } + */ + vec mac; +}; diff --git a/identity/1.0/vts/OWNERS b/identity/1.0/vts/OWNERS new file mode 100644 index 0000000000..6969910ce5 --- /dev/null +++ b/identity/1.0/vts/OWNERS @@ -0,0 +1,2 @@ +swillden@google.com +zeuthen@google.com diff --git a/identity/1.0/vts/functional/Android.bp b/identity/1.0/vts/functional/Android.bp new file mode 100644 index 0000000000..03b49de716 --- /dev/null +++ b/identity/1.0/vts/functional/Android.bp @@ -0,0 +1,36 @@ +// +// 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. +// + +cc_test { + name: "VtsHalIdentityCredentialTargetTest", + defaults: ["VtsHalTargetTestDefaults"], + srcs: [ + "VtsHalIdentityCredentialTargetTest.cpp", + ], + static_libs: [ + "android.hardware.identity@1.0", + "android.hardware.identity-support-lib", + "android.hardware.keymaster@4.0", + "libcppbor", + ], + shared_libs: [ + "libcrypto", + ], + test_suites: [ + "general-tests", + "vts-core", + ], +} diff --git a/identity/1.0/vts/functional/VtsHalIdentityCredentialTargetTest.cpp b/identity/1.0/vts/functional/VtsHalIdentityCredentialTargetTest.cpp new file mode 100644 index 0000000000..903e912b83 --- /dev/null +++ b/identity/1.0/vts/functional/VtsHalIdentityCredentialTargetTest.cpp @@ -0,0 +1,527 @@ +/* + * 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 "IdentityCredentialHidlHalTest" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using std::map; +using std::optional; +using std::string; +using std::vector; + +namespace android { +namespace hardware { +namespace identity { +namespace test { + +using ::android::hardware::identity::V1_0::IIdentityCredential; +using ::android::hardware::identity::V1_0::IIdentityCredentialStore; +using ::android::hardware::identity::V1_0::IWritableIdentityCredential; +using ::android::hardware::identity::V1_0::Result; +using ::android::hardware::identity::V1_0::ResultCode; +using ::android::hardware::identity::V1_0::SecureAccessControlProfile; +using ::android::hardware::keymaster::V4_0::HardwareAuthToken; + +// --------------------------------------------------------------------------- +// Test Data. +// --------------------------------------------------------------------------- + +struct TestEntryData { + TestEntryData(string nameSpace, string name, vector profileIds) + : nameSpace(nameSpace), name(name), profileIds(profileIds) {} + + TestEntryData(string nameSpace, string name, const string& value, vector profileIds) + : TestEntryData(nameSpace, name, profileIds) { + valueCbor = cppbor::Tstr(((const char*)value.data())).encode(); + } + TestEntryData(string nameSpace, string name, const vector& value, + vector profileIds) + : TestEntryData(nameSpace, name, profileIds) { + valueCbor = cppbor::Bstr(value).encode(); + } + TestEntryData(string nameSpace, string name, bool value, vector profileIds) + : TestEntryData(nameSpace, name, profileIds) { + valueCbor = cppbor::Bool(value).encode(); + } + TestEntryData(string nameSpace, string name, int64_t value, vector profileIds) + : TestEntryData(nameSpace, name, profileIds) { + if (value >= 0) { + valueCbor = cppbor::Uint(value).encode(); + } else { + valueCbor = cppbor::Nint(-value).encode(); + } + } + + string nameSpace; + string name; + vector valueCbor; + vector profileIds; +}; + +struct TestProfile { + uint16_t id; + hidl_vec readerCertificate; + bool userAuthenticationRequired; + uint64_t timeoutMillis; +}; + +/************************************ + * TEST DATA FOR AUTHENTICATION + ************************************/ +// Test authentication token for user authentication + +class IdentityCredentialStoreHidlTest : public ::testing::TestWithParam { + public: + virtual void SetUp() override { + string serviceName = GetParam(); + ASSERT_FALSE(serviceName.empty()); + credentialStore_ = IIdentityCredentialStore::getService(serviceName); + ASSERT_NE(credentialStore_, nullptr); + + credentialStore_->getHardwareInformation( + [&](const Result& result, const hidl_string& credentialStoreName, + const hidl_string& credentialStoreAuthorName, uint32_t chunkSize, + bool /* isDirectAccess */, + const hidl_vec /* supportedDocTypes */) { + EXPECT_EQ("", result.message); + ASSERT_EQ(ResultCode::OK, result.code); + ASSERT_GT(credentialStoreName.size(), 0u); + ASSERT_GT(credentialStoreAuthorName.size(), 0u); + ASSERT_GE(chunkSize, 256u); // Chunk sizes < APDU buffer won't be supported + dataChunkSize_ = chunkSize; + }); + } + virtual void TearDown() override {} + + uint32_t dataChunkSize_ = 0; + + sp credentialStore_; +}; + +TEST_P(IdentityCredentialStoreHidlTest, HardwareConfiguration) { + credentialStore_->getHardwareInformation( + [&](const Result& result, const hidl_string& credentialStoreName, + const hidl_string& credentialStoreAuthorName, uint32_t chunkSize, + bool /* isDirectAccess */, const hidl_vec /* supportedDocTypes */) { + EXPECT_EQ("", result.message); + ASSERT_EQ(ResultCode::OK, result.code); + ASSERT_GT(credentialStoreName.size(), 0u); + ASSERT_GT(credentialStoreAuthorName.size(), 0u); + ASSERT_GE(chunkSize, 256u); // Chunk sizes < APDU buffer won't be supported + }); +} + +TEST_P(IdentityCredentialStoreHidlTest, createAndRetrieveCredential) { + // First, generate a key-pair for the reader since its public key will be + // part of the request data. + optional> readerKeyPKCS8 = support::createEcKeyPair(); + ASSERT_TRUE(readerKeyPKCS8); + optional> readerPublicKey = + support::ecKeyPairGetPublicKey(readerKeyPKCS8.value()); + optional> 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> 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 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 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 testEntries = { + {"PersonalData", "Last name", string("Turing"), vector{0, 1}}, + {"PersonalData", "Birth date", string("19120623"), vector{0, 1}}, + {"PersonalData", "First name", string("Alan"), vector{0, 1}}, + {"PersonalData", "Home address", string("Maida Vale, London, England"), + vector{0}}, + {"Image", "Portrait image", portraitImage, vector{0, 1}}, + }; + const vector testEntriesEntryCounts = {static_cast(testEntries.size() - 1), + 1u}; + + string cborPretty; + sp writableCredential; + + hidl_vec empty{0}; + + string docType = "org.iso.18013-5.2019.mdl"; + bool testCredential = true; + Result result; + credentialStore_->createCredential( + docType, testCredential, + [&](const Result& _result, const sp& _writableCredential) { + result = _result; + writableCredential = _writableCredential; + }); + EXPECT_EQ("", result.message); + ASSERT_EQ(ResultCode::OK, result.code); + ASSERT_NE(writableCredential, nullptr); + + string challenge = "attestationChallenge"; + vector attestationChallenge(challenge.begin(), challenge.end()); + vector attestationCertificate; + writableCredential->getAttestationCertificate( + attestationChallenge, + [&](const Result& _result, const hidl_vec& _attestationCertificate) { + result = _result; + attestationCertificate = _attestationCertificate; + }); + EXPECT_EQ("", result.message); + ASSERT_EQ(ResultCode::OK, result.code); + + writableCredential->startPersonalization(testProfiles.size(), testEntriesEntryCounts, + [&](const Result& _result) { result = _result; }); + EXPECT_EQ("", result.message); + ASSERT_EQ(ResultCode::OK, result.code); + + vector returnedSecureProfiles; + for (const auto& testProfile : testProfiles) { + SecureAccessControlProfile profile; + writableCredential->addAccessControlProfile( + testProfile.id, testProfile.readerCertificate, + testProfile.userAuthenticationRequired, testProfile.timeoutMillis, + 0, // secureUserId + [&](const Result& _result, const SecureAccessControlProfile& _profile) { + result = _result; + profile = _profile; + }); + EXPECT_EQ("", result.message); + ASSERT_EQ(ResultCode::OK, result.code); + ASSERT_EQ(testProfile.id, profile.id); + ASSERT_EQ(testProfile.readerCertificate, profile.readerCertificate); + 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>> encryptedBlobs; + + for (const auto& entry : testEntries) { + vector> chunks = support::chunkVector(entry.valueCbor, dataChunkSize_); + + writableCredential->beginAddEntry(entry.profileIds, entry.nameSpace, entry.name, + entry.valueCbor.size(), + [&](const Result& _result) { result = _result; }); + EXPECT_EQ("", result.message); + ASSERT_EQ(ResultCode::OK, result.code); + + vector> encryptedChunks; + for (const auto& chunk : chunks) { + writableCredential->addEntryValue( + chunk, [&](const Result& result, hidl_vec encryptedContent) { + EXPECT_EQ("", result.message); + ASSERT_EQ(ResultCode::OK, result.code); + ASSERT_GT(encryptedContent.size(), 0u); + encryptedChunks.push_back(encryptedContent); + }); + } + encryptedBlobs[&entry] = encryptedChunks; + } + + vector credentialData; + vector proofOfProvisioningSignature; + writableCredential->finishAddingEntries( + [&](const Result& _result, const hidl_vec& _credentialData, + const hidl_vec& _proofOfProvisioningSignature) { + result = _result; + credentialData = _credentialData; + proofOfProvisioningSignature = _proofOfProvisioningSignature; + }); + EXPECT_EQ("", result.message); + ASSERT_EQ(ResultCode::OK, result.code); + + optional> 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' : ,\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' : ,\n" + " 'accessControlProfiles' : [0, 1, ],\n" + " },\n" + " ],\n" + " },\n" + " true,\n" + "]", + cborPretty); + + optional> credentialPubKey = + support::certificateChainGetTopMostKey(attestationCertificate); + 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 credential; + credentialStore_->getCredential( + credentialData, [&](const Result& _result, const sp& _credential) { + result = _result; + credential = _credential; + }); + EXPECT_EQ("", result.message); + ASSERT_EQ(ResultCode::OK, result.code); + ASSERT_NE(credential, nullptr); + + optional> readerEphemeralKeyPair = support::createEcKeyPair(); + ASSERT_TRUE(readerEphemeralKeyPair); + optional> readerEphemeralPublicKey = + support::ecKeyPairGetPublicKey(readerEphemeralKeyPair.value()); + credential->setReaderEphemeralPublicKey(readerEphemeralPublicKey.value(), + [&](const Result& _result) { result = _result; }); + EXPECT_EQ("", result.message); + ASSERT_EQ(ResultCode::OK, result.code); + + vector ephemeralKeyPair; + credential->createEphemeralKeyPair( + [&](const Result& _result, const hidl_vec& _ephemeralKeyPair) { + result = _result; + ephemeralKeyPair = _ephemeralKeyPair; + }); + EXPECT_EQ("", result.message); + ASSERT_EQ(ResultCode::OK, result.code); + optional> 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 deviceEngagementBytes = deviceEngagement.encode(); + vector eReaderPubBytes = cppbor::Tstr("ignored").encode(); + cppbor::Array sessionTranscript = cppbor::Array() + .add(cppbor::Semantic(24, deviceEngagementBytes)) + .add(cppbor::Semantic(24, eReaderPubBytes)); + vector sessionTranscriptBytes = sessionTranscript.encode(); + + vector 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 dataToSign = cppbor::Array() + .add("ReaderAuthentication") + .add(sessionTranscript.clone()) + .add(cppbor::Semantic(24, itemsRequestBytes)) + .encode(); + optional> readerSignature = + support::coseSignEcDsa(readerKey.value(), {}, // content + dataToSign, // detached content + readerCertificate.value()); + ASSERT_TRUE(readerSignature); + + credential->startRetrieval(returnedSecureProfiles, authToken, itemsRequestBytes, + sessionTranscriptBytes, readerSignature.value(), + testEntriesEntryCounts, + [&](const Result& _result) { result = _result; }); + EXPECT_EQ("", result.message); + ASSERT_EQ(ResultCode::OK, result.code); + + for (const auto& entry : testEntries) { + credential->startRetrieveEntryValue(entry.nameSpace, entry.name, entry.valueCbor.size(), + entry.profileIds, + [&](const Result& _result) { result = _result; }); + EXPECT_EQ("", result.message); + ASSERT_EQ(ResultCode::OK, result.code); + + auto it = encryptedBlobs.find(&entry); + ASSERT_NE(it, encryptedBlobs.end()); + const vector>& encryptedChunks = it->second; + + vector content; + for (const auto& encryptedChunk : encryptedChunks) { + vector chunk; + credential->retrieveEntryValue( + encryptedChunk, [&](const Result& _result, const hidl_vec& _chunk) { + result = _result; + chunk = _chunk; + }); + EXPECT_EQ("", result.message); + ASSERT_EQ(ResultCode::OK, result.code); + content.insert(content.end(), chunk.begin(), chunk.end()); + } + EXPECT_EQ(content, entry.valueCbor); + } + + // Generate the key that will be used to sign AuthenticatedData. + vector signingKeyBlob; + vector signingKeyCertificate; + credential->generateSigningKeyPair([&](const Result& _result, + const hidl_vec _signingKeyBlob, + const hidl_vec _signingKeyCertificate) { + result = _result; + signingKeyBlob = _signingKeyBlob; + signingKeyCertificate = _signingKeyCertificate; + }); + EXPECT_EQ("", result.message); + ASSERT_EQ(ResultCode::OK, result.code); + + vector mac; + vector deviceNameSpacesBytes; + credential->finishRetrieval(signingKeyBlob, + [&](const Result& _result, const hidl_vec _mac, + const hidl_vec _deviceNameSpacesBytes) { + result = _result; + mac = _mac; + deviceNameSpacesBytes = _deviceNameSpacesBytes; + }); + EXPECT_EQ("", result.message); + ASSERT_EQ(ResultCode::OK, result.code); + 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' : ,\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 encodedDeviceAuthentication = deviceAuthentication.encode(); + optional> signingPublicKey = + support::certificateChainGetTopMostKey(signingKeyCertificate); + EXPECT_TRUE(signingPublicKey); + + // Derive the key used for MACing. + optional> readerEphemeralPrivateKey = + support::ecKeyPairGetPrivateKey(readerEphemeralKeyPair.value()); + optional> sharedSecret = + support::ecdh(signingPublicKey.value(), readerEphemeralPrivateKey.value()); + ASSERT_TRUE(sharedSecret); + vector salt = {0x00}; + vector info = {}; + optional> derivedKey = support::hkdf(sharedSecret.value(), salt, info, 32); + ASSERT_TRUE(derivedKey); + optional> calculatedMac = + support::coseMac0(derivedKey.value(), {}, // payload + encodedDeviceAuthentication); // detached content + ASSERT_TRUE(calculatedMac); + EXPECT_EQ(mac, calculatedMac); +} + +INSTANTIATE_TEST_SUITE_P(PerInstance, IdentityCredentialStoreHidlTest, + testing::ValuesIn(android::hardware::getAllHalInstanceNames( + IIdentityCredentialStore::descriptor)), + android::hardware::PrintInstanceNameToString); + +} // namespace test +} // namespace identity +} // namespace hardware +} // namespace android diff --git a/identity/support/Android.bp b/identity/support/Android.bp new file mode 100644 index 0000000000..38dc10b485 --- /dev/null +++ b/identity/support/Android.bp @@ -0,0 +1,103 @@ +// 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. +// + +cc_library { + name: "android.hardware.identity-support-lib", + vendor_available: true, + srcs: [ + "src/IdentityCredentialSupport.cpp", + ], + export_include_dirs: [ + "include", + ], + shared_libs: [ + "android.hardware.identity@1.0", + "libcrypto", + "libbase", + "libhidlbase", + "libhardware", + ], + static_libs: [ + "libcppbor", + ], +} + +cc_test { + name: "android.hardware.identity-support-lib-test", + srcs: [ + "tests/IdentityCredentialSupportTest.cpp", + ], + shared_libs: [ + "android.hardware.identity-support-lib", + "android.hardware.identity@1.0", + "libcrypto", + "libbase", + "libhidlbase", + "libhardware", + ], + static_libs: [ + "libcppbor", + "libgmock", + ], + test_suites: ["general-tests"], +} + +// -- + +cc_library { + name: "libcppbor", + vendor_available: true, + host_supported: true, + srcs: [ + "src/cppbor.cpp", + "src/cppbor_parse.cpp", + ], + export_include_dirs: [ + "include/cppbor", + ], + shared_libs: [ + "libbase", + ], +} + +cc_test { + name: "cppbor_test", + srcs: [ + "tests/cppbor_test.cpp", + ], + shared_libs: [ + "libcppbor", + "libbase", + ], + static_libs: [ + "libgmock", + ], + test_suites: ["general-tests"], +} + +cc_test_host { + name: "cppbor_host_test", + srcs: [ + "tests/cppbor_test.cpp", + ], + shared_libs: [ + "libcppbor", + "libbase", + ], + static_libs: [ + "libgmock", + ], + test_suites: ["general-tests"], +} diff --git a/identity/support/include/android/hardware/identity/support/IdentityCredentialSupport.h b/identity/support/include/android/hardware/identity/support/IdentityCredentialSupport.h new file mode 100644 index 0000000000..485571a775 --- /dev/null +++ b/identity/support/include/android/hardware/identity/support/IdentityCredentialSupport.h @@ -0,0 +1,303 @@ +/* + * 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 IDENTITY_SUPPORT_INCLUDE_IDENTITY_CREDENTIAL_UTILS_H_ +#define IDENTITY_SUPPORT_INCLUDE_IDENTITY_CREDENTIAL_UTILS_H_ + +#include +#include +#include +#include + +#include + +namespace android { +namespace hardware { +namespace identity { +namespace support { + +using ::std::optional; +using ::std::string; +using ::std::tuple; +using ::std::vector; + +using ::android::hardware::identity::V1_0::Result; +using ::android::hardware::identity::V1_0::ResultCode; +using ::android::hardware::identity::V1_0::SecureAccessControlProfile; + +// --------------------------------------------------------------------------- +// Miscellaneous utilities. +// --------------------------------------------------------------------------- + +// Dumps the data in |data| to stderr. The written data will be of the following +// form for the call hexdump("signature", data) where |data| is of size 71: +// +// signature: dumping 71 bytes +// 0000 30 45 02 21 00 ac c6 12 60 56 a2 e9 ee 16 be 14 0E.!....`V...... +// 0010 69 7f c4 00 95 8c e8 55 1f 22 de 34 0b 08 8a 3b i......U.".4...; +// 0020 a0 56 54 05 07 02 20 58 77 d9 8c f9 eb 41 df fd .VT... Xw....A.. +// 0030 c1 a3 14 e0 bf b0 a2 c5 0c b6 85 8c 4a 0d f9 2b ............J..+ +// 0040 b7 8f d2 1d 9b 11 ac ....... +// +// This should only be used for debugging. +void hexdump(const string& name, const vector& data); + +string encodeHex(const string& str); + +string encodeHex(const vector& data); + +string encodeHex(const uint8_t* data, size_t dataLen); + +optional> decodeHex(const string& hexEncoded); + +// --------------------------------------------------------------------------- +// CBOR utilities. +// --------------------------------------------------------------------------- + +// Returns pretty-printed CBOR for |value|. +// +// Only valid CBOR should be passed to this function. +// +// If a byte-string is larger than |maxBStrSize| its contents will not be +// printed, instead the value of the form "" will be printed. Pass zero +// for |maxBStrSize| to disable this. +// +// The |mapKeysToNotPrint| parameter specifies the name of map values +// to not print. This is useful for unit tests. +string cborPrettyPrint(const vector& encodedCbor, size_t maxBStrSize = 32, + const vector& mapKeysToNotPrint = {}); + +// --------------------------------------------------------------------------- +// Crypto functionality / abstraction. +// --------------------------------------------------------------------------- + +constexpr size_t kAesGcmIvSize = 12; +constexpr size_t kAesGcmTagSize = 16; +constexpr size_t kAes128GcmKeySize = 16; + +// Returns |numBytes| bytes of random data. +optional> getRandom(size_t numBytes); + +// Calculates the SHA-256 of |data|. +vector sha256(const vector& data); + +// Decrypts |encryptedData| using |key| and |additionalAuthenticatedData|, +// returns resulting plaintext. The format of |encryptedData| must +// be as specified in the encryptAes128Gcm() function. +optional> decryptAes128Gcm(const vector& key, + const vector& encryptedData, + const vector& additionalAuthenticatedData); + +// Encrypts |data| with |key| and |additionalAuthenticatedData| using |nonce|, +// returns the resulting (nonce || ciphertext || tag). +optional> encryptAes128Gcm(const vector& key, const vector& nonce, + const vector& data, + const vector& additionalAuthenticatedData); + +// --------------------------------------------------------------------------- +// EC crypto functionality / abstraction (only supports P-256). +// --------------------------------------------------------------------------- + +// Creates an 256-bit EC key using the NID_X9_62_prime256v1 curve, returns the +// PKCS#8 encoded key-pair. +// +optional> createEcKeyPair(); + +// For an EC key |keyPair| encoded in PKCS#8 format, extracts the public key in +// uncompressed point form. +// +optional> ecKeyPairGetPublicKey(const vector& keyPair); + +// For an EC key |keyPair| encoded in PKCS#8 format, extracts the private key as +// an EC uncompressed key. +// +optional> ecKeyPairGetPrivateKey(const vector& keyPair); + +// For an EC key |keyPair| encoded in PKCS#8 format, creates a PKCS#12 structure +// with the key-pair (not using a password to encrypt the data). The public key +// in the created structure is included as a certificate, using the given fields +// |serialDecimal|, |issuer|, |subject|, |validityNotBefore|, and +// |validityNotAfter|. +// +optional> ecKeyPairGetPkcs12(const vector& keyPair, const string& name, + const string& serialDecimal, const string& issuer, + const string& subject, time_t validityNotBefore, + time_t validityNotAfter); + +// Signs |data| with |key| (which must be in the format returned by +// ecKeyPairGetPrivateKey()). Signature is returned and will be in DER format. +// +optional> signEcDsa(const vector& key, const vector& data); + +// Calculates the HMAC with SHA-256 for |data| using |key|. The calculated HMAC +// is returned and will be 32 bytes. +// +optional> hmacSha256(const vector& key, const vector& data); + +// Checks that |signature| (in DER format) is a valid signature of |digest|, +// made with |publicKey| (which must be in the format returned by +// ecKeyPairGetPublicKey()). +// +bool checkEcDsaSignature(const vector& digest, const vector& signature, + const vector& publicKey); + +// Extracts the public-key from the top-most certificate in |certificateChain| +// (which should be a concatenated chain of DER-encoded X.509 certificates). +// +// The returned public key will be in the same format as returned by +// ecKeyPairGetPublicKey(). +// +optional> certificateChainGetTopMostKey(const vector& certificateChain); + +// Generates a X.509 certificate for |publicKey| (which must be in the format +// returned by ecKeyPairGetPublicKey()). +// +// The certificate is signed by |signingKey| (which must be in the format +// returned by ecKeyPairGetPrivateKey()) +// +optional> ecPublicKeyGenerateCertificate( + const vector& publicKey, const vector& signingKey, + const string& serialDecimal, const string& issuer, const string& subject, + time_t validityNotBefore, time_t validityNotAfter); + +// Performs Elliptic-curve Diffie-Helman using |publicKey| (which must be in the +// format returned by ecKeyPairGetPublicKey()) and |privateKey| (which must be +// in the format returned by ecKeyPairGetPrivateKey()). +// +// On success, the computed shared secret is returned. +// +optional> ecdh(const vector& publicKey, const vector& privateKey); + +// Key derivation function using SHA-256, conforming to RFC 5869. +// +// On success, the derived key is returned. +// +optional> hkdf(const vector& sharedSecret, const vector& salt, + const vector& info, size_t size); + +// Returns the X and Y coordinates from |publicKey| (which must be in the format +// returned by ecKeyPairGetPublicKey()). +// +// Success is indicated by the first value in the returned tuple. If successful, +// the returned coordinates will be in uncompressed form. +// +tuple, vector> ecPublicKeyGetXandY(const vector& publicKey); + +// Concatenates all certificates into |certificateChain| together into a +// single bytestring. +// +// This is the reverse operation of certificateChainSplit(). +vector certificateChainJoin(const vector>& certificateChain); + +// Splits all the certificates in a single bytestring into individual +// certificates. +// +// Returns nothing if |certificateChain| contains invalid data. +// +// This is the reverse operation of certificateChainJoin(). +optional>> certificateChainSplit(const vector& certificateChain); + +// Validates that the certificate chain is valid. In particular, checks that each +// certificate in the chain is signed by the public key in the following certificate. +// +// Returns false if |certificateChain| failed validation or if each certificate +// is not signed by its successor. +// +bool certificateChainValidate(const vector& certificateChain); + +// Signs |data| and |detachedContent| with |key| (which must be in the format +// returned by ecKeyPairGetPrivateKey()). +// +// On success, the Signature is returned and will be in COSE_Sign1 format. +// +// If |certificateChain| is non-empty it's included in the 'x5chain' +// protected header element (as as described in'draft-ietf-cose-x509-04'). +// +optional> coseSignEcDsa(const vector& key, const vector& data, + const vector& detachedContent, + const vector& certificateChain); + +// Checks that |signatureCoseSign1| (in COSE_Sign1 format) is a valid signature +// made with |public_key| (which must be in the format returned by +// ecKeyPairGetPublicKey()) where |detachedContent| is the detached content. +// +bool coseCheckEcDsaSignature(const vector& signatureCoseSign1, + const vector& detachedContent, + const vector& publicKey); + +// Extracts the payload from a COSE_Sign1. +optional> coseSignGetPayload(const vector& signatureCoseSign1); + +// Extracts the X.509 certificate chain, if present. Returns the data as a +// concatenated chain of DER-encoded X.509 certificates +// +// Returns nothing if there is no 'x5chain' element or an error occurs. +// +optional> coseSignGetX5Chain(const vector& signatureCoseSign1); + +// MACs |data| and |detachedContent| with |key| (which can be any sequence of +// bytes). +// +// If successful, the MAC is returned and will be in COSE_Mac0 format. +// +optional> coseMac0(const vector& key, const vector& data, + const vector& detachedContent); + +// --------------------------------------------------------------------------- +// Platform abstraction. +// --------------------------------------------------------------------------- + +// Returns the hardware-bound AES-128 key. +const vector& getHardwareBoundKey(); + +// --------------------------------------------------------------------------- +// Utility functions specific to IdentityCredential. +// --------------------------------------------------------------------------- + +// Returns a reference to a Result with code OK and empty message. +const Result& resultOK(); + +// Returns a new Result with the given code and message. +Result result(ResultCode code, const char* format, ...) __attribute__((format(printf, 2, 3))); + +// Splits the given bytestring into chunks. If the given vector is smaller or equal to +// |maxChunkSize| a vector with |content| as the only element is returned. Otherwise +// |content| is split into N vectors each of size |maxChunkSize| except the final element +// may be smaller than |maxChunkSize|. +vector> chunkVector(const vector& content, size_t maxChunkSize); + +// Calculates the MAC for |profile| using |storageKey|. +optional> secureAccessControlProfileCalcMac( + const SecureAccessControlProfile& profile, const vector& storageKey); + +// Checks authenticity of the MAC in |profile| using |storageKey|. +bool secureAccessControlProfileCheckMac(const SecureAccessControlProfile& profile, + const vector& storageKey); + +// Returns the testing AES-128 key where all bits are set to 0. +const vector& getTestHardwareBoundKey(); + +// Creates the AdditionalData CBOR used in the addEntryValue() HIDL method. +vector entryCreateAdditionalData(const string& nameSpace, const string& name, + const vector accessControlProfileIds); + +} // namespace support +} // namespace identity +} // namespace hardware +} // namespace android + +#endif // IDENTITY_SUPPORT_INCLUDE_IDENTITY_CREDENTIAL_UTILS_H_ diff --git a/identity/support/include/cppbor/README.md b/identity/support/include/cppbor/README.md new file mode 100644 index 0000000000..723bfcfbaf --- /dev/null +++ b/identity/support/include/cppbor/README.md @@ -0,0 +1,216 @@ +CppBor: A Modern C++ CBOR Parser and Generator +============================================== + +CppBor provides a natural and easy-to-use syntax for constructing and +parsing CBOR messages. It does not (yet) support all features of +CBOR, nor (yet) support validation against CDDL schemata, though both +are planned. CBOR features that aren't supported include: + +* Indefinite length values +* Semantic tagging +* Floating point + +CppBor requires C++-17. + +## CBOR representation + +CppBor represents CBOR data items as instances of the `Item` class or, +more precisely, as instances of subclasses of `Item`, since `Item` is a +pure interface. The subclasses of `Item` correspond almost one-to-one +with CBOR major types, and are named to match the CDDL names to which +they correspond. They are: + +* `Uint` corresponds to major type 0, and can hold unsigned integers + up through (2^64 - 1). +* `Nint` corresponds to major type 1. It can only hold values from -1 + to -(2^63 - 1), since it's internal representation is an int64_t. + This can be fixed, but it seems unlikely that applications will need + the omitted range from -(2^63) to (2^64 - 1), since it's + inconvenient to represent them in many programming languages. +* `Int` is an abstract base of `Uint` and `Nint` that facilitates + working with all signed integers representable with int64_t. +* `Bstr` corresponds to major type 2, a byte string. +* `Tstr` corresponds to major type 3, a text string. +* `Array` corresponds to major type 4, an Array. It holds a + variable-length array of `Item`s. +* `Map` corresponds to major type 5, a Map. It holds a + variable-length array of pairs of `Item`s. +* `Simple` corresponds to major type 7. It's an abstract class since + items require more specific type. +* `Bool` is the only currently-implemented subclass of `Simple`. + +Note that major type 6, semantic tag, is not yet implemented. + +In practice, users of CppBor will rarely use most of these classes +when generating CBOR encodings. This is because CppBor provides +straightforward conversions from the obvious normal C++ types. +Specifically, the following conversions are provided in appropriate +contexts: + +* Signed and unsigned integers convert to `Uint` or `Nint`, as + appropriate. +* `std::string`, `std::string_view`, `const char*` and + `std::pair` convert to `Tstr`. +* `std::vector`, `std::pair` and `std::pair` convert to `Bstr`. +* `bool` converts to `Bool`. + +## CBOR generation + +### Complete tree generation + +The set of `encode` methods in `Item` provide the interface for +producing encoded CBOR. The basic process for "complete tree" +generation (as opposed to "incremental" generation, which is discussed +below) is to construct an `Item` which models the data to be encoded, +and then call one of the `encode` methods, whichever is convenient for +the encoding destination. A trivial example: + +``` +cppbor::Uint val(0); +std::vector encoding = val.encode(); +``` + + It's relatively rare that single values are encoded as above. More often, the + "root" data item will be an `Array` or `Map` which contains a more complex structure.For example + : + +``` using cppbor::Map; +using cppbor::Array; + +std::vector vec = // ... + Map val("key1", Array(Map("key_a", 99 "key_b", vec), "foo"), "key2", true); +std::vector encoding = val.encode(); +``` + +This creates a map with two entries, with `Tstr` keys "Outer1" and +"Outer2", respectively. The "Outer1" entry has as its value an +`Array` containing a `Map` and a `Tstr`. The "Outer2" entry has a +`Bool` value. + +This example demonstrates how automatic conversion of C++ types to +CppBor `Item` subclass instances is done. Where the caller provides a +C++ or C string, a `Tstr` entry is added. Where the caller provides +an integer literal or variable, a `Uint` or `Nint` is added, depending +on whether the value is positive or negative. + +As an alternative, a more fluent-style API is provided for building up +structures. For example: + +``` +using cppbor::Map; +using cppbor::Array; + +std::vector vec = // ... + Map val(); +val.add("key1", Array().add(Map().add("key_a", 99).add("key_b", vec)).add("foo")).add("key2", true); +std::vector encoding = val.encode(); +``` + + An advantage of this interface over the constructor - + based creation approach above is that it need not be done all at once + .The `add` methods return a reference to the object added to to allow calls to be chained, + but chaining is not necessary; calls can be made +sequentially, as the data to add is available. + +#### `encode` methods + +There are several variations of `Item::encode`, all of which +accomplish the same task but output the encoded data in different +ways, and with somewhat different performance characteristics. The +provided options are: + +* `bool encode(uint8\_t** pos, const uint8\_t* end)` encodes into the + buffer referenced by the range [`*pos`, end). `*pos` is moved. If + the encoding runs out of buffer space before finishing, the method + returns false. This is the most efficient way to encode, into an + already-allocated buffer. +* `void encode(EncodeCallback encodeCallback)` calls `encodeCallback` + for each encoded byte. It's the responsibility of the implementor + of the callback to behave safely in the event that the output buffer + (if applicable) is exhausted. This is less efficient than the prior + method because it imposes an additional function call for each byte. +* `template void encode(OutputIterator i)` + encodes into the provided iterator. SFINAE ensures that the + template doesn't match for non-iterators. The implementation + actually uses the callback-based method, plus has whatever overhead + the iterator adds. +* `std::vector encode()` creates a new std::vector, reserves + sufficient capacity to hold the encoding, and inserts the encoded + bytes with a std::pushback_iterator and the previous method. +* `std::string toString()` does the same as the previous method, but + returns a string instead of a vector. + +### Incremental generation + +Incremental generation requires deeper understanding of CBOR, because +the library can't do as much to ensure that the output is valid. The +basic tool for intcremental generation is the `encodeHeader` +function. There are two variations, one which writes into a buffer, +and one which uses a callback. Both simply write out the bytes of a +header. To construct the same map as in the above examples, +incrementally, one might write: + +``` +using namespace cppbor; // For example brevity + +std::vector encoding; +auto iter = std::back_inserter(result); +encodeHeader(MAP, 2 /* # of map entries */, iter); +std::string s = "key1"; +encodeHeader(TSTR, s.size(), iter); +std::copy(s.begin(), s.end(), iter); +encodeHeader(ARRAY, 2 /* # of array entries */, iter); +Map().add("key_a", 99).add("key_b", vec).encode(iter) +s = "foo"; +encodeHeader(TSTR, foo.size(), iter); +std::copy(s.begin(), s.end(), iter); +s = "key2"; +encodeHeader(TSTR, foo.size(), iter); +std::copy(s.begin(), s.end(), iter); +encodeHeader(SIMPLE, TRUE, iter); +``` + +As the above example demonstrates, the styles can be mixed -- Note the +creation and encoding of the inner Map using the fluent style. + +## Parsing + +CppBor also supports parsing of encoded CBOR data, with the same +feature set as encoding. There are two basic approaches to parsing, +"full" and "stream" + +### Full parsing + +Full parsing means completely parsing a (possibly-compound) data +item from a byte buffer. The `parse` functions that do not take a +`ParseClient` pointer do this. They return a `ParseResult` which is a +tuple of three values: + +* std::unique_ptr that points to the parsed item, or is nullptr + if there was a parse error. +* const uint8_t* that points to the byte after the end of the decoded + item, or to the first unparseable byte in the event of an error. +* std::string that is empty on success or contains an error message if + a parse error occurred. + +Assuming a successful parse, you can then use `Item::type()` to +discover the type of the parsed item (e.g. MAP), and then use the +appropriate `Item::as*()` method (e.g. `Item::asMap()`) to get a +pointer to an interface which allows you to retrieve specific values. + +### Stream parsing + +Stream parsing is more complex, but more flexible. To use +StreamParsing, you must create your own subclass of `ParseClient` and +call one of the `parse` functions that accepts it. See the +`ParseClient` methods docstrings for details. + +One unusual feature of stream parsing is that the `ParseClient` +callback methods not only provide the parsed Item, but also pointers +to the portion of the buffer that encode that Item. This is useful +if, for example, you want to find an element inside of a structure, +and then copy the encoding of that sub-structure, without bothering to +parse the rest. + +The full parser is implemented with the stream parser. diff --git a/identity/support/include/cppbor/cppbor.h b/identity/support/include/cppbor/cppbor.h new file mode 100644 index 0000000000..a755db13a2 --- /dev/null +++ b/identity/support/include/cppbor/cppbor.h @@ -0,0 +1,827 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace cppbor { + +enum MajorType : uint8_t { + UINT = 0 << 5, + NINT = 1 << 5, + BSTR = 2 << 5, + TSTR = 3 << 5, + ARRAY = 4 << 5, + MAP = 5 << 5, + SEMANTIC = 6 << 5, + SIMPLE = 7 << 5, +}; + +enum SimpleType { + BOOLEAN, + NULL_T, // Only two supported, as yet. +}; + +enum SpecialAddlInfoValues : uint8_t { + FALSE = 20, + TRUE = 21, + NULL_V = 22, + ONE_BYTE_LENGTH = 24, + TWO_BYTE_LENGTH = 25, + FOUR_BYTE_LENGTH = 26, + EIGHT_BYTE_LENGTH = 27, +}; + +class Item; +class Uint; +class Nint; +class Int; +class Tstr; +class Bstr; +class Simple; +class Bool; +class Array; +class Map; +class Null; +class Semantic; + +/** + * Returns the size of a CBOR header that contains the additional info value addlInfo. + */ +size_t headerSize(uint64_t addlInfo); + +/** + * Encodes a CBOR header with the specified type and additional info into the range [pos, end). + * Returns a pointer to one past the last byte written, or nullptr if there isn't sufficient space + * to write the header. + */ +uint8_t* encodeHeader(MajorType type, uint64_t addlInfo, uint8_t* pos, const uint8_t* end); + +using EncodeCallback = std::function; + +/** + * Encodes a CBOR header with the specified type and additional info, passing each byte in turn to + * encodeCallback. + */ +void encodeHeader(MajorType type, uint64_t addlInfo, EncodeCallback encodeCallback); + +/** + * Encodes a CBOR header with the specified type and additional info, writing each byte to the + * provided OutputIterator. + */ +template ::iterator_category>>> +void encodeHeader(MajorType type, uint64_t addlInfo, OutputIterator iter) { + return encodeHeader(type, addlInfo, [&](uint8_t v) { *iter++ = v; }); +} + +/** + * Item represents a CBOR-encodeable data item. Item is an abstract interface with a set of virtual + * methods that allow encoding of the item or conversion to the appropriate derived type. + */ +class Item { + public: + virtual ~Item() {} + + /** + * Returns the CBOR type of the item. + */ + virtual MajorType type() const = 0; + + // These methods safely downcast an Item to the appropriate subclass. + virtual const Int* asInt() const { return nullptr; } + virtual const Uint* asUint() const { return nullptr; } + virtual const Nint* asNint() const { return nullptr; } + virtual const Tstr* asTstr() const { return nullptr; } + virtual const Bstr* asBstr() const { return nullptr; } + virtual const Simple* asSimple() const { return nullptr; } + virtual const Map* asMap() const { return nullptr; } + virtual const Array* asArray() const { return nullptr; } + virtual const Semantic* asSemantic() const { return nullptr; } + + /** + * Returns true if this is a "compound" item, i.e. one that contains one or more other items. + */ + virtual bool isCompound() const { return false; } + + bool operator==(const Item& other) const&; + bool operator!=(const Item& other) const& { return !(*this == other); } + + /** + * Returns the number of bytes required to encode this Item into CBOR. Note that if this is a + * complex Item, calling this method will require walking the whole tree. + */ + virtual size_t encodedSize() const = 0; + + /** + * Encodes the Item into buffer referenced by range [*pos, end). Returns a pointer to one past + * the last position written. Returns nullptr if there isn't enough space to encode. + */ + virtual uint8_t* encode(uint8_t* pos, const uint8_t* end) const = 0; + + /** + * Encodes the Item by passing each encoded byte to encodeCallback. + */ + virtual void encode(EncodeCallback encodeCallback) const = 0; + + /** + * Clones the Item + */ + virtual std::unique_ptr clone() const = 0; + + /** + * Encodes the Item into the provided OutputIterator. + */ + template ::iterator_category> + void encode(OutputIterator i) const { + return encode([&](uint8_t v) { *i++ = v; }); + } + + /** + * Encodes the Item into a new std::vector. + */ + std::vector encode() const { + std::vector retval; + retval.reserve(encodedSize()); + encode(std::back_inserter(retval)); + return retval; + } + + /** + * Encodes the Item into a new std::string. + */ + std::string toString() const { + std::string retval; + retval.reserve(encodedSize()); + encode([&](uint8_t v) { retval.push_back(v); }); + return retval; + } + + /** + * Encodes only the header of the Item. + */ + inline uint8_t* encodeHeader(uint64_t addlInfo, uint8_t* pos, const uint8_t* end) const { + return ::cppbor::encodeHeader(type(), addlInfo, pos, end); + } + + /** + * Encodes only the header of the Item. + */ + inline void encodeHeader(uint64_t addlInfo, EncodeCallback encodeCallback) const { + ::cppbor::encodeHeader(type(), addlInfo, encodeCallback); + } +}; + +/** + * Int is an abstraction that allows Uint and Nint objects to be manipulated without caring about + * the sign. + */ +class Int : public Item { + public: + bool operator==(const Int& other) const& { return value() == other.value(); } + + virtual int64_t value() const = 0; + + const Int* asInt() const override { return this; } +}; + +/** + * Uint is a concrete Item that implements CBOR major type 0. + */ +class Uint : public Int { + public: + static constexpr MajorType kMajorType = UINT; + + explicit Uint(uint64_t v) : mValue(v) {} + + bool operator==(const Uint& other) const& { return mValue == other.mValue; } + + MajorType type() const override { return kMajorType; } + const Uint* asUint() const override { return this; } + + size_t encodedSize() const override { return headerSize(mValue); } + + int64_t value() const override { return mValue; } + uint64_t unsignedValue() const { return mValue; } + + using Item::encode; + uint8_t* encode(uint8_t* pos, const uint8_t* end) const override { + return encodeHeader(mValue, pos, end); + } + void encode(EncodeCallback encodeCallback) const override { + encodeHeader(mValue, encodeCallback); + } + + virtual std::unique_ptr clone() const override { return std::make_unique(mValue); } + + private: + uint64_t mValue; +}; + +/** + * Nint is a concrete Item that implements CBOR major type 1. + + * Note that it is incapable of expressing the full range of major type 1 values, becaue it can only + * express values that fall into the range [std::numeric_limits::min(), -1]. It cannot + * express values in the range [std::numeric_limits::min() - 1, + * -std::numeric_limits::max()]. + */ +class Nint : public Int { + public: + static constexpr MajorType kMajorType = NINT; + + explicit Nint(int64_t v); + + bool operator==(const Nint& other) const& { return mValue == other.mValue; } + + MajorType type() const override { return kMajorType; } + const Nint* asNint() const override { return this; } + size_t encodedSize() const override { return headerSize(addlInfo()); } + + int64_t value() const override { return mValue; } + + using Item::encode; + uint8_t* encode(uint8_t* pos, const uint8_t* end) const override { + return encodeHeader(addlInfo(), pos, end); + } + void encode(EncodeCallback encodeCallback) const override { + encodeHeader(addlInfo(), encodeCallback); + } + + virtual std::unique_ptr clone() const override { return std::make_unique(mValue); } + + private: + uint64_t addlInfo() const { return -1ll - mValue; } + + int64_t mValue; +}; + +/** + * Bstr is a concrete Item that implements major type 2. + */ +class Bstr : public Item { + public: + static constexpr MajorType kMajorType = BSTR; + + // Construct from a vector + explicit Bstr(std::vector v) : mValue(std::move(v)) {} + + // Construct from a string + explicit Bstr(const std::string& v) + : mValue(reinterpret_cast(v.data()), + reinterpret_cast(v.data()) + v.size()) {} + + // Construct from a pointer/size pair + explicit Bstr(const std::pair& buf) + : mValue(buf.first, buf.first + buf.second) {} + + // Construct from a pair of iterators + template ::iterator_category, + typename = typename std::iterator_traits::iterator_category> + explicit Bstr(const std::pair& pair) : mValue(pair.first, pair.second) {} + + // Construct from an iterator range. + template ::iterator_category, + typename = typename std::iterator_traits::iterator_category> + Bstr(I1 begin, I2 end) : mValue(begin, end) {} + + bool operator==(const Bstr& other) const& { return mValue == other.mValue; } + + MajorType type() const override { return kMajorType; } + const Bstr* asBstr() const override { return this; } + size_t encodedSize() const override { return headerSize(mValue.size()) + mValue.size(); } + using Item::encode; + uint8_t* encode(uint8_t* pos, const uint8_t* end) const override; + void encode(EncodeCallback encodeCallback) const override { + encodeHeader(mValue.size(), encodeCallback); + encodeValue(encodeCallback); + } + + const std::vector& value() const { return mValue; } + + virtual std::unique_ptr clone() const override { return std::make_unique(mValue); } + + private: + void encodeValue(EncodeCallback encodeCallback) const; + + std::vector mValue; +}; + +/** + * Bstr is a concrete Item that implements major type 3. + */ +class Tstr : public Item { + public: + static constexpr MajorType kMajorType = TSTR; + + // Construct from a string + explicit Tstr(std::string v) : mValue(std::move(v)) {} + + // Construct from a string_view + explicit Tstr(const std::string_view& v) : mValue(v) {} + + // Construct from a C string + explicit Tstr(const char* v) : mValue(std::string(v)) {} + + // Construct from a pair of iterators + template ::iterator_category, + typename = typename std::iterator_traits::iterator_category> + explicit Tstr(const std::pair& pair) : mValue(pair.first, pair.second) {} + + // Construct from an iterator range + template ::iterator_category, + typename = typename std::iterator_traits::iterator_category> + Tstr(I1 begin, I2 end) : mValue(begin, end) {} + + bool operator==(const Tstr& other) const& { return mValue == other.mValue; } + + MajorType type() const override { return kMajorType; } + const Tstr* asTstr() const override { return this; } + size_t encodedSize() const override { return headerSize(mValue.size()) + mValue.size(); } + using Item::encode; + uint8_t* encode(uint8_t* pos, const uint8_t* end) const override; + void encode(EncodeCallback encodeCallback) const override { + encodeHeader(mValue.size(), encodeCallback); + encodeValue(encodeCallback); + } + + const std::string& value() const { return mValue; } + + virtual std::unique_ptr clone() const override { return std::make_unique(mValue); } + + private: + void encodeValue(EncodeCallback encodeCallback) const; + + std::string mValue; +}; + +/** + * CompoundItem is an abstract Item that provides common functionality for Items that contain other + * items, i.e. Arrays (CBOR type 4) and Maps (CBOR type 5). + */ +class CompoundItem : public Item { + public: + bool operator==(const CompoundItem& other) const&; + + virtual size_t size() const { return mEntries.size(); } + + bool isCompound() const override { return true; } + + size_t encodedSize() const override { + return std::accumulate(mEntries.begin(), mEntries.end(), headerSize(size()), + [](size_t sum, auto& entry) { return sum + entry->encodedSize(); }); + } + + using Item::encode; // Make base versions visible. + uint8_t* encode(uint8_t* pos, const uint8_t* end) const override; + void encode(EncodeCallback encodeCallback) const override; + + virtual uint64_t addlInfo() const = 0; + + protected: + std::vector> mEntries; +}; + +/* + * Array is a concrete Item that implements CBOR major type 4. + * + * Note that Arrays are not copyable. This is because copying them is expensive and making them + * move-only ensures that they're never copied accidentally. If you actually want to copy an Array, + * use the clone() method. + */ +class Array : public CompoundItem { + public: + static constexpr MajorType kMajorType = ARRAY; + + Array() = default; + Array(const Array& other) = delete; + Array(Array&&) = default; + Array& operator=(const Array&) = delete; + Array& operator=(Array&&) = default; + + /** + * Construct an Array from a variable number of arguments of different types. See + * details::makeItem below for details on what types may be provided. In general, this accepts + * all of the types you'd expect and doest the things you'd expect (integral values are addes as + * Uint or Nint, std::string and char* are added as Tstr, bools are added as Bool, etc.). + */ + template + Array(Args&&... args); + + /** + * Append a single element to the Array, of any compatible type. + */ + template + Array& add(T&& v) &; + template + Array&& add(T&& v) &&; + + const std::unique_ptr& operator[](size_t index) const { return mEntries[index]; } + std::unique_ptr& operator[](size_t index) { return mEntries[index]; } + + MajorType type() const override { return kMajorType; } + const Array* asArray() const override { return this; } + + virtual std::unique_ptr clone() const override; + + uint64_t addlInfo() const override { return size(); } +}; + +/* + * Map is a concrete Item that implements CBOR major type 5. + * + * Note that Maps are not copyable. This is because copying them is expensive and making them + * move-only ensures that they're never copied accidentally. If you actually want to copy a + * Map, use the clone() method. + */ +class Map : public CompoundItem { + public: + static constexpr MajorType kMajorType = MAP; + + Map() = default; + Map(const Map& other) = delete; + Map(Map&&) = default; + Map& operator=(const Map& other) = delete; + Map& operator=(Map&&) = default; + + /** + * Construct a Map from a variable number of arguments of different types. An even number of + * arguments must be provided (this is verified statically). See details::makeItem below for + * details on what types may be provided. In general, this accepts all of the types you'd + * expect and doest the things you'd expect (integral values are addes as Uint or Nint, + * std::string and char* are added as Tstr, bools are added as Bool, etc.). + */ + template + Map(Args&&... args); + + /** + * Append a key/value pair to the Map, of any compatible types. + */ + template + Map& add(Key&& key, Value&& value) &; + template + Map&& add(Key&& key, Value&& value) &&; + + size_t size() const override { + assertInvariant(); + return mEntries.size() / 2; + } + + template + std::pair&, bool> get(Key key); + + std::pair&, const std::unique_ptr&> operator[]( + size_t index) const { + assertInvariant(); + return {mEntries[index * 2], mEntries[index * 2 + 1]}; + } + + std::pair&, std::unique_ptr&> operator[](size_t index) { + assertInvariant(); + return {mEntries[index * 2], mEntries[index * 2 + 1]}; + } + + MajorType type() const override { return kMajorType; } + const Map* asMap() const override { return this; } + + virtual std::unique_ptr clone() const override; + + uint64_t addlInfo() const override { return size(); } + + private: + void assertInvariant() const; +}; + +class Semantic : public CompoundItem { + public: + static constexpr MajorType kMajorType = SEMANTIC; + + template + explicit Semantic(uint64_t value, T&& child); + + Semantic(const Semantic& other) = delete; + Semantic(Semantic&&) = default; + Semantic& operator=(const Semantic& other) = delete; + Semantic& operator=(Semantic&&) = default; + + size_t size() const override { + assertInvariant(); + return 1; + } + + size_t encodedSize() const override { + return std::accumulate(mEntries.begin(), mEntries.end(), headerSize(mValue), + [](size_t sum, auto& entry) { return sum + entry->encodedSize(); }); + } + + MajorType type() const override { return kMajorType; } + const Semantic* asSemantic() const override { return this; } + + const std::unique_ptr& child() const { + assertInvariant(); + return mEntries[0]; + } + + std::unique_ptr& child() { + assertInvariant(); + return mEntries[0]; + } + + uint64_t value() const { return mValue; } + + uint64_t addlInfo() const override { return value(); } + + virtual std::unique_ptr clone() const override { + assertInvariant(); + return std::make_unique(mValue, mEntries[0]->clone()); + } + + protected: + Semantic() = default; + Semantic(uint64_t value) : mValue(value) {} + uint64_t mValue; + + private: + void assertInvariant() const; +}; + +/** + * Simple is abstract Item that implements CBOR major type 7. It is intended to be subclassed to + * create concrete Simple types. At present only Bool is provided. + */ +class Simple : public Item { + public: + static constexpr MajorType kMajorType = SIMPLE; + + bool operator==(const Simple& other) const&; + + virtual SimpleType simpleType() const = 0; + MajorType type() const override { return kMajorType; } + + const Simple* asSimple() const override { return this; } + + virtual const Bool* asBool() const { return nullptr; }; + virtual const Null* asNull() const { return nullptr; }; +}; + +/** + * Bool is a concrete type that implements CBOR major type 7, with additional item values for TRUE + * and FALSE. + */ +class Bool : public Simple { + public: + static constexpr SimpleType kSimpleType = BOOLEAN; + + explicit Bool(bool v) : mValue(v) {} + + bool operator==(const Bool& other) const& { return mValue == other.mValue; } + + SimpleType simpleType() const override { return kSimpleType; } + const Bool* asBool() const override { return this; } + + size_t encodedSize() const override { return 1; } + + using Item::encode; + uint8_t* encode(uint8_t* pos, const uint8_t* end) const override { + return encodeHeader(mValue ? TRUE : FALSE, pos, end); + } + void encode(EncodeCallback encodeCallback) const override { + encodeHeader(mValue ? TRUE : FALSE, encodeCallback); + } + + bool value() const { return mValue; } + + virtual std::unique_ptr clone() const override { return std::make_unique(mValue); } + + private: + bool mValue; +}; + +/** + * Null is a concrete type that implements CBOR major type 7, with additional item value for NULL + */ +class Null : public Simple { + public: + static constexpr SimpleType kSimpleType = NULL_T; + + explicit Null() {} + + SimpleType simpleType() const override { return kSimpleType; } + const Null* asNull() const override { return this; } + + size_t encodedSize() const override { return 1; } + + using Item::encode; + uint8_t* encode(uint8_t* pos, const uint8_t* end) const override { + return encodeHeader(NULL_V, pos, end); + } + void encode(EncodeCallback encodeCallback) const override { + encodeHeader(NULL_V, encodeCallback); + } + + virtual std::unique_ptr clone() const override { return std::make_unique(); } +}; + +template +std::unique_ptr downcastItem(std::unique_ptr&& v) { + static_assert(std::is_base_of_v && !std::is_abstract_v, + "returned type is not an Item or is an abstract class"); + if (v && T::kMajorType == v->type()) { + if constexpr (std::is_base_of_v) { + if (T::kSimpleType != v->asSimple()->simpleType()) { + return nullptr; + } + } + return std::unique_ptr(static_cast(v.release())); + } else { + return nullptr; + } +} + +/** + * Details. Mostly you shouldn't have to look below, except perhaps at the docstring for makeItem. + */ +namespace details { + +template +struct is_iterator_pair_over : public std::false_type {}; + +template +struct is_iterator_pair_over< + std::pair, V, + typename std::enable_if_t::value_type>>> + : public std::true_type {}; + +template +struct is_unique_ptr_of_subclass_of_v : public std::false_type {}; + +template +struct is_unique_ptr_of_subclass_of_v, + typename std::enable_if_t>> + : public std::true_type {}; + +/* check if type is one of std::string (1), std::string_view (2), null-terminated char* (3) or pair + * of iterators (4)*/ +template +struct is_text_type_v : public std::false_type {}; + +template +struct is_text_type_v< + T, typename std::enable_if_t< + /* case 1 */ // + std::is_same_v>, std::string> + /* case 2 */ // + || std::is_same_v>, std::string_view> + /* case 3 */ // + || std::is_same_v>, char*> // + || std::is_same_v>, const char*> + /* case 4 */ + || details::is_iterator_pair_over::value>> : public std::true_type {}; + +/** + * Construct a unique_ptr from many argument types. Accepts: + * + * (a) booleans; + * (b) integers, all sizes and signs; + * (c) text strings, as defined by is_text_type_v above; + * (d) byte strings, as std::vector(d1), pair of iterators (d2) or pair + * (d3); and + * (e) Item subclass instances, including Array and Map. Items may be provided by naked pointer + * (e1), unique_ptr (e2), reference (e3) or value (e3). If provided by reference or value, will + * be moved if possible. If provided by pointer, ownership is taken. + * (f) null pointer; + */ +template +std::unique_ptr makeItem(T v) { + Item* p = nullptr; + if constexpr (/* case a */ std::is_same_v) { + p = new Bool(v); + } else if constexpr (/* case b */ std::is_integral_v) { // b + if (v < 0) { + p = new Nint(v); + } else { + p = new Uint(static_cast(v)); + } + } else if constexpr (/* case c */ // + details::is_text_type_v::value) { + p = new Tstr(v); + } else if constexpr (/* case d1 */ // + std::is_same_v>, + std::vector> + /* case d2 */ // + || details::is_iterator_pair_over::value + /* case d3 */ // + || std::is_same_v>, + std::pair>) { + p = new Bstr(v); + } else if constexpr (/* case e1 */ // + std::is_pointer_v && + std::is_base_of_v>) { + p = v; + } else if constexpr (/* case e2 */ // + details::is_unique_ptr_of_subclass_of_v::value) { + p = v.release(); + } else if constexpr (/* case e3 */ // + std::is_base_of_v) { + p = new T(std::move(v)); + } else if constexpr (/* case f */ std::is_null_pointer_v) { + p = new Null(); + } else { + // It's odd that this can't be static_assert(false), since it shouldn't be evaluated if one + // of the above ifs matches. But static_assert(false) always triggers. + static_assert(std::is_same_v, "makeItem called with unsupported type"); + } + return std::unique_ptr(p); +} + +} // namespace details + +template >> || ...)>> +Array::Array(Args&&... args) { + mEntries.reserve(sizeof...(args)); + (mEntries.push_back(details::makeItem(std::forward(args))), ...); +} + +template +Array& Array::add(T&& v) & { + mEntries.push_back(details::makeItem(std::forward(v))); + return *this; +} + +template +Array&& Array::add(T&& v) && { + mEntries.push_back(details::makeItem(std::forward(v))); + return std::move(*this); +} + +template > +Map::Map(Args&&... args) { + static_assert((sizeof...(Args)) % 2 == 0, "Map must have an even number of entries"); + mEntries.reserve(sizeof...(args)); + (mEntries.push_back(details::makeItem(std::forward(args))), ...); +} + +template +Map& Map::add(Key&& key, Value&& value) & { + mEntries.push_back(details::makeItem(std::forward(key))); + mEntries.push_back(details::makeItem(std::forward(value))); + return *this; +} + +template +Map&& Map::add(Key&& key, Value&& value) && { + this->add(std::forward(key), std::forward(value)); + return std::move(*this); +} + +template || + details::is_text_type_v::value>> +std::pair&, bool> Map::get(Key key) { + assertInvariant(); + auto keyItem = details::makeItem(key); + for (size_t i = 0; i < mEntries.size(); i += 2) { + if (*keyItem == *mEntries[i]) { + return {mEntries[i + 1], true}; + } + } + return {keyItem, false}; +} + +template +Semantic::Semantic(uint64_t value, T&& child) : mValue(value) { + mEntries.reserve(1); + mEntries.push_back(details::makeItem(std::forward(child))); +} + +} // namespace cppbor diff --git a/identity/support/include/cppbor/cppbor_parse.h b/identity/support/include/cppbor/cppbor_parse.h new file mode 100644 index 0000000000..66cd5a3d62 --- /dev/null +++ b/identity/support/include/cppbor/cppbor_parse.h @@ -0,0 +1,133 @@ +/* + * 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. + */ + +#pragma once + +#include "cppbor.h" + +namespace cppbor { + +using ParseResult = std::tuple /* result */, const uint8_t* /* newPos */, + std::string /* errMsg */>; + +/** + * Parse the first CBOR data item (possibly compound) from the range [begin, end). + * + * Returns a tuple of Item pointer, buffer pointer and error message. If parsing is successful, the + * Item pointer is non-null, the buffer pointer points to the first byte after the + * successfully-parsed item and the error message string is empty. If parsing fails, the Item + * pointer is null, the buffer pointer points to the first byte that was unparseable (the first byte + * of a data item header that is malformed in some way, e.g. an invalid value, or a length that is + * too large for the remining buffer, etc.) and the string contains an error message describing the + * problem encountered. + */ +ParseResult parse(const uint8_t* begin, const uint8_t* end); + +/** + * Parse the first CBOR data item (possibly compound) from the byte vector. + * + * Returns a tuple of Item pointer, buffer pointer and error message. If parsing is successful, the + * Item pointer is non-null, the buffer pointer points to the first byte after the + * successfully-parsed item and the error message string is empty. If parsing fails, the Item + * pointer is null, the buffer pointer points to the first byte that was unparseable (the first byte + * of a data item header that is malformed in some way, e.g. an invalid value, or a length that is + * too large for the remining buffer, etc.) and the string contains an error message describing the + * problem encountered. + */ +inline ParseResult parse(const std::vector& encoding) { + return parse(encoding.data(), encoding.data() + encoding.size()); +} + +/** + * Parse the first CBOR data item (possibly compound) from the range [begin, begin + size). + * + * Returns a tuple of Item pointer, buffer pointer and error message. If parsing is successful, the + * Item pointer is non-null, the buffer pointer points to the first byte after the + * successfully-parsed item and the error message string is empty. If parsing fails, the Item + * pointer is null, the buffer pointer points to the first byte that was unparseable (the first byte + * of a data item header that is malformed in some way, e.g. an invalid value, or a length that is + * too large for the remining buffer, etc.) and the string contains an error message describing the + * problem encountered. + */ +inline ParseResult parse(const uint8_t* begin, size_t size) { + return parse(begin, begin + size); +} + +class ParseClient; + +/** + * Parse the CBOR data in the range [begin, end) in streaming fashion, calling methods on the + * provided ParseClient when elements are found. + */ +void parse(const uint8_t* begin, const uint8_t* end, ParseClient* parseClient); + +/** + * Parse the CBOR data in the vector in streaming fashion, calling methods on the + * provided ParseClient when elements are found. + */ +inline void parse(const std::vector& encoding, ParseClient* parseClient) { + return parse(encoding.data(), encoding.data() + encoding.size(), parseClient); +} + +/** + * A pure interface that callers of the streaming parse functions must implement. + */ +class ParseClient { + public: + virtual ~ParseClient() {} + + /** + * Called when an item is found. The Item pointer points to the found item; use type() and + * the appropriate as*() method to examine the value. hdrBegin points to the first byte of the + * header, valueBegin points to the first byte of the value and end points one past the end of + * the item. In the case of header-only items, such as integers, and compound items (ARRAY, + * MAP or SEMANTIC) whose end has not yet been found, valueBegin and end are equal and point to + * the byte past the header. + * + * Note that for compound types (ARRAY, MAP, and SEMANTIC), the Item will have no content. For + * Map and Array items, the size() method will return a correct value, but the index operators + * are unsafe, and the object cannot be safely compared with another Array/Map. + * + * The method returns a ParseClient*. In most cases "return this;" will be the right answer, + * but a different ParseClient may be returned, which the parser will begin using. If the method + * returns nullptr, parsing will be aborted immediately. + */ + virtual ParseClient* item(std::unique_ptr& item, const uint8_t* hdrBegin, + const uint8_t* valueBegin, const uint8_t* end) = 0; + + /** + * Called when the end of a compound item (MAP or ARRAY) is found. The item argument will be + * the same one passed to the item() call -- and may be empty if item() moved its value out. + * hdrBegin, valueBegin and end point to the beginning of the item header, the beginning of the + * first contained value, and one past the end of the last contained value, respectively. + * + * Note that the Item will have no content. + * + * As with item(), itemEnd() can change the ParseClient by returning a different one, or end the + * parsing by returning nullptr; + */ + virtual ParseClient* itemEnd(std::unique_ptr& item, const uint8_t* hdrBegin, + const uint8_t* valueBegin, const uint8_t* end) = 0; + + /** + * Called when parsing encounters an error. position is set to the first unparsed byte (one + * past the last successfully-parsed byte) and errorMessage contains an message explaining what + * sort of error occurred. + */ + virtual void error(const uint8_t* position, const std::string& errorMessage) = 0; +}; + +} // namespace cppbor diff --git a/identity/support/src/IdentityCredentialSupport.cpp b/identity/support/src/IdentityCredentialSupport.cpp new file mode 100644 index 0000000000..7d93a4b737 --- /dev/null +++ b/identity/support/src/IdentityCredentialSupport.cpp @@ -0,0 +1,1815 @@ +/* + * 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 "IdentityCredentialSupport" + +#include + +#define _POSIX_C_SOURCE 199309L + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace android { +namespace hardware { +namespace identity { +namespace support { + +using ::std::pair; +using ::std::unique_ptr; + +// --------------------------------------------------------------------------- +// Miscellaneous utilities. +// --------------------------------------------------------------------------- + +void hexdump(const string& name, const vector& data) { + fprintf(stderr, "%s: dumping %zd bytes\n", name.c_str(), data.size()); + size_t n, m, o; + for (n = 0; n < data.size(); n += 16) { + fprintf(stderr, "%04zx ", n); + for (m = 0; m < 16 && n + m < data.size(); m++) { + fprintf(stderr, "%02x ", data[n + m]); + } + for (o = m; o < 16; o++) { + fprintf(stderr, " "); + } + fprintf(stderr, " "); + for (m = 0; m < 16 && n + m < data.size(); m++) { + int c = data[n + m]; + fprintf(stderr, "%c", isprint(c) ? c : '.'); + } + fprintf(stderr, "\n"); + } + fprintf(stderr, "\n"); +} + +string encodeHex(const uint8_t* data, size_t dataLen) { + static const char hexDigits[16] = {'0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + string ret; + ret.resize(dataLen * 2); + for (size_t n = 0; n < dataLen; n++) { + uint8_t byte = data[n]; + ret[n * 2 + 0] = hexDigits[byte >> 4]; + ret[n * 2 + 1] = hexDigits[byte & 0x0f]; + } + + return ret; +} + +string encodeHex(const string& str) { + return encodeHex(reinterpret_cast(str.data()), str.size()); +} + +string encodeHex(const vector& data) { + return encodeHex(data.data(), data.size()); +} + +// Returns -1 on error, otherwise an integer in the range 0 through 15, both inclusive. +int parseHexDigit(char hexDigit) { + if (hexDigit >= '0' && hexDigit <= '9') { + return int(hexDigit) - '0'; + } else if (hexDigit >= 'a' && hexDigit <= 'f') { + return int(hexDigit) - 'a' + 10; + } else if (hexDigit >= 'A' && hexDigit <= 'F') { + return int(hexDigit) - 'A' + 10; + } + return -1; +} + +optional> decodeHex(const string& hexEncoded) { + vector out; + size_t hexSize = hexEncoded.size(); + if ((hexSize & 1) != 0) { + LOG(ERROR) << "Size of data cannot be odd"; + return {}; + } + + out.resize(hexSize / 2); + for (size_t n = 0; n < hexSize / 2; n++) { + int upperNibble = parseHexDigit(hexEncoded[n * 2]); + int lowerNibble = parseHexDigit(hexEncoded[n * 2 + 1]); + if (upperNibble == -1 || lowerNibble == -1) { + LOG(ERROR) << "Invalid hex digit at position " << n; + return {}; + } + out[n] = (upperNibble << 4) + lowerNibble; + } + + return out; +} + +// --------------------------------------------------------------------------- +// CBOR utilities. +// --------------------------------------------------------------------------- + +static bool cborAreAllElementsNonCompound(const cppbor::CompoundItem* compoundItem) { + if (compoundItem->type() == cppbor::ARRAY) { + const cppbor::Array* array = compoundItem->asArray(); + for (size_t n = 0; n < array->size(); n++) { + const cppbor::Item* entry = (*array)[n].get(); + switch (entry->type()) { + case cppbor::ARRAY: + case cppbor::MAP: + return false; + default: + break; + } + } + } else { + const cppbor::Map* map = compoundItem->asMap(); + for (size_t n = 0; n < map->size(); n++) { + auto [keyEntry, valueEntry] = (*map)[n]; + switch (keyEntry->type()) { + case cppbor::ARRAY: + case cppbor::MAP: + return false; + default: + break; + } + switch (valueEntry->type()) { + case cppbor::ARRAY: + case cppbor::MAP: + return false; + default: + break; + } + } + } + return true; +} + +static bool cborPrettyPrintInternal(const cppbor::Item* item, string& out, size_t indent, + size_t maxBStrSize, const vector& mapKeysToNotPrint) { + char buf[80]; + + string indentString(indent, ' '); + + switch (item->type()) { + case cppbor::UINT: + snprintf(buf, sizeof(buf), "%" PRIu64, item->asUint()->unsignedValue()); + out.append(buf); + break; + + case cppbor::NINT: + snprintf(buf, sizeof(buf), "%" PRId64, item->asNint()->value()); + out.append(buf); + break; + + case cppbor::BSTR: { + const cppbor::Bstr* bstr = item->asBstr(); + const vector& value = bstr->value(); + if (value.size() > maxBStrSize) { + unsigned char digest[SHA_DIGEST_LENGTH]; + SHA_CTX ctx; + SHA1_Init(&ctx); + SHA1_Update(&ctx, value.data(), value.size()); + SHA1_Final(digest, &ctx); + char buf2[SHA_DIGEST_LENGTH * 2 + 1]; + for (size_t n = 0; n < SHA_DIGEST_LENGTH; n++) { + snprintf(buf2 + n * 2, 3, "%02x", digest[n]); + } + snprintf(buf, sizeof(buf), "", value.size(), buf2); + out.append(buf); + } else { + out.append("{"); + for (size_t n = 0; n < value.size(); n++) { + if (n > 0) { + out.append(", "); + } + snprintf(buf, sizeof(buf), "0x%02x", value[n]); + out.append(buf); + } + out.append("}"); + } + } break; + + case cppbor::TSTR: + out.append("'"); + { + // TODO: escape "'" characters + out.append(item->asTstr()->value().c_str()); + } + out.append("'"); + break; + + case cppbor::ARRAY: { + const cppbor::Array* array = item->asArray(); + if (array->size() == 0) { + out.append("[]"); + } else if (cborAreAllElementsNonCompound(array)) { + out.append("["); + for (size_t n = 0; n < array->size(); n++) { + if (!cborPrettyPrintInternal((*array)[n].get(), out, indent + 2, maxBStrSize, + mapKeysToNotPrint)) { + return false; + } + out.append(", "); + } + out.append("]"); + } else { + out.append("[\n" + indentString); + for (size_t n = 0; n < array->size(); n++) { + out.append(" "); + if (!cborPrettyPrintInternal((*array)[n].get(), out, indent + 2, maxBStrSize, + mapKeysToNotPrint)) { + return false; + } + out.append(",\n" + indentString); + } + out.append("]"); + } + } break; + + case cppbor::MAP: { + const cppbor::Map* map = item->asMap(); + + if (map->size() == 0) { + out.append("{}"); + } else { + out.append("{\n" + indentString); + for (size_t n = 0; n < map->size(); n++) { + out.append(" "); + + auto [map_key, map_value] = (*map)[n]; + + if (!cborPrettyPrintInternal(map_key.get(), out, indent + 2, maxBStrSize, + mapKeysToNotPrint)) { + return false; + } + out.append(" : "); + if (map_key->type() == cppbor::TSTR && + std::find(mapKeysToNotPrint.begin(), mapKeysToNotPrint.end(), + map_key->asTstr()->value()) != mapKeysToNotPrint.end()) { + out.append(""); + } else { + if (!cborPrettyPrintInternal(map_value.get(), out, indent + 2, maxBStrSize, + mapKeysToNotPrint)) { + return false; + } + } + out.append(",\n" + indentString); + } + out.append("}"); + } + } break; + + case cppbor::SEMANTIC: { + const cppbor::Semantic* semantic = item->asSemantic(); + snprintf(buf, sizeof(buf), "tag %" PRIu64 " ", semantic->value()); + out.append(buf); + cborPrettyPrintInternal(semantic->child().get(), out, indent, maxBStrSize, + mapKeysToNotPrint); + } break; + + case cppbor::SIMPLE: + const cppbor::Bool* asBool = item->asSimple()->asBool(); + const cppbor::Null* asNull = item->asSimple()->asNull(); + if (asBool != nullptr) { + out.append(asBool->value() ? "true" : "false"); + } else if (asNull != nullptr) { + out.append("null"); + } else { + LOG(ERROR) << "Only boolean/null is implemented for SIMPLE"; + return false; + } + break; + } + + return true; +} + +string cborPrettyPrint(const vector& encodedCbor, size_t maxBStrSize, + const vector& mapKeysToNotPrint) { + auto [item, _, message] = cppbor::parse(encodedCbor); + if (item == nullptr) { + LOG(ERROR) << "Data to pretty print is not valid CBOR: " << message; + return ""; + } + + string out; + cborPrettyPrintInternal(item.get(), out, 0, maxBStrSize, mapKeysToNotPrint); + return out; +} + +// --------------------------------------------------------------------------- +// Crypto functionality / abstraction. +// --------------------------------------------------------------------------- + +struct EVP_CIPHER_CTX_Deleter { + void operator()(EVP_CIPHER_CTX* ctx) const { + if (ctx != nullptr) { + EVP_CIPHER_CTX_free(ctx); + } + } +}; + +using EvpCipherCtxPtr = unique_ptr; + +// bool getRandom(size_t numBytes, vector& output) { +optional> getRandom(size_t numBytes) { + vector output; + output.resize(numBytes); + if (RAND_bytes(output.data(), numBytes) != 1) { + LOG(ERROR) << "RAND_bytes: failed getting " << numBytes << " random"; + return {}; + } + return output; +} + +optional> decryptAes128Gcm(const vector& key, + const vector& encryptedData, + const vector& additionalAuthenticatedData) { + int cipherTextSize = int(encryptedData.size()) - kAesGcmIvSize - kAesGcmTagSize; + if (cipherTextSize < 0) { + LOG(ERROR) << "encryptedData too small"; + return {}; + } + unsigned char* nonce = (unsigned char*)encryptedData.data(); + unsigned char* cipherText = nonce + kAesGcmIvSize; + unsigned char* tag = cipherText + cipherTextSize; + + vector plainText; + plainText.resize(cipherTextSize); + + auto ctx = EvpCipherCtxPtr(EVP_CIPHER_CTX_new()); + if (ctx.get() == nullptr) { + LOG(ERROR) << "EVP_CIPHER_CTX_new: failed"; + return {}; + } + + if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_gcm(), NULL, NULL, NULL) != 1) { + LOG(ERROR) << "EVP_DecryptInit_ex: failed"; + return {}; + } + + if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, kAesGcmIvSize, NULL) != 1) { + LOG(ERROR) << "EVP_CIPHER_CTX_ctrl: failed setting nonce length"; + return {}; + } + + if (EVP_DecryptInit_ex(ctx.get(), NULL, NULL, (unsigned char*)key.data(), nonce) != 1) { + LOG(ERROR) << "EVP_DecryptInit_ex: failed"; + return {}; + } + + int numWritten; + if (additionalAuthenticatedData.size() > 0) { + if (EVP_DecryptUpdate(ctx.get(), NULL, &numWritten, + (unsigned char*)additionalAuthenticatedData.data(), + additionalAuthenticatedData.size()) != 1) { + LOG(ERROR) << "EVP_DecryptUpdate: failed for additionalAuthenticatedData"; + return {}; + } + if ((size_t)numWritten != additionalAuthenticatedData.size()) { + LOG(ERROR) << "EVP_DecryptUpdate: Unexpected outl=" << numWritten << " (expected " + << additionalAuthenticatedData.size() << ") for additionalAuthenticatedData"; + return {}; + } + } + + if (EVP_DecryptUpdate(ctx.get(), (unsigned char*)plainText.data(), &numWritten, cipherText, + cipherTextSize) != 1) { + LOG(ERROR) << "EVP_DecryptUpdate: failed"; + return {}; + } + if (numWritten != cipherTextSize) { + LOG(ERROR) << "EVP_DecryptUpdate: Unexpected outl=" << numWritten << " (expected " + << cipherTextSize << ")"; + return {}; + } + + if (!EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_TAG, kAesGcmTagSize, tag)) { + LOG(ERROR) << "EVP_CIPHER_CTX_ctrl: failed setting expected tag"; + return {}; + } + + int ret = EVP_DecryptFinal_ex(ctx.get(), (unsigned char*)plainText.data() + numWritten, + &numWritten); + if (ret != 1) { + LOG(ERROR) << "EVP_DecryptFinal_ex: failed"; + return {}; + } + if (numWritten != 0) { + LOG(ERROR) << "EVP_DecryptFinal_ex: Unexpected non-zero outl=" << numWritten; + return {}; + } + + return plainText; +} + +optional> encryptAes128Gcm(const vector& key, const vector& nonce, + const vector& data, + const vector& additionalAuthenticatedData) { + if (key.size() != kAes128GcmKeySize) { + LOG(ERROR) << "key is not kAes128GcmKeySize bytes"; + return {}; + } + if (nonce.size() != kAesGcmIvSize) { + LOG(ERROR) << "nonce is not kAesGcmIvSize bytes"; + return {}; + } + + // The result is the nonce (kAesGcmIvSize bytes), the ciphertext, and + // finally the tag (kAesGcmTagSize bytes). + vector encryptedData; + encryptedData.resize(data.size() + kAesGcmIvSize + kAesGcmTagSize); + unsigned char* noncePtr = (unsigned char*)encryptedData.data(); + unsigned char* cipherText = noncePtr + kAesGcmIvSize; + unsigned char* tag = cipherText + data.size(); + memcpy(noncePtr, nonce.data(), kAesGcmIvSize); + + auto ctx = EvpCipherCtxPtr(EVP_CIPHER_CTX_new()); + if (ctx.get() == nullptr) { + LOG(ERROR) << "EVP_CIPHER_CTX_new: failed"; + return {}; + } + + if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_gcm(), NULL, NULL, NULL) != 1) { + LOG(ERROR) << "EVP_EncryptInit_ex: failed"; + return {}; + } + + if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, kAesGcmIvSize, NULL) != 1) { + LOG(ERROR) << "EVP_CIPHER_CTX_ctrl: failed setting nonce length"; + return {}; + } + + if (EVP_EncryptInit_ex(ctx.get(), NULL, NULL, (unsigned char*)key.data(), + (unsigned char*)nonce.data()) != 1) { + LOG(ERROR) << "EVP_EncryptInit_ex: failed"; + return {}; + } + + int numWritten; + if (additionalAuthenticatedData.size() > 0) { + if (EVP_EncryptUpdate(ctx.get(), NULL, &numWritten, + (unsigned char*)additionalAuthenticatedData.data(), + additionalAuthenticatedData.size()) != 1) { + LOG(ERROR) << "EVP_EncryptUpdate: failed for additionalAuthenticatedData"; + return {}; + } + if ((size_t)numWritten != additionalAuthenticatedData.size()) { + LOG(ERROR) << "EVP_EncryptUpdate: Unexpected outl=" << numWritten << " (expected " + << additionalAuthenticatedData.size() << ") for additionalAuthenticatedData"; + return {}; + } + } + + if (data.size() > 0) { + if (EVP_EncryptUpdate(ctx.get(), cipherText, &numWritten, (unsigned char*)data.data(), + data.size()) != 1) { + LOG(ERROR) << "EVP_EncryptUpdate: failed"; + return {}; + } + if ((size_t)numWritten != data.size()) { + LOG(ERROR) << "EVP_EncryptUpdate: Unexpected outl=" << numWritten << " (expected " + << data.size() << ")"; + return {}; + } + } + + if (EVP_EncryptFinal_ex(ctx.get(), cipherText + numWritten, &numWritten) != 1) { + LOG(ERROR) << "EVP_EncryptFinal_ex: failed"; + return {}; + } + if (numWritten != 0) { + LOG(ERROR) << "EVP_EncryptFinal_ex: Unexpected non-zero outl=" << numWritten; + return {}; + } + + if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_GET_TAG, kAesGcmTagSize, tag) != 1) { + LOG(ERROR) << "EVP_CIPHER_CTX_ctrl: failed getting tag"; + return {}; + } + + return encryptedData; +} + +struct EC_KEY_Deleter { + void operator()(EC_KEY* key) const { + if (key != nullptr) { + EC_KEY_free(key); + } + } +}; +using EC_KEY_Ptr = unique_ptr; + +struct EVP_PKEY_Deleter { + void operator()(EVP_PKEY* key) const { + if (key != nullptr) { + EVP_PKEY_free(key); + } + } +}; +using EVP_PKEY_Ptr = unique_ptr; + +struct EVP_PKEY_CTX_Deleter { + void operator()(EVP_PKEY_CTX* ctx) const { + if (ctx != nullptr) { + EVP_PKEY_CTX_free(ctx); + } + } +}; +using EVP_PKEY_CTX_Ptr = unique_ptr; + +struct EC_GROUP_Deleter { + void operator()(EC_GROUP* group) const { + if (group != nullptr) { + EC_GROUP_free(group); + } + } +}; +using EC_GROUP_Ptr = unique_ptr; + +struct EC_POINT_Deleter { + void operator()(EC_POINT* point) const { + if (point != nullptr) { + EC_POINT_free(point); + } + } +}; + +using EC_POINT_Ptr = unique_ptr; + +struct ECDSA_SIG_Deleter { + void operator()(ECDSA_SIG* sig) const { + if (sig != nullptr) { + ECDSA_SIG_free(sig); + } + } +}; +using ECDSA_SIG_Ptr = unique_ptr; + +struct X509_Deleter { + void operator()(X509* x509) const { + if (x509 != nullptr) { + X509_free(x509); + } + } +}; +using X509_Ptr = unique_ptr; + +struct PKCS12_Deleter { + void operator()(PKCS12* pkcs12) const { + if (pkcs12 != nullptr) { + PKCS12_free(pkcs12); + } + } +}; +using PKCS12_Ptr = unique_ptr; + +struct BIGNUM_Deleter { + void operator()(BIGNUM* bignum) const { + if (bignum != nullptr) { + BN_free(bignum); + } + } +}; +using BIGNUM_Ptr = unique_ptr; + +struct ASN1_INTEGER_Deleter { + void operator()(ASN1_INTEGER* value) const { + if (value != nullptr) { + ASN1_INTEGER_free(value); + } + } +}; +using ASN1_INTEGER_Ptr = unique_ptr; + +struct ASN1_TIME_Deleter { + void operator()(ASN1_TIME* value) const { + if (value != nullptr) { + ASN1_TIME_free(value); + } + } +}; +using ASN1_TIME_Ptr = unique_ptr; + +struct X509_NAME_Deleter { + void operator()(X509_NAME* value) const { + if (value != nullptr) { + X509_NAME_free(value); + } + } +}; +using X509_NAME_Ptr = unique_ptr; + +vector certificateChainJoin(const vector>& certificateChain) { + vector ret; + for (const vector& certificate : certificateChain) { + ret.insert(ret.end(), certificate.begin(), certificate.end()); + } + return ret; +} + +optional>> certificateChainSplit(const vector& certificateChain) { + const unsigned char* pStart = (unsigned char*)certificateChain.data(); + const unsigned char* p = pStart; + const unsigned char* pEnd = p + certificateChain.size(); + vector> certificates; + while (p < pEnd) { + size_t begin = p - pStart; + auto x509 = X509_Ptr(d2i_X509(nullptr, &p, pEnd - p)); + size_t next = p - pStart; + if (x509 == nullptr) { + LOG(ERROR) << "Error parsing X509 certificate"; + return {}; + } + vector cert = + vector(certificateChain.begin() + begin, certificateChain.begin() + next); + certificates.push_back(std::move(cert)); + } + return certificates; +} + +static bool parseX509Certificates(const vector& certificateChain, + vector& parsedCertificates) { + const unsigned char* p = (unsigned char*)certificateChain.data(); + const unsigned char* pEnd = p + certificateChain.size(); + parsedCertificates.resize(0); + while (p < pEnd) { + auto x509 = X509_Ptr(d2i_X509(nullptr, &p, pEnd - p)); + if (x509 == nullptr) { + LOG(ERROR) << "Error parsing X509 certificate"; + return false; + } + parsedCertificates.push_back(std::move(x509)); + } + return true; +} + +// TODO: Right now the only check we perform is to check that each certificate +// is signed by its successor. We should - but currently don't - also check +// things like valid dates etc. +// +// It would be nice to use X509_verify_cert() instead of doing our own thing. +// +bool certificateChainValidate(const vector& certificateChain) { + vector certs; + + if (!parseX509Certificates(certificateChain, certs)) { + LOG(ERROR) << "Error parsing X509 certificates"; + return false; + } + + if (certs.size() == 1) { + return true; + } + + for (size_t n = 1; n < certs.size(); n++) { + const X509_Ptr& keyCert = certs[n - 1]; + const X509_Ptr& signingCert = certs[n]; + EVP_PKEY_Ptr signingPubkey(X509_get_pubkey(signingCert.get())); + if (X509_verify(keyCert.get(), signingPubkey.get()) != 1) { + LOG(ERROR) << "Error validating cert at index " << n - 1 + << " is signed by its successor"; + return false; + } + } + + return true; +} + +bool checkEcDsaSignature(const vector& digest, const vector& signature, + const vector& publicKey) { + const unsigned char* p = (unsigned char*)signature.data(); + auto sig = ECDSA_SIG_Ptr(d2i_ECDSA_SIG(nullptr, &p, signature.size())); + if (sig.get() == nullptr) { + LOG(ERROR) << "Error decoding DER encoded signature"; + return false; + } + + auto group = EC_GROUP_Ptr(EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)); + auto point = EC_POINT_Ptr(EC_POINT_new(group.get())); + if (EC_POINT_oct2point(group.get(), point.get(), publicKey.data(), publicKey.size(), nullptr) != + 1) { + LOG(ERROR) << "Error decoding publicKey"; + return false; + } + auto ecKey = EC_KEY_Ptr(EC_KEY_new()); + auto pkey = EVP_PKEY_Ptr(EVP_PKEY_new()); + if (ecKey.get() == nullptr || pkey.get() == nullptr) { + LOG(ERROR) << "Memory allocation failed"; + return false; + } + if (EC_KEY_set_group(ecKey.get(), group.get()) != 1) { + LOG(ERROR) << "Error setting group"; + return false; + } + if (EC_KEY_set_public_key(ecKey.get(), point.get()) != 1) { + LOG(ERROR) << "Error setting point"; + return false; + } + if (EVP_PKEY_set1_EC_KEY(pkey.get(), ecKey.get()) != 1) { + LOG(ERROR) << "Error setting key"; + return false; + } + + int rc = ECDSA_do_verify(digest.data(), digest.size(), sig.get(), ecKey.get()); + if (rc != 1) { + LOG(ERROR) << "Error verifying signature (rc=" << rc << ")"; + return false; + } + + return true; +} + +vector sha256(const vector& data) { + vector ret; + ret.resize(SHA256_DIGEST_LENGTH); + SHA256_CTX ctx; + SHA256_Init(&ctx); + SHA256_Update(&ctx, data.data(), data.size()); + SHA256_Final((unsigned char*)ret.data(), &ctx); + return ret; +} + +optional> signEcDsa(const vector& key, const vector& data) { + auto bn = BIGNUM_Ptr(BN_bin2bn(key.data(), key.size(), nullptr)); + if (bn.get() == nullptr) { + LOG(ERROR) << "Error creating BIGNUM"; + return {}; + } + + auto ec_key = EC_KEY_Ptr(EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)); + if (EC_KEY_set_private_key(ec_key.get(), bn.get()) != 1) { + LOG(ERROR) << "Error setting private key from BIGNUM"; + return {}; + } + + auto digest = sha256(data); + ECDSA_SIG* sig = ECDSA_do_sign(digest.data(), digest.size(), ec_key.get()); + if (sig == nullptr) { + LOG(ERROR) << "Error signing digest"; + return {}; + } + size_t len = i2d_ECDSA_SIG(sig, nullptr); + vector signature; + signature.resize(len); + unsigned char* p = (unsigned char*)signature.data(); + i2d_ECDSA_SIG(sig, &p); + ECDSA_SIG_free(sig); + return signature; +} + +optional> hmacSha256(const vector& key, const vector& data) { + HMAC_CTX ctx; + HMAC_CTX_init(&ctx); + if (HMAC_Init_ex(&ctx, key.data(), key.size(), EVP_sha256(), nullptr /* impl */) != 1) { + LOG(ERROR) << "Error initializing HMAC_CTX"; + return {}; + } + if (HMAC_Update(&ctx, data.data(), data.size()) != 1) { + LOG(ERROR) << "Error updating HMAC_CTX"; + return {}; + } + vector hmac; + hmac.resize(32); + unsigned int size = 0; + if (HMAC_Final(&ctx, hmac.data(), &size) != 1) { + LOG(ERROR) << "Error finalizing HMAC_CTX"; + return {}; + } + if (size != 32) { + LOG(ERROR) << "Expected 32 bytes from HMAC_Final, got " << size; + return {}; + } + return hmac; +} + +optional> createEcKeyPair() { + auto ec_key = EC_KEY_Ptr(EC_KEY_new()); + auto pkey = EVP_PKEY_Ptr(EVP_PKEY_new()); + auto group = EC_GROUP_Ptr(EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)); + if (ec_key.get() == nullptr || pkey.get() == nullptr) { + LOG(ERROR) << "Memory allocation failed"; + return {}; + } + + if (EC_KEY_set_group(ec_key.get(), group.get()) != 1 || + EC_KEY_generate_key(ec_key.get()) != 1 || EC_KEY_check_key(ec_key.get()) < 0) { + LOG(ERROR) << "Error generating key"; + return {}; + } + + if (EVP_PKEY_set1_EC_KEY(pkey.get(), ec_key.get()) != 1) { + LOG(ERROR) << "Error getting private key"; + return {}; + } + + int size = i2d_PrivateKey(pkey.get(), nullptr); + if (size == 0) { + LOG(ERROR) << "Error generating public key encoding"; + return {}; + } + vector keyPair; + keyPair.resize(size); + unsigned char* p = keyPair.data(); + i2d_PrivateKey(pkey.get(), &p); + return keyPair; +} + +optional> ecKeyPairGetPublicKey(const vector& keyPair) { + const unsigned char* p = (const unsigned char*)keyPair.data(); + auto pkey = EVP_PKEY_Ptr(d2i_PrivateKey(EVP_PKEY_EC, nullptr, &p, keyPair.size())); + if (pkey.get() == nullptr) { + LOG(ERROR) << "Error parsing keyPair"; + return {}; + } + + auto ecKey = EC_KEY_Ptr(EVP_PKEY_get1_EC_KEY(pkey.get())); + if (ecKey.get() == nullptr) { + LOG(ERROR) << "Failed getting EC key"; + return {}; + } + + auto ecGroup = EC_KEY_get0_group(ecKey.get()); + auto ecPoint = EC_KEY_get0_public_key(ecKey.get()); + int size = EC_POINT_point2oct(ecGroup, ecPoint, POINT_CONVERSION_UNCOMPRESSED, nullptr, 0, + nullptr); + if (size == 0) { + LOG(ERROR) << "Error generating public key encoding"; + return {}; + } + + vector publicKey; + publicKey.resize(size); + EC_POINT_point2oct(ecGroup, ecPoint, POINT_CONVERSION_UNCOMPRESSED, publicKey.data(), + publicKey.size(), nullptr); + return publicKey; +} + +optional> ecKeyPairGetPrivateKey(const vector& keyPair) { + const unsigned char* p = (const unsigned char*)keyPair.data(); + auto pkey = EVP_PKEY_Ptr(d2i_PrivateKey(EVP_PKEY_EC, nullptr, &p, keyPair.size())); + if (pkey.get() == nullptr) { + LOG(ERROR) << "Error parsing keyPair"; + return {}; + } + + auto ecKey = EC_KEY_Ptr(EVP_PKEY_get1_EC_KEY(pkey.get())); + if (ecKey.get() == nullptr) { + LOG(ERROR) << "Failed getting EC key"; + return {}; + } + + const BIGNUM* bignum = EC_KEY_get0_private_key(ecKey.get()); + if (bignum == nullptr) { + LOG(ERROR) << "Error getting bignum from private key"; + return {}; + } + vector privateKey; + privateKey.resize(BN_num_bytes(bignum)); + BN_bn2bin(bignum, privateKey.data()); + return privateKey; +} + +optional> ecKeyPairGetPkcs12(const vector& keyPair, const string& name, + const string& serialDecimal, const string& issuer, + const string& subject, time_t validityNotBefore, + time_t validityNotAfter) { + const unsigned char* p = (const unsigned char*)keyPair.data(); + auto pkey = EVP_PKEY_Ptr(d2i_PrivateKey(EVP_PKEY_EC, nullptr, &p, keyPair.size())); + if (pkey.get() == nullptr) { + LOG(ERROR) << "Error parsing keyPair"; + return {}; + } + + auto x509 = X509_Ptr(X509_new()); + if (!x509.get()) { + LOG(ERROR) << "Error creating X509 certificate"; + return {}; + } + + if (!X509_set_version(x509.get(), 2 /* version 3, but zero-based */)) { + LOG(ERROR) << "Error setting version to 3"; + return {}; + } + + if (X509_set_pubkey(x509.get(), pkey.get()) != 1) { + LOG(ERROR) << "Error setting public key"; + return {}; + } + + BIGNUM* bignumSerial = nullptr; + if (BN_dec2bn(&bignumSerial, serialDecimal.c_str()) == 0) { + LOG(ERROR) << "Error parsing serial"; + return {}; + } + auto bignumSerialPtr = BIGNUM_Ptr(bignumSerial); + auto asnSerial = ASN1_INTEGER_Ptr(BN_to_ASN1_INTEGER(bignumSerial, nullptr)); + if (X509_set_serialNumber(x509.get(), asnSerial.get()) != 1) { + LOG(ERROR) << "Error setting serial"; + return {}; + } + + auto x509Issuer = X509_NAME_Ptr(X509_NAME_new()); + if (x509Issuer.get() == nullptr || + X509_NAME_add_entry_by_txt(x509Issuer.get(), "CN", MBSTRING_ASC, + (const uint8_t*)issuer.c_str(), issuer.size(), -1 /* loc */, + 0 /* set */) != 1 || + X509_set_issuer_name(x509.get(), x509Issuer.get()) != 1) { + LOG(ERROR) << "Error setting issuer"; + return {}; + } + + auto x509Subject = X509_NAME_Ptr(X509_NAME_new()); + if (x509Subject.get() == nullptr || + X509_NAME_add_entry_by_txt(x509Subject.get(), "CN", MBSTRING_ASC, + (const uint8_t*)subject.c_str(), subject.size(), -1 /* loc */, + 0 /* set */) != 1 || + X509_set_subject_name(x509.get(), x509Subject.get()) != 1) { + LOG(ERROR) << "Error setting subject"; + return {}; + } + + auto asnNotBefore = ASN1_TIME_Ptr(ASN1_TIME_set(nullptr, validityNotBefore)); + if (asnNotBefore.get() == nullptr || X509_set_notBefore(x509.get(), asnNotBefore.get()) != 1) { + LOG(ERROR) << "Error setting notBefore"; + return {}; + } + + auto asnNotAfter = ASN1_TIME_Ptr(ASN1_TIME_set(nullptr, validityNotAfter)); + if (asnNotAfter.get() == nullptr || X509_set_notAfter(x509.get(), asnNotAfter.get()) != 1) { + LOG(ERROR) << "Error setting notAfter"; + return {}; + } + + if (X509_sign(x509.get(), pkey.get(), EVP_sha256()) == 0) { + LOG(ERROR) << "Error signing X509 certificate"; + return {}; + } + + // Ideally we wouldn't encrypt it (we're only using this function for + // sending a key-pair over binder to the Android app) but BoringSSL does not + // support this: from pkcs8_x509.c in BoringSSL: "In OpenSSL, -1 here means + // to use no encryption, which we do not currently support." + // + // Passing nullptr as |pass|, though, means "no password". So we'll do that. + // Compare with the receiving side - CredstoreIdentityCredential.java - where + // an empty char[] is passed as the password. + // + auto pkcs12 = PKCS12_Ptr(PKCS12_create(nullptr, name.c_str(), pkey.get(), x509.get(), + nullptr, // ca + 0, // nid_key + 0, // nid_cert + 0, // iter, + 0, // mac_iter, + 0)); // keytype + if (pkcs12.get() == nullptr) { + char buf[128]; + long errCode = ERR_get_error(); + ERR_error_string_n(errCode, buf, sizeof buf); + LOG(ERROR) << "Error creating PKCS12, code " << errCode << ": " << buf; + return {}; + } + + unsigned char* buffer = nullptr; + int length = i2d_PKCS12(pkcs12.get(), &buffer); + if (length < 0) { + LOG(ERROR) << "Error encoding PKCS12"; + return {}; + } + vector pkcs12Bytes; + pkcs12Bytes.resize(length); + memcpy(pkcs12Bytes.data(), buffer, length); + OPENSSL_free(buffer); + + return pkcs12Bytes; +} + +optional> ecPublicKeyGenerateCertificate( + const vector& publicKey, const vector& signingKey, + const string& serialDecimal, const string& issuer, const string& subject, + time_t validityNotBefore, time_t validityNotAfter) { + auto group = EC_GROUP_Ptr(EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)); + auto point = EC_POINT_Ptr(EC_POINT_new(group.get())); + if (EC_POINT_oct2point(group.get(), point.get(), publicKey.data(), publicKey.size(), nullptr) != + 1) { + LOG(ERROR) << "Error decoding publicKey"; + return {}; + } + auto ecKey = EC_KEY_Ptr(EC_KEY_new()); + auto pkey = EVP_PKEY_Ptr(EVP_PKEY_new()); + if (ecKey.get() == nullptr || pkey.get() == nullptr) { + LOG(ERROR) << "Memory allocation failed"; + return {}; + } + if (EC_KEY_set_group(ecKey.get(), group.get()) != 1) { + LOG(ERROR) << "Error setting group"; + return {}; + } + if (EC_KEY_set_public_key(ecKey.get(), point.get()) != 1) { + LOG(ERROR) << "Error setting point"; + return {}; + } + if (EVP_PKEY_set1_EC_KEY(pkey.get(), ecKey.get()) != 1) { + LOG(ERROR) << "Error setting key"; + return {}; + } + + auto bn = BIGNUM_Ptr(BN_bin2bn(signingKey.data(), signingKey.size(), nullptr)); + if (bn.get() == nullptr) { + LOG(ERROR) << "Error creating BIGNUM for private key"; + return {}; + } + auto privEcKey = EC_KEY_Ptr(EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)); + if (EC_KEY_set_private_key(privEcKey.get(), bn.get()) != 1) { + LOG(ERROR) << "Error setting private key from BIGNUM"; + return {}; + } + auto privPkey = EVP_PKEY_Ptr(EVP_PKEY_new()); + if (EVP_PKEY_set1_EC_KEY(privPkey.get(), privEcKey.get()) != 1) { + LOG(ERROR) << "Error setting private key"; + return {}; + } + + auto x509 = X509_Ptr(X509_new()); + if (!x509.get()) { + LOG(ERROR) << "Error creating X509 certificate"; + return {}; + } + + if (!X509_set_version(x509.get(), 2 /* version 3, but zero-based */)) { + LOG(ERROR) << "Error setting version to 3"; + return {}; + } + + if (X509_set_pubkey(x509.get(), pkey.get()) != 1) { + LOG(ERROR) << "Error setting public key"; + return {}; + } + + BIGNUM* bignumSerial = nullptr; + if (BN_dec2bn(&bignumSerial, serialDecimal.c_str()) == 0) { + LOG(ERROR) << "Error parsing serial"; + return {}; + } + auto bignumSerialPtr = BIGNUM_Ptr(bignumSerial); + auto asnSerial = ASN1_INTEGER_Ptr(BN_to_ASN1_INTEGER(bignumSerial, nullptr)); + if (X509_set_serialNumber(x509.get(), asnSerial.get()) != 1) { + LOG(ERROR) << "Error setting serial"; + return {}; + } + + auto x509Issuer = X509_NAME_Ptr(X509_NAME_new()); + if (x509Issuer.get() == nullptr || + X509_NAME_add_entry_by_txt(x509Issuer.get(), "CN", MBSTRING_ASC, + (const uint8_t*)issuer.c_str(), issuer.size(), -1 /* loc */, + 0 /* set */) != 1 || + X509_set_issuer_name(x509.get(), x509Issuer.get()) != 1) { + LOG(ERROR) << "Error setting issuer"; + return {}; + } + + auto x509Subject = X509_NAME_Ptr(X509_NAME_new()); + if (x509Subject.get() == nullptr || + X509_NAME_add_entry_by_txt(x509Subject.get(), "CN", MBSTRING_ASC, + (const uint8_t*)subject.c_str(), subject.size(), -1 /* loc */, + 0 /* set */) != 1 || + X509_set_subject_name(x509.get(), x509Subject.get()) != 1) { + LOG(ERROR) << "Error setting subject"; + return {}; + } + + auto asnNotBefore = ASN1_TIME_Ptr(ASN1_TIME_set(nullptr, validityNotBefore)); + if (asnNotBefore.get() == nullptr || X509_set_notBefore(x509.get(), asnNotBefore.get()) != 1) { + LOG(ERROR) << "Error setting notBefore"; + return {}; + } + + auto asnNotAfter = ASN1_TIME_Ptr(ASN1_TIME_set(nullptr, validityNotAfter)); + if (asnNotAfter.get() == nullptr || X509_set_notAfter(x509.get(), asnNotAfter.get()) != 1) { + LOG(ERROR) << "Error setting notAfter"; + return {}; + } + + if (X509_sign(x509.get(), privPkey.get(), EVP_sha256()) == 0) { + LOG(ERROR) << "Error signing X509 certificate"; + return {}; + } + + unsigned char* buffer = nullptr; + int length = i2d_X509(x509.get(), &buffer); + if (length < 0) { + LOG(ERROR) << "Error DER encoding X509 certificate"; + return {}; + } + + vector certificate; + certificate.resize(length); + memcpy(certificate.data(), buffer, length); + OPENSSL_free(buffer); + return certificate; +} + +optional> ecdh(const vector& publicKey, + const vector& privateKey) { + auto group = EC_GROUP_Ptr(EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)); + auto point = EC_POINT_Ptr(EC_POINT_new(group.get())); + if (EC_POINT_oct2point(group.get(), point.get(), publicKey.data(), publicKey.size(), nullptr) != + 1) { + LOG(ERROR) << "Error decoding publicKey"; + return {}; + } + auto ecKey = EC_KEY_Ptr(EC_KEY_new()); + auto pkey = EVP_PKEY_Ptr(EVP_PKEY_new()); + if (ecKey.get() == nullptr || pkey.get() == nullptr) { + LOG(ERROR) << "Memory allocation failed"; + return {}; + } + if (EC_KEY_set_group(ecKey.get(), group.get()) != 1) { + LOG(ERROR) << "Error setting group"; + return {}; + } + if (EC_KEY_set_public_key(ecKey.get(), point.get()) != 1) { + LOG(ERROR) << "Error setting point"; + return {}; + } + if (EVP_PKEY_set1_EC_KEY(pkey.get(), ecKey.get()) != 1) { + LOG(ERROR) << "Error setting key"; + return {}; + } + + auto bn = BIGNUM_Ptr(BN_bin2bn(privateKey.data(), privateKey.size(), nullptr)); + if (bn.get() == nullptr) { + LOG(ERROR) << "Error creating BIGNUM for private key"; + return {}; + } + auto privEcKey = EC_KEY_Ptr(EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)); + if (EC_KEY_set_private_key(privEcKey.get(), bn.get()) != 1) { + LOG(ERROR) << "Error setting private key from BIGNUM"; + return {}; + } + auto privPkey = EVP_PKEY_Ptr(EVP_PKEY_new()); + if (EVP_PKEY_set1_EC_KEY(privPkey.get(), privEcKey.get()) != 1) { + LOG(ERROR) << "Error setting private key"; + return {}; + } + + auto ctx = EVP_PKEY_CTX_Ptr(EVP_PKEY_CTX_new(privPkey.get(), NULL)); + if (ctx.get() == nullptr) { + LOG(ERROR) << "Error creating context"; + return {}; + } + + if (EVP_PKEY_derive_init(ctx.get()) != 1) { + LOG(ERROR) << "Error initializing context"; + return {}; + } + + if (EVP_PKEY_derive_set_peer(ctx.get(), pkey.get()) != 1) { + LOG(ERROR) << "Error setting peer"; + return {}; + } + + /* Determine buffer length for shared secret */ + size_t secretLen = 0; + if (EVP_PKEY_derive(ctx.get(), NULL, &secretLen) != 1) { + LOG(ERROR) << "Error determing length of shared secret"; + return {}; + } + vector sharedSecret; + sharedSecret.resize(secretLen); + + if (EVP_PKEY_derive(ctx.get(), sharedSecret.data(), &secretLen) != 1) { + LOG(ERROR) << "Error deriving shared secret"; + return {}; + } + return sharedSecret; +} + +optional> hkdf(const vector& sharedSecret, const vector& salt, + const vector& info, size_t size) { + vector derivedKey; + derivedKey.resize(size); + if (HKDF(derivedKey.data(), derivedKey.size(), EVP_sha256(), sharedSecret.data(), + sharedSecret.size(), salt.data(), salt.size(), info.data(), info.size()) != 1) { + LOG(ERROR) << "Error deriving key"; + return {}; + } + return derivedKey; +} + +void removeLeadingZeroes(vector& vec) { + while (vec.size() >= 1 && vec[0] == 0x00) { + vec.erase(vec.begin()); + } +} + +tuple, vector> ecPublicKeyGetXandY( + const vector& publicKey) { + if (publicKey.size() != 65 || publicKey[0] != 0x04) { + LOG(ERROR) << "publicKey is not in the expected format"; + return std::make_tuple(false, vector(), vector()); + } + vector x, y; + x.resize(32); + y.resize(32); + memcpy(x.data(), publicKey.data() + 1, 32); + memcpy(y.data(), publicKey.data() + 33, 32); + + removeLeadingZeroes(x); + removeLeadingZeroes(y); + + return std::make_tuple(true, x, y); +} + +optional> certificateChainGetTopMostKey(const vector& certificateChain) { + vector certs; + if (!parseX509Certificates(certificateChain, certs)) { + return {}; + } + if (certs.size() < 1) { + LOG(ERROR) << "No certificates in chain"; + return {}; + } + + int algoId = OBJ_obj2nid(certs[0]->cert_info->key->algor->algorithm); + if (algoId != NID_X9_62_id_ecPublicKey) { + LOG(ERROR) << "Expected NID_X9_62_id_ecPublicKey, got " << OBJ_nid2ln(algoId); + return {}; + } + + auto pkey = EVP_PKEY_Ptr(X509_get_pubkey(certs[0].get())); + if (pkey.get() == nullptr) { + LOG(ERROR) << "No public key"; + return {}; + } + + auto ecKey = EC_KEY_Ptr(EVP_PKEY_get1_EC_KEY(pkey.get())); + if (ecKey.get() == nullptr) { + LOG(ERROR) << "Failed getting EC key"; + return {}; + } + + auto ecGroup = EC_KEY_get0_group(ecKey.get()); + auto ecPoint = EC_KEY_get0_public_key(ecKey.get()); + int size = EC_POINT_point2oct(ecGroup, ecPoint, POINT_CONVERSION_UNCOMPRESSED, nullptr, 0, + nullptr); + if (size == 0) { + LOG(ERROR) << "Error generating public key encoding"; + return {}; + } + vector publicKey; + publicKey.resize(size); + EC_POINT_point2oct(ecGroup, ecPoint, POINT_CONVERSION_UNCOMPRESSED, publicKey.data(), + publicKey.size(), nullptr); + return publicKey; +} + +// --------------------------------------------------------------------------- +// COSE Utility Functions +// --------------------------------------------------------------------------- + +vector coseBuildToBeSigned(const vector& encodedProtectedHeaders, + const vector& data, + const vector& detachedContent) { + cppbor::Array sigStructure; + sigStructure.add("Signature1"); + sigStructure.add(encodedProtectedHeaders); + + // We currently don't support Externally Supplied Data (RFC 8152 section 4.3) + // so external_aad is the empty bstr + vector emptyExternalAad; + sigStructure.add(emptyExternalAad); + + // Next field is the payload, independently of how it's transported (RFC + // 8152 section 4.4). Since our API specifies only one of |data| and + // |detachedContent| can be non-empty, it's simply just the non-empty one. + if (data.size() > 0) { + sigStructure.add(data); + } else { + sigStructure.add(detachedContent); + } + return sigStructure.encode(); +} + +vector coseEncodeHeaders(const cppbor::Map& protectedHeaders) { + if (protectedHeaders.size() == 0) { + cppbor::Bstr emptyBstr(vector({})); + return emptyBstr.encode(); + } + return protectedHeaders.encode(); +} + +// From https://tools.ietf.org/html/rfc8152 +const int COSE_LABEL_ALG = 1; +const int COSE_LABEL_X5CHAIN = 33; // temporary identifier + +// From "COSE Algorithms" registry +const int COSE_ALG_ECDSA_256 = -7; +const int COSE_ALG_HMAC_256_256 = 5; + +bool ecdsaSignatureCoseToDer(const vector& ecdsaCoseSignature, + vector& ecdsaDerSignature) { + if (ecdsaCoseSignature.size() != 64) { + LOG(ERROR) << "COSE signature length is " << ecdsaCoseSignature.size() << ", expected 64"; + return false; + } + + auto rBn = BIGNUM_Ptr(BN_bin2bn(ecdsaCoseSignature.data(), 32, nullptr)); + if (rBn.get() == nullptr) { + LOG(ERROR) << "Error creating BIGNUM for r"; + return false; + } + + auto sBn = BIGNUM_Ptr(BN_bin2bn(ecdsaCoseSignature.data() + 32, 32, nullptr)); + if (sBn.get() == nullptr) { + LOG(ERROR) << "Error creating BIGNUM for s"; + return false; + } + + ECDSA_SIG sig; + sig.r = rBn.get(); + sig.s = sBn.get(); + + size_t len = i2d_ECDSA_SIG(&sig, nullptr); + ecdsaDerSignature.resize(len); + unsigned char* p = (unsigned char*)ecdsaDerSignature.data(); + i2d_ECDSA_SIG(&sig, &p); + + return true; +} + +bool ecdsaSignatureDerToCose(const vector& ecdsaDerSignature, + vector& ecdsaCoseSignature) { + ECDSA_SIG* sig; + const unsigned char* p = ecdsaDerSignature.data(); + sig = d2i_ECDSA_SIG(nullptr, &p, ecdsaDerSignature.size()); + if (sig == nullptr) { + LOG(ERROR) << "Error decoding DER signature"; + return false; + } + + ecdsaCoseSignature.clear(); + ecdsaCoseSignature.resize(64); + if (BN_bn2binpad(sig->r, ecdsaCoseSignature.data(), 32) != 32) { + LOG(ERROR) << "Error encoding r"; + return false; + } + if (BN_bn2binpad(sig->s, ecdsaCoseSignature.data() + 32, 32) != 32) { + LOG(ERROR) << "Error encoding s"; + return false; + } + return true; +} + +optional> coseSignEcDsa(const vector& key, const vector& data, + const vector& detachedContent, + const vector& certificateChain) { + cppbor::Map unprotectedHeaders; + cppbor::Map protectedHeaders; + + if (data.size() > 0 && detachedContent.size() > 0) { + LOG(ERROR) << "data and detachedContent cannot both be non-empty"; + return {}; + } + + protectedHeaders.add(COSE_LABEL_ALG, COSE_ALG_ECDSA_256); + + if (certificateChain.size() != 0) { + optional>> certs = support::certificateChainSplit(certificateChain); + if (!certs) { + LOG(ERROR) << "Error splitting certificate chain"; + return {}; + } + if (certs.value().size() == 1) { + unprotectedHeaders.add(COSE_LABEL_X5CHAIN, certs.value()[0]); + } else { + cppbor::Array certArray; + for (const vector& cert : certs.value()) { + certArray.add(cert); + } + unprotectedHeaders.add(COSE_LABEL_X5CHAIN, std::move(certArray)); + } + } + + vector encodedProtectedHeaders = coseEncodeHeaders(protectedHeaders); + vector toBeSigned = + coseBuildToBeSigned(encodedProtectedHeaders, data, detachedContent); + + optional> derSignature = signEcDsa(key, toBeSigned); + if (!derSignature) { + LOG(ERROR) << "Error signing toBeSigned data"; + return {}; + } + vector coseSignature; + if (!ecdsaSignatureDerToCose(derSignature.value(), coseSignature)) { + LOG(ERROR) << "Error converting ECDSA signature from DER to COSE format"; + return {}; + } + + cppbor::Array coseSign1; + coseSign1.add(encodedProtectedHeaders); + coseSign1.add(std::move(unprotectedHeaders)); + if (data.size() == 0) { + cppbor::Null nullValue; + coseSign1.add(std::move(nullValue)); + } else { + coseSign1.add(data); + } + coseSign1.add(coseSignature); + vector signatureCoseSign1; + signatureCoseSign1 = coseSign1.encode(); + return signatureCoseSign1; +} + +bool coseCheckEcDsaSignature(const vector& signatureCoseSign1, + const vector& detachedContent, + const vector& publicKey) { + auto [item, _, message] = cppbor::parse(signatureCoseSign1); + if (item == nullptr) { + LOG(ERROR) << "Passed-in COSE_Sign1 is not valid CBOR: " << message; + return false; + } + const cppbor::Array* array = item->asArray(); + if (array == nullptr) { + LOG(ERROR) << "Value for COSE_Sign1 is not an array"; + return false; + } + if (array->size() != 4) { + LOG(ERROR) << "Value for COSE_Sign1 is not an array of size 4"; + return false; + } + + const cppbor::Bstr* encodedProtectedHeadersBstr = (*array)[0]->asBstr(); + ; + if (encodedProtectedHeadersBstr == nullptr) { + LOG(ERROR) << "Value for encodedProtectedHeaders is not a bstr"; + return false; + } + const vector encodedProtectedHeaders = encodedProtectedHeadersBstr->value(); + + const cppbor::Map* unprotectedHeaders = (*array)[1]->asMap(); + if (unprotectedHeaders == nullptr) { + LOG(ERROR) << "Value for unprotectedHeaders is not a map"; + return false; + } + + vector data; + const cppbor::Simple* payloadAsSimple = (*array)[2]->asSimple(); + if (payloadAsSimple != nullptr) { + if (payloadAsSimple->asNull() == nullptr) { + LOG(ERROR) << "Value for payload is not null or a bstr"; + return false; + } + } else { + const cppbor::Bstr* payloadAsBstr = (*array)[2]->asBstr(); + if (payloadAsBstr == nullptr) { + LOG(ERROR) << "Value for payload is not null or a bstr"; + return false; + } + data = payloadAsBstr->value(); // TODO: avoid copy + } + + if (data.size() > 0 && detachedContent.size() > 0) { + LOG(ERROR) << "data and detachedContent cannot both be non-empty"; + return false; + } + + const cppbor::Bstr* signatureBstr = (*array)[3]->asBstr(); + if (signatureBstr == nullptr) { + LOG(ERROR) << "Value for signature is a bstr"; + return false; + } + const vector& coseSignature = signatureBstr->value(); + + vector derSignature; + if (!ecdsaSignatureCoseToDer(coseSignature, derSignature)) { + LOG(ERROR) << "Error converting ECDSA signature from COSE to DER format"; + return false; + } + + vector toBeSigned = + coseBuildToBeSigned(encodedProtectedHeaders, data, detachedContent); + if (!checkEcDsaSignature(support::sha256(toBeSigned), derSignature, publicKey)) { + LOG(ERROR) << "Signature check failed"; + return false; + } + return true; +} + +optional> coseSignGetPayload(const vector& signatureCoseSign1) { + auto [item, _, message] = cppbor::parse(signatureCoseSign1); + if (item == nullptr) { + LOG(ERROR) << "Passed-in COSE_Sign1 is not valid CBOR: " << message; + return {}; + } + const cppbor::Array* array = item->asArray(); + if (array == nullptr) { + LOG(ERROR) << "Value for COSE_Sign1 is not an array"; + return {}; + } + if (array->size() != 4) { + LOG(ERROR) << "Value for COSE_Sign1 is not an array of size 4"; + return {}; + } + + vector data; + const cppbor::Simple* payloadAsSimple = (*array)[2]->asSimple(); + if (payloadAsSimple != nullptr) { + if (payloadAsSimple->asNull() == nullptr) { + LOG(ERROR) << "Value for payload is not null or a bstr"; + return {}; + } + // payload is null, so |data| should be empty (as it is) + } else { + const cppbor::Bstr* payloadAsBstr = (*array)[2]->asBstr(); + if (payloadAsBstr == nullptr) { + LOG(ERROR) << "Value for payload is not null or a bstr"; + return {}; + } + // Copy payload into |data| + data = payloadAsBstr->value(); + } + + return data; +} + +optional> coseSignGetX5Chain(const vector& signatureCoseSign1) { + auto [item, _, message] = cppbor::parse(signatureCoseSign1); + if (item == nullptr) { + LOG(ERROR) << "Passed-in COSE_Sign1 is not valid CBOR: " << message; + return {}; + } + const cppbor::Array* array = item->asArray(); + if (array == nullptr) { + LOG(ERROR) << "Value for COSE_Sign1 is not an array"; + return {}; + } + if (array->size() != 4) { + LOG(ERROR) << "Value for COSE_Sign1 is not an array of size 4"; + return {}; + } + + const cppbor::Map* unprotectedHeaders = (*array)[1]->asMap(); + if (unprotectedHeaders == nullptr) { + LOG(ERROR) << "Value for unprotectedHeaders is not a map"; + return {}; + } + + for (size_t n = 0; n < unprotectedHeaders->size(); n++) { + auto [keyItem, valueItem] = (*unprotectedHeaders)[n]; + const cppbor::Int* number = keyItem->asInt(); + if (number == nullptr) { + LOG(ERROR) << "Key item in top-level map is not a number"; + return {}; + } + int label = number->value(); + if (label == COSE_LABEL_X5CHAIN) { + const cppbor::Bstr* bstr = valueItem->asBstr(); + if (bstr != nullptr) { + return bstr->value(); + } + const cppbor::Array* array = valueItem->asArray(); + if (array != nullptr) { + vector certs; + for (size_t m = 0; m < array->size(); m++) { + const cppbor::Bstr* bstr = ((*array)[m])->asBstr(); + if (bstr == nullptr) { + LOG(ERROR) << "Item in x5chain array is not a bstr"; + return {}; + } + const vector& certValue = bstr->value(); + certs.insert(certs.end(), certValue.begin(), certValue.end()); + } + return certs; + } + LOG(ERROR) << "Value for x5chain label is not a bstr or array"; + return {}; + } + } + LOG(ERROR) << "Did not find x5chain label in unprotected headers"; + return {}; +} + +vector coseBuildToBeMACed(const vector& encodedProtectedHeaders, + const vector& data, + const vector& detachedContent) { + cppbor::Array macStructure; + macStructure.add("MAC0"); + macStructure.add(encodedProtectedHeaders); + + // We currently don't support Externally Supplied Data (RFC 8152 section 4.3) + // so external_aad is the empty bstr + vector emptyExternalAad; + macStructure.add(emptyExternalAad); + + // Next field is the payload, independently of how it's transported (RFC + // 8152 section 4.4). Since our API specifies only one of |data| and + // |detachedContent| can be non-empty, it's simply just the non-empty one. + if (data.size() > 0) { + macStructure.add(data); + } else { + macStructure.add(detachedContent); + } + + return macStructure.encode(); +} + +optional> coseMac0(const vector& key, const vector& data, + const vector& detachedContent) { + cppbor::Map unprotectedHeaders; + cppbor::Map protectedHeaders; + + if (data.size() > 0 && detachedContent.size() > 0) { + LOG(ERROR) << "data and detachedContent cannot both be non-empty"; + return {}; + } + + protectedHeaders.add(COSE_LABEL_ALG, COSE_ALG_HMAC_256_256); + + vector encodedProtectedHeaders = coseEncodeHeaders(protectedHeaders); + vector toBeMACed = coseBuildToBeMACed(encodedProtectedHeaders, data, detachedContent); + + optional> mac = hmacSha256(key, toBeMACed); + if (!mac) { + LOG(ERROR) << "Error MACing toBeMACed data"; + return {}; + } + + cppbor::Array array; + array.add(encodedProtectedHeaders); + array.add(std::move(unprotectedHeaders)); + if (data.size() == 0) { + cppbor::Null nullValue; + array.add(std::move(nullValue)); + } else { + array.add(data); + } + array.add(mac.value()); + return array.encode(); +} + +// --------------------------------------------------------------------------- +// Platform abstraction. +// --------------------------------------------------------------------------- + +// 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 hardwareBoundKey = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + +const vector& getHardwareBoundKey() { + return hardwareBoundKey; +} + +// --------------------------------------------------------------------------- +// Utility functions specific to IdentityCredential. +// --------------------------------------------------------------------------- + +Result okResult{ResultCode::OK, ""}; + +const Result& resultOK() { + return okResult; +} + +Result result(ResultCode code, const char* format, ...) { + va_list ap; + va_start(ap, format); + string str; + android::base::StringAppendV(&str, format, ap); + va_end(ap); + return Result{code, str}; +} + +vector> chunkVector(const vector& content, size_t maxChunkSize) { + vector> ret; + + size_t contentSize = content.size(); + if (contentSize <= maxChunkSize) { + ret.push_back(content); + return ret; + } + + size_t numChunks = (contentSize + maxChunkSize - 1) / maxChunkSize; + + size_t pos = 0; + for (size_t n = 0; n < numChunks; n++) { + size_t size = contentSize - pos; + if (size > maxChunkSize) { + size = maxChunkSize; + } + auto begin = content.begin() + pos; + auto end = content.begin() + pos + size; + ret.emplace_back(vector(begin, end)); + pos += maxChunkSize; + } + + return ret; +} + +vector secureAccessControlProfileEncodeCbor(const SecureAccessControlProfile& profile) { + cppbor::Map map; + map.add("id", profile.id); + + if (profile.readerCertificate.size() > 0) { + map.add("readerCertificate", cppbor::Bstr(profile.readerCertificate)); + } + + if (profile.userAuthenticationRequired) { + map.add("userAuthenticationRequired", profile.userAuthenticationRequired); + map.add("timeoutMillis", profile.timeoutMillis); + map.add("secureUserId", profile.secureUserId); + } + + return map.encode(); +} + +optional> secureAccessControlProfileCalcMac( + const SecureAccessControlProfile& profile, const vector& storageKey) { + vector cborData = secureAccessControlProfileEncodeCbor(profile); + + optional> nonce = getRandom(12); + if (!nonce) { + return {}; + } + optional> macO = encryptAes128Gcm(storageKey, nonce.value(), {}, cborData); + if (!macO) { + return {}; + } + return macO.value(); +} + +bool secureAccessControlProfileCheckMac(const SecureAccessControlProfile& profile, + const vector& storageKey) { + vector cborData = secureAccessControlProfileEncodeCbor(profile); + + if (profile.mac.size() < kAesGcmIvSize) { + return false; + } + vector nonce = + vector(profile.mac.begin(), profile.mac.begin() + kAesGcmIvSize); + optional> mac = encryptAes128Gcm(storageKey, nonce, {}, cborData); + if (!mac) { + return false; + } + if (mac.value() != vector(profile.mac)) { + return false; + } + return true; +} + +vector testHardwareBoundKey = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + +const vector& getTestHardwareBoundKey() { + return testHardwareBoundKey; +} + +vector entryCreateAdditionalData(const string& nameSpace, const string& name, + const vector 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 support +} // namespace identity +} // namespace hardware +} // namespace android diff --git a/identity/support/src/cppbor.cpp b/identity/support/src/cppbor.cpp new file mode 100644 index 0000000000..d289985175 --- /dev/null +++ b/identity/support/src/cppbor.cpp @@ -0,0 +1,225 @@ +/* + * 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. + */ + +#include "cppbor.h" +#include "cppbor_parse.h" + +#define LOG_TAG "CppBor" +#include + +namespace cppbor { + +namespace { + +template ::value>> +Iterator writeBigEndian(T value, Iterator pos) { + for (unsigned i = 0; i < sizeof(value); ++i) { + *pos++ = static_cast(value >> (8 * (sizeof(value) - 1))); + value = static_cast(value << 8); + } + return pos; +} + +template ::value>> +void writeBigEndian(T value, std::function& cb) { + for (unsigned i = 0; i < sizeof(value); ++i) { + cb(static_cast(value >> (8 * (sizeof(value) - 1)))); + value = static_cast(value << 8); + } +} + +} // namespace + +size_t headerSize(uint64_t addlInfo) { + if (addlInfo < ONE_BYTE_LENGTH) return 1; + if (addlInfo <= std::numeric_limits::max()) return 2; + if (addlInfo <= std::numeric_limits::max()) return 3; + if (addlInfo <= std::numeric_limits::max()) return 5; + return 9; +} + +uint8_t* encodeHeader(MajorType type, uint64_t addlInfo, uint8_t* pos, const uint8_t* end) { + size_t sz = headerSize(addlInfo); + if (end - pos < static_cast(sz)) return nullptr; + switch (sz) { + case 1: + *pos++ = type | static_cast(addlInfo); + return pos; + case 2: + *pos++ = type | ONE_BYTE_LENGTH; + *pos++ = static_cast(addlInfo); + return pos; + case 3: + *pos++ = type | TWO_BYTE_LENGTH; + return writeBigEndian(static_cast(addlInfo), pos); + case 5: + *pos++ = type | FOUR_BYTE_LENGTH; + return writeBigEndian(static_cast(addlInfo), pos); + case 9: + *pos++ = type | EIGHT_BYTE_LENGTH; + return writeBigEndian(addlInfo, pos); + default: + CHECK(false); // Impossible to get here. + return nullptr; + } +} + +void encodeHeader(MajorType type, uint64_t addlInfo, EncodeCallback encodeCallback) { + size_t sz = headerSize(addlInfo); + switch (sz) { + case 1: + encodeCallback(type | static_cast(addlInfo)); + break; + case 2: + encodeCallback(type | ONE_BYTE_LENGTH); + encodeCallback(static_cast(addlInfo)); + break; + case 3: + encodeCallback(type | TWO_BYTE_LENGTH); + writeBigEndian(static_cast(addlInfo), encodeCallback); + break; + case 5: + encodeCallback(type | FOUR_BYTE_LENGTH); + writeBigEndian(static_cast(addlInfo), encodeCallback); + break; + case 9: + encodeCallback(type | EIGHT_BYTE_LENGTH); + writeBigEndian(addlInfo, encodeCallback); + break; + default: + CHECK(false); // Impossible to get here. + } +} + +bool Item::operator==(const Item& other) const& { + if (type() != other.type()) return false; + switch (type()) { + case UINT: + return *asUint() == *(other.asUint()); + case NINT: + return *asNint() == *(other.asNint()); + case BSTR: + return *asBstr() == *(other.asBstr()); + case TSTR: + return *asTstr() == *(other.asTstr()); + case ARRAY: + return *asArray() == *(other.asArray()); + case MAP: + return *asMap() == *(other.asMap()); + case SIMPLE: + return *asSimple() == *(other.asSimple()); + case SEMANTIC: + return *asSemantic() == *(other.asSemantic()); + default: + CHECK(false); // Impossible to get here. + return false; + } +} + +Nint::Nint(int64_t v) : mValue(v) { + CHECK(v < 0) << "Only negative values allowed"; +} + +bool Simple::operator==(const Simple& other) const& { + if (simpleType() != other.simpleType()) return false; + + switch (simpleType()) { + case BOOLEAN: + return *asBool() == *(other.asBool()); + case NULL_T: + return true; + default: + CHECK(false); // Impossible to get here. + return false; + } +} + +uint8_t* Bstr::encode(uint8_t* pos, const uint8_t* end) const { + pos = encodeHeader(mValue.size(), pos, end); + if (!pos || end - pos < static_cast(mValue.size())) return nullptr; + return std::copy(mValue.begin(), mValue.end(), pos); +} + +void Bstr::encodeValue(EncodeCallback encodeCallback) const { + for (auto c : mValue) { + encodeCallback(c); + } +} + +uint8_t* Tstr::encode(uint8_t* pos, const uint8_t* end) const { + pos = encodeHeader(mValue.size(), pos, end); + if (!pos || end - pos < static_cast(mValue.size())) return nullptr; + return std::copy(mValue.begin(), mValue.end(), pos); +} + +void Tstr::encodeValue(EncodeCallback encodeCallback) const { + for (auto c : mValue) { + encodeCallback(static_cast(c)); + } +} + +bool CompoundItem::operator==(const CompoundItem& other) const& { + return type() == other.type() // + && addlInfo() == other.addlInfo() // + // Can't use vector::operator== because the contents are pointers. std::equal lets us + // provide a predicate that does the dereferencing. + && std::equal(mEntries.begin(), mEntries.end(), other.mEntries.begin(), + [](auto& a, auto& b) -> bool { return *a == *b; }); +} + +uint8_t* CompoundItem::encode(uint8_t* pos, const uint8_t* end) const { + pos = encodeHeader(addlInfo(), pos, end); + if (!pos) return nullptr; + for (auto& entry : mEntries) { + pos = entry->encode(pos, end); + if (!pos) return nullptr; + } + return pos; +} + +void CompoundItem::encode(EncodeCallback encodeCallback) const { + encodeHeader(addlInfo(), encodeCallback); + for (auto& entry : mEntries) { + entry->encode(encodeCallback); + } +} + +void Map::assertInvariant() const { + CHECK(mEntries.size() % 2 == 0); +} + +std::unique_ptr Map::clone() const { + assertInvariant(); + auto res = std::make_unique(); + for (size_t i = 0; i < mEntries.size(); i += 2) { + res->add(mEntries[i]->clone(), mEntries[i + 1]->clone()); + } + return res; +} + +std::unique_ptr Array::clone() const { + auto res = std::make_unique(); + for (size_t i = 0; i < mEntries.size(); i++) { + res->add(mEntries[i]->clone()); + } + return res; +} + +void Semantic::assertInvariant() const { + CHECK(mEntries.size() == 1); +} + +} // namespace cppbor diff --git a/identity/support/src/cppbor_parse.cpp b/identity/support/src/cppbor_parse.cpp new file mode 100644 index 0000000000..c9ebb8ac1a --- /dev/null +++ b/identity/support/src/cppbor_parse.cpp @@ -0,0 +1,351 @@ +/* + * 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. + */ + +#include "cppbor_parse.h" + +#include +#include + +#define LOG_TAG "CppBor" +#include + +namespace cppbor { + +namespace { + +std::string insufficientLengthString(size_t bytesNeeded, size_t bytesAvail, + const std::string& type) { + std::stringstream errStream; + errStream << "Need " << bytesNeeded << " byte(s) for " << type << ", have " << bytesAvail + << "."; + return errStream.str(); +} + +template >> +std::tuple parseLength(const uint8_t* pos, const uint8_t* end, + ParseClient* parseClient) { + if (pos + sizeof(T) > end) { + parseClient->error(pos - 1, insufficientLengthString(sizeof(T), end - pos, "length field")); + return {false, 0, pos}; + } + + const uint8_t* intEnd = pos + sizeof(T); + T result = 0; + do { + result = static_cast((result << 8) | *pos++); + } while (pos < intEnd); + return {true, result, pos}; +} + +std::tuple parseRecursively(const uint8_t* begin, const uint8_t* end, + ParseClient* parseClient); + +std::tuple handleUint(uint64_t value, const uint8_t* hdrBegin, + const uint8_t* hdrEnd, + ParseClient* parseClient) { + std::unique_ptr item = std::make_unique(value); + return {hdrEnd, + parseClient->item(item, hdrBegin, hdrEnd /* valueBegin */, hdrEnd /* itemEnd */)}; +} + +std::tuple handleNint(uint64_t value, const uint8_t* hdrBegin, + const uint8_t* hdrEnd, + ParseClient* parseClient) { + if (value > std::numeric_limits::max()) { + parseClient->error(hdrBegin, "NINT values that don't fit in int64_t are not supported."); + return {hdrBegin, nullptr /* end parsing */}; + } + std::unique_ptr item = std::make_unique(-1 - static_cast(value)); + return {hdrEnd, + parseClient->item(item, hdrBegin, hdrEnd /* valueBegin */, hdrEnd /* itemEnd */)}; +} + +std::tuple handleBool(uint64_t value, const uint8_t* hdrBegin, + const uint8_t* hdrEnd, + ParseClient* parseClient) { + std::unique_ptr item = std::make_unique(value == TRUE); + return {hdrEnd, + parseClient->item(item, hdrBegin, hdrEnd /* valueBegin */, hdrEnd /* itemEnd */)}; +} + +std::tuple handleNull(const uint8_t* hdrBegin, const uint8_t* hdrEnd, + ParseClient* parseClient) { + std::unique_ptr item = std::make_unique(); + return {hdrEnd, + parseClient->item(item, hdrBegin, hdrEnd /* valueBegin */, hdrEnd /* itemEnd */)}; +} + +template +std::tuple handleString(uint64_t length, const uint8_t* hdrBegin, + const uint8_t* valueBegin, const uint8_t* end, + const std::string& errLabel, + ParseClient* parseClient) { + if (end - valueBegin < static_cast(length)) { + parseClient->error(hdrBegin, insufficientLengthString(length, end - valueBegin, errLabel)); + return {hdrBegin, nullptr /* end parsing */}; + } + + std::unique_ptr item = std::make_unique(valueBegin, valueBegin + length); + return {valueBegin + length, + parseClient->item(item, hdrBegin, valueBegin, valueBegin + length)}; +} + +class IncompleteItem { + public: + virtual ~IncompleteItem() {} + virtual void add(std::unique_ptr item) = 0; +}; + +class IncompleteArray : public Array, public IncompleteItem { + public: + IncompleteArray(size_t size) : mSize(size) {} + + // We return the "complete" size, rather than the actual size. + size_t size() const override { return mSize; } + + void add(std::unique_ptr item) override { + mEntries.reserve(mSize); + mEntries.push_back(std::move(item)); + } + + private: + size_t mSize; +}; + +class IncompleteMap : public Map, public IncompleteItem { + public: + IncompleteMap(size_t size) : mSize(size) {} + + // We return the "complete" size, rather than the actual size. + size_t size() const override { return mSize; } + + void add(std::unique_ptr item) override { + mEntries.reserve(mSize * 2); + mEntries.push_back(std::move(item)); + } + + private: + size_t mSize; +}; + +class IncompleteSemantic : public Semantic, public IncompleteItem { + public: + IncompleteSemantic(uint64_t value) : Semantic(value) {} + + // We return the "complete" size, rather than the actual size. + size_t size() const override { return 1; } + + void add(std::unique_ptr item) override { + mEntries.reserve(1); + mEntries.push_back(std::move(item)); + } +}; + +std::tuple handleEntries(size_t entryCount, const uint8_t* hdrBegin, + const uint8_t* pos, const uint8_t* end, + const std::string& typeName, + ParseClient* parseClient) { + while (entryCount > 0) { + --entryCount; + if (pos == end) { + parseClient->error(hdrBegin, "Not enough entries for " + typeName + "."); + return {hdrBegin, nullptr /* end parsing */}; + } + std::tie(pos, parseClient) = parseRecursively(pos, end, parseClient); + if (!parseClient) return {hdrBegin, nullptr}; + } + return {pos, parseClient}; +} + +std::tuple handleCompound( + std::unique_ptr item, uint64_t entryCount, const uint8_t* hdrBegin, + const uint8_t* valueBegin, const uint8_t* end, const std::string& typeName, + ParseClient* parseClient) { + parseClient = + parseClient->item(item, hdrBegin, valueBegin, valueBegin /* don't know the end yet */); + if (!parseClient) return {hdrBegin, nullptr}; + + const uint8_t* pos; + std::tie(pos, parseClient) = + handleEntries(entryCount, hdrBegin, valueBegin, end, typeName, parseClient); + if (!parseClient) return {hdrBegin, nullptr}; + + return {pos, parseClient->itemEnd(item, hdrBegin, valueBegin, pos)}; +} + +std::tuple parseRecursively(const uint8_t* begin, const uint8_t* end, + ParseClient* parseClient) { + const uint8_t* pos = begin; + + MajorType type = static_cast(*pos & 0xE0); + uint8_t tagInt = *pos & 0x1F; + ++pos; + + bool success = true; + uint64_t addlData; + if (tagInt < ONE_BYTE_LENGTH || tagInt > EIGHT_BYTE_LENGTH) { + addlData = tagInt; + } else { + switch (tagInt) { + case ONE_BYTE_LENGTH: + std::tie(success, addlData, pos) = parseLength(pos, end, parseClient); + break; + + case TWO_BYTE_LENGTH: + std::tie(success, addlData, pos) = parseLength(pos, end, parseClient); + break; + + case FOUR_BYTE_LENGTH: + std::tie(success, addlData, pos) = parseLength(pos, end, parseClient); + break; + + case EIGHT_BYTE_LENGTH: + std::tie(success, addlData, pos) = parseLength(pos, end, parseClient); + break; + + default: + CHECK(false); // It's impossible to get here + break; + } + } + + if (!success) return {begin, nullptr}; + + switch (type) { + case UINT: + return handleUint(addlData, begin, pos, parseClient); + + case NINT: + return handleNint(addlData, begin, pos, parseClient); + + case BSTR: + return handleString(addlData, begin, pos, end, "byte string", parseClient); + + case TSTR: + return handleString(addlData, begin, pos, end, "text string", parseClient); + + case ARRAY: + return handleCompound(std::make_unique(addlData), addlData, begin, pos, + end, "array", parseClient); + + case MAP: + return handleCompound(std::make_unique(addlData), addlData * 2, begin, + pos, end, "map", parseClient); + + case SEMANTIC: + return handleCompound(std::make_unique(addlData), 1, begin, pos, + end, "semantic", parseClient); + + case SIMPLE: + switch (addlData) { + case TRUE: + case FALSE: + return handleBool(addlData, begin, pos, parseClient); + case NULL_V: + return handleNull(begin, pos, parseClient); + } + } + CHECK(false); // Impossible to get here. + return {}; +} + +class FullParseClient : public ParseClient { + public: + virtual ParseClient* item(std::unique_ptr& item, const uint8_t*, const uint8_t*, + const uint8_t* end) override { + if (mParentStack.empty() && !item->isCompound()) { + // This is the first and only item. + mTheItem = std::move(item); + mPosition = end; + return nullptr; // We're done. + } + + if (item->isCompound()) { + // Starting a new compound data item, i.e. a new parent. Save it on the parent stack. + // It's safe to save a raw pointer because the unique_ptr is guaranteed to stay in + // existence until the corresponding itemEnd() call. + assert(dynamic_cast(item.get())); + mParentStack.push(static_cast(item.get())); + return this; + } else { + appendToLastParent(std::move(item)); + return this; + } + } + + virtual ParseClient* itemEnd(std::unique_ptr& item, const uint8_t*, const uint8_t*, + const uint8_t* end) override { + CHECK(item->isCompound() && item.get() == mParentStack.top()); + mParentStack.pop(); + + if (mParentStack.empty()) { + mTheItem = std::move(item); + mPosition = end; + return nullptr; // We're done + } else { + appendToLastParent(std::move(item)); + return this; + } + } + + virtual void error(const uint8_t* position, const std::string& errorMessage) override { + mPosition = position; + mErrorMessage = errorMessage; + } + + std::tuple /* result */, const uint8_t* /* newPos */, + std::string /* errMsg */> + parseResult() { + std::unique_ptr p = std::move(mTheItem); + return {std::move(p), mPosition, std::move(mErrorMessage)}; + } + + private: + void appendToLastParent(std::unique_ptr item) { + auto parent = mParentStack.top(); + assert(dynamic_cast(parent)); + if (parent->type() == ARRAY) { + static_cast(parent)->add(std::move(item)); + } else if (parent->type() == MAP) { + static_cast(parent)->add(std::move(item)); + } else if (parent->type() == SEMANTIC) { + static_cast(parent)->add(std::move(item)); + } else { + CHECK(false); // Impossible to get here. + } + } + + std::unique_ptr mTheItem; + std::stack mParentStack; + const uint8_t* mPosition = nullptr; + std::string mErrorMessage; +}; + +} // anonymous namespace + +void parse(const uint8_t* begin, const uint8_t* end, ParseClient* parseClient) { + parseRecursively(begin, end, parseClient); +} + +std::tuple /* result */, const uint8_t* /* newPos */, + std::string /* errMsg */> +parse(const uint8_t* begin, const uint8_t* end) { + FullParseClient parseClient; + parse(begin, end, &parseClient); + return parseClient.parseResult(); +} + +} // namespace cppbor diff --git a/identity/support/tests/IdentityCredentialSupportTest.cpp b/identity/support/tests/IdentityCredentialSupportTest.cpp new file mode 100644 index 0000000000..c356549d00 --- /dev/null +++ b/identity/support/tests/IdentityCredentialSupportTest.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. + */ + +#include +#include +#include + +#include +#include + +#include + +#include +#include + +using std::optional; +using std::string; +using std::vector; + +namespace android { +namespace hardware { +namespace identity { + +TEST(IdentityCredentialSupport, encodeHex) { + EXPECT_EQ("", support::encodeHex(vector({}))); + EXPECT_EQ("01", support::encodeHex(vector({1}))); + EXPECT_EQ("000102030405060708090a0b0c0d0e0f10", + support::encodeHex( + vector({0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}))); + EXPECT_EQ("0102ffe060", support::encodeHex(vector({1, 2, 255, 224, 96}))); +} + +TEST(IdentityCredentialSupport, decodeHex) { + EXPECT_EQ(vector({}), support::decodeHex("")); + EXPECT_EQ(vector({1}), support::decodeHex("01")); + + EXPECT_EQ(vector({0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}), + support::decodeHex("000102030405060708090a0b0c0d0e0f10")); + + EXPECT_FALSE(support::decodeHex("0g")); + EXPECT_FALSE(support::decodeHex("0")); + EXPECT_FALSE(support::decodeHex("012")); +} + +TEST(IdentityCredentialSupport, CborPrettyPrint) { + EXPECT_EQ("'Some text'", support::cborPrettyPrint(cppbor::Tstr("Some text").encode())); + + EXPECT_EQ("''", support::cborPrettyPrint(cppbor::Tstr("").encode())); + + EXPECT_EQ("{0x01, 0x00, 0x02, 0xf0, 0xff, 0x40}", + support::cborPrettyPrint( + cppbor::Bstr(vector({1, 0, 2, 240, 255, 64})).encode())); + + EXPECT_EQ("{}", support::cborPrettyPrint(cppbor::Bstr(vector()).encode())); + + EXPECT_EQ("true", support::cborPrettyPrint(cppbor::Bool(true).encode())); + + EXPECT_EQ("false", support::cborPrettyPrint(cppbor::Bool(false).encode())); + + EXPECT_EQ("42", support::cborPrettyPrint(cppbor::Uint(42).encode())); + + EXPECT_EQ("9223372036854775807", // 0x7fff ffff ffff ffff + support::cborPrettyPrint(cppbor::Uint(std::numeric_limits::max()).encode())); + + EXPECT_EQ("-42", support::cborPrettyPrint(cppbor::Nint(-42).encode())); + + EXPECT_EQ("-9223372036854775808", // -0x8000 0000 0000 0000 + support::cborPrettyPrint(cppbor::Nint(std::numeric_limits::min()).encode())); +} + +TEST(IdentityCredentialSupport, CborPrettyPrintCompound) { + cppbor::Array array = cppbor::Array("foo", "bar", "baz"); + EXPECT_EQ("['foo', 'bar', 'baz', ]", support::cborPrettyPrint(array.encode())); + + cppbor::Map map = cppbor::Map().add("foo", 42).add("bar", 43).add("baz", 44); + EXPECT_EQ( + "{\n" + " 'foo' : 42,\n" + " 'bar' : 43,\n" + " 'baz' : 44,\n" + "}", + support::cborPrettyPrint(map.encode())); + + cppbor::Array array2 = cppbor::Array(cppbor::Tstr("Some text"), cppbor::Nint(-42)); + EXPECT_EQ("['Some text', -42, ]", support::cborPrettyPrint(array2.encode())); + + cppbor::Map map2 = cppbor::Map().add(42, "foo").add(43, "bar").add(44, "baz"); + EXPECT_EQ( + "{\n" + " 42 : 'foo',\n" + " 43 : 'bar',\n" + " 44 : 'baz',\n" + "}", + support::cborPrettyPrint(map2.encode())); + + cppbor::Array deeplyNestedArrays = + cppbor::Array(cppbor::Array(cppbor::Array("a", "b", "c")), + cppbor::Array(cppbor::Array("d", "e", cppbor::Array("f", "g")))); + EXPECT_EQ( + "[\n" + " ['a', 'b', 'c', ],\n" + " [\n 'd',\n" + " 'e',\n" + " ['f', 'g', ],\n" + " ],\n" + "]", + support::cborPrettyPrint(deeplyNestedArrays.encode())); + + EXPECT_EQ( + "[\n" + " {0x0a, 0x0b},\n" + " 'foo',\n" + " 42,\n" + " ['foo', 'bar', 'baz', ],\n" + " {\n" + " 'foo' : 42,\n" + " 'bar' : 43,\n" + " 'baz' : 44,\n" + " },\n" + " {\n" + " 'deep1' : ['Some text', -42, ],\n" + " 'deep2' : {\n" + " 42 : 'foo',\n" + " 43 : 'bar',\n" + " 44 : 'baz',\n" + " },\n" + " },\n" + "]", + support::cborPrettyPrint(cppbor::Array(cppbor::Bstr(vector{10, 11}), + cppbor::Tstr("foo"), cppbor::Uint(42), + std::move(array), std::move(map), + (cppbor::Map() + .add("deep1", std::move(array2)) + .add("deep2", std::move(map2)))) + .encode())); +} + +TEST(IdentityCredentialSupport, Signatures) { + vector data = {1, 2, 3}; + + optional> keyPair = support::createEcKeyPair(); + ASSERT_TRUE(keyPair); + optional> privKey = support::ecKeyPairGetPrivateKey(keyPair.value()); + ASSERT_TRUE(privKey); + optional> pubKey = support::ecKeyPairGetPublicKey(keyPair.value()); + ASSERT_TRUE(pubKey); + + optional> signature = support::signEcDsa(privKey.value(), data); + ASSERT_TRUE( + support::checkEcDsaSignature(support::sha256(data), signature.value(), pubKey.value())); + + // Manipulate the signature, check that verification fails. + vector modifiedSignature = signature.value(); + modifiedSignature[0] ^= 0xff; + ASSERT_FALSE( + support::checkEcDsaSignature(support::sha256(data), modifiedSignature, pubKey.value())); + + // Manipulate the data being checked, check that verification fails. + vector modifiedDigest = support::sha256(data); + modifiedDigest[0] ^= 0xff; + ASSERT_FALSE(support::checkEcDsaSignature(modifiedDigest, signature.value(), pubKey.value())); +} + +string replaceLine(const string& str, ssize_t lineNumber, const string& replacement) { + vector lines; + std::istringstream f(str); + string s; + while (std::getline(f, s, '\n')) { + lines.push_back(s); + } + + size_t numLines = lines.size(); + if (lineNumber < 0) { + lineNumber = numLines - (-lineNumber); + } + + string ret; + size_t n = 0; + for (const string& line : lines) { + if (n == lineNumber) { + ret += replacement + "\n"; + } else { + ret += line + "\n"; + } + n++; + } + return ret; +} + +TEST(IdentityCredentialSupport, CoseSignatures) { + optional> keyPair = support::createEcKeyPair(); + ASSERT_TRUE(keyPair); + optional> privKey = support::ecKeyPairGetPrivateKey(keyPair.value()); + ASSERT_TRUE(privKey); + optional> pubKey = support::ecKeyPairGetPublicKey(keyPair.value()); + ASSERT_TRUE(pubKey); + + vector data = {1, 2, 3}; + optional> coseSign1 = support::coseSignEcDsa( + privKey.value(), data, {} /* detachedContent */, {} /* x5chain */); + ASSERT_TRUE(support::coseCheckEcDsaSignature(coseSign1.value(), {} /* detachedContent */, + pubKey.value())); + + optional> payload = support::coseSignGetPayload(coseSign1.value()); + ASSERT_TRUE(payload); + ASSERT_EQ(data, payload.value()); + + // Finally, check that |coseSign1| are the bytes of a valid COSE_Sign1 message + string out = support::cborPrettyPrint(coseSign1.value()); + out = replaceLine(out, -2, " [] // Signature Removed"); + EXPECT_EQ( + "[\n" + " {0xa1, 0x01, 0x26},\n" // Bytes of {1:-7} 1 is 'alg' label and -7 is "ECDSA 256" + " {},\n" + " {0x01, 0x02, 0x03},\n" + " [] // Signature Removed\n" + "]\n", + out); +} + +TEST(IdentityCredentialSupport, CoseSignaturesAdditionalData) { + optional> keyPair = support::createEcKeyPair(); + ASSERT_TRUE(keyPair); + optional> privKey = support::ecKeyPairGetPrivateKey(keyPair.value()); + ASSERT_TRUE(privKey); + optional> pubKey = support::ecKeyPairGetPublicKey(keyPair.value()); + ASSERT_TRUE(pubKey); + + vector detachedContent = {1, 2, 3}; + optional> coseSign1 = support::coseSignEcDsa(privKey.value(), {} /* data */, + detachedContent, {} /* x5chain */); + ASSERT_TRUE( + support::coseCheckEcDsaSignature(coseSign1.value(), detachedContent, pubKey.value())); + + optional> payload = support::coseSignGetPayload(coseSign1.value()); + ASSERT_TRUE(payload); + ASSERT_EQ(0, payload.value().size()); + + // Finally, check that |coseSign1| are the bytes of a valid COSE_Sign1 message + string out = support::cborPrettyPrint(coseSign1.value()); + out = replaceLine(out, -2, " [] // Signature Removed"); + EXPECT_EQ( + "[\n" + " {0xa1, 0x01, 0x26},\n" // Bytes of {1:-7} 1 is 'alg' label and -7 is "ECDSA 256" + " {},\n" + " null,\n" + " [] // Signature Removed\n" + "]\n", + out); +} + +vector generateCertChain(size_t numCerts) { + vector> certs; + + for (size_t n = 0; n < numCerts; n++) { + optional> keyPair = support::createEcKeyPair(); + optional> privKey = support::ecKeyPairGetPrivateKey(keyPair.value()); + optional> pubKey = support::ecKeyPairGetPublicKey(keyPair.value()); + + optional> cert = support::ecPublicKeyGenerateCertificate( + pubKey.value(), privKey.value(), "0001", "someIssuer", "someSubject", 0, 0); + certs.push_back(cert.value()); + } + return support::certificateChainJoin(certs); +} + +TEST(IdentityCredentialSupport, CoseSignaturesX5ChainWithSingleCert) { + optional> keyPair = support::createEcKeyPair(); + ASSERT_TRUE(keyPair); + optional> privKey = support::ecKeyPairGetPrivateKey(keyPair.value()); + ASSERT_TRUE(privKey); + optional> pubKey = support::ecKeyPairGetPublicKey(keyPair.value()); + ASSERT_TRUE(pubKey); + + vector certChain = generateCertChain(1); + optional>> splitCerts = support::certificateChainSplit(certChain); + ASSERT_EQ(1, splitCerts.value().size()); + + vector detachedContent = {1, 2, 3}; + optional> coseSign1 = + support::coseSignEcDsa(privKey.value(), {} /* data */, detachedContent, certChain); + ASSERT_TRUE( + support::coseCheckEcDsaSignature(coseSign1.value(), detachedContent, pubKey.value())); + + optional> payload = support::coseSignGetPayload(coseSign1.value()); + ASSERT_TRUE(payload); + ASSERT_EQ(0, payload.value().size()); + + optional> certsRecovered = support::coseSignGetX5Chain(coseSign1.value()); + EXPECT_EQ(certsRecovered.value(), certChain); +} + +TEST(IdentityCredentialSupport, CoseSignaturesX5ChainWithMultipleCerts) { + optional> keyPair = support::createEcKeyPair(); + ASSERT_TRUE(keyPair); + optional> privKey = support::ecKeyPairGetPrivateKey(keyPair.value()); + ASSERT_TRUE(privKey); + optional> pubKey = support::ecKeyPairGetPublicKey(keyPair.value()); + ASSERT_TRUE(pubKey); + + vector certChain = generateCertChain(5); + optional>> splitCerts = support::certificateChainSplit(certChain); + ASSERT_EQ(5, splitCerts.value().size()); + + vector detachedContent = {1, 2, 3}; + optional> coseSign1 = + support::coseSignEcDsa(privKey.value(), {} /* data */, detachedContent, certChain); + ASSERT_TRUE( + support::coseCheckEcDsaSignature(coseSign1.value(), detachedContent, pubKey.value())); + + optional> payload = support::coseSignGetPayload(coseSign1.value()); + ASSERT_TRUE(payload); + ASSERT_EQ(0, payload.value().size()); + + optional> certsRecovered = support::coseSignGetX5Chain(coseSign1.value()); + EXPECT_EQ(certsRecovered.value(), certChain); +} + +TEST(IdentityCredentialSupport, CertificateChain) { + optional> keyPair = support::createEcKeyPair(); + ASSERT_TRUE(keyPair); + optional> privKey = support::ecKeyPairGetPrivateKey(keyPair.value()); + ASSERT_TRUE(privKey); + optional> pubKey = support::ecKeyPairGetPublicKey(keyPair.value()); + ASSERT_TRUE(pubKey); + + optional> cert = support::ecPublicKeyGenerateCertificate( + pubKey.value(), privKey.value(), "0001", "someIssuer", "someSubject", 0, 0); + + optional> extractedPubKey = + support::certificateChainGetTopMostKey(cert.value()); + ASSERT_TRUE(extractedPubKey); + ASSERT_EQ(pubKey.value(), extractedPubKey.value()); + + // We expect to the chain returned by ecPublicKeyGenerateCertificate() to only have a + // single element + optional>> splitCerts = support::certificateChainSplit(cert.value()); + ASSERT_EQ(1, splitCerts.value().size()); + ASSERT_EQ(splitCerts.value()[0], cert.value()); + + optional> otherKeyPair = support::createEcKeyPair(); + ASSERT_TRUE(otherKeyPair); + optional> otherPrivKey = support::ecKeyPairGetPrivateKey(keyPair.value()); + ASSERT_TRUE(otherPrivKey); + optional> otherPubKey = support::ecKeyPairGetPublicKey(keyPair.value()); + ASSERT_TRUE(otherPubKey); + optional> otherCert = support::ecPublicKeyGenerateCertificate( + otherPubKey.value(), privKey.value(), "0001", "someIssuer", "someSubject", 0, 0); + + // Now both cert and otherCert are two distinct certificates. Let's make a + // chain and check that certificateChainSplit() works as expected. + ASSERT_NE(cert.value(), otherCert.value()); + const vector> certs2 = {cert.value(), otherCert.value()}; + vector certs2combined = support::certificateChainJoin(certs2); + ASSERT_EQ(certs2combined.size(), cert.value().size() + otherCert.value().size()); + optional>> splitCerts2 = support::certificateChainSplit(certs2combined); + ASSERT_EQ(certs2, splitCerts2.value()); +} + +vector strToVec(const string& str) { + vector ret; + size_t size = str.size(); + ret.resize(size); + memcpy(ret.data(), str.data(), size); + return ret; +} + +// Test vector from https://en.wikipedia.org/wiki/HMAC +TEST(IdentityCredentialSupport, hmacSha256) { + vector key = strToVec("key"); + vector data = strToVec("The quick brown fox jumps over the lazy dog"); + + vector expected = + support::decodeHex("f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8") + .value(); + + optional> hmac = support::hmacSha256(key, data); + ASSERT_TRUE(hmac); + ASSERT_EQ(expected, hmac.value()); +} + +// See also CoseMac0 test in UtilUnitTest.java inside cts/tests/tests/identity/ +TEST(IdentityCredentialSupport, CoseMac0) { + vector key; + key.resize(32); + vector data = {0x10, 0x11, 0x12, 0x13}; + vector detachedContent = {}; + + optional> mac = support::coseMac0(key, data, detachedContent); + ASSERT_TRUE(mac); + + EXPECT_EQ( + "[\n" + " {0xa1, 0x01, 0x05},\n" + " {},\n" + " {0x10, 0x11, 0x12, 0x13},\n" + " {0x6c, 0xec, 0xb5, 0x6a, 0xc9, 0x5c, 0xae, 0x3b, 0x41, 0x13, 0xde, 0xa4, 0xd8, " + "0x86, 0x5c, 0x28, 0x2c, 0xd5, 0xa5, 0x13, 0xff, 0x3b, 0xd1, 0xde, 0x70, 0x5e, 0xbb, " + "0xe2, 0x2d, 0x42, 0xbe, 0x53},\n" + "]", + support::cborPrettyPrint(mac.value())); +} + +TEST(IdentityCredentialSupport, CoseMac0DetachedContent) { + vector key; + key.resize(32); + vector data = {}; + vector detachedContent = {0x10, 0x11, 0x12, 0x13}; + + optional> mac = support::coseMac0(key, data, detachedContent); + ASSERT_TRUE(mac); + + // Same HMAC as in CoseMac0 test, only difference is that payload is null. + EXPECT_EQ( + "[\n" + " {0xa1, 0x01, 0x05},\n" + " {},\n" + " null,\n" + " {0x6c, 0xec, 0xb5, 0x6a, 0xc9, 0x5c, 0xae, 0x3b, 0x41, 0x13, 0xde, 0xa4, 0xd8, " + "0x86, 0x5c, 0x28, 0x2c, 0xd5, 0xa5, 0x13, 0xff, 0x3b, 0xd1, 0xde, 0x70, 0x5e, 0xbb, " + "0xe2, 0x2d, 0x42, 0xbe, 0x53},\n" + "]", + support::cborPrettyPrint(mac.value())); +} + +} // namespace identity +} // namespace hardware +} // namespace android + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/identity/support/tests/cppbor_test.cpp b/identity/support/tests/cppbor_test.cpp new file mode 100644 index 0000000000..3eb5598961 --- /dev/null +++ b/identity/support/tests/cppbor_test.cpp @@ -0,0 +1,1013 @@ +/* + * 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. + */ + +#include +#include + +#include +#include + +#include "cppbor.h" +#include "cppbor_parse.h" + +using namespace cppbor; +using namespace std; + +using ::testing::_; +using ::testing::AllOf; +using ::testing::ByRef; +using ::testing::InSequence; +using ::testing::IsNull; +using ::testing::NotNull; +using ::testing::Return; +using ::testing::Truly; +using ::testing::Unused; + +string hexDump(const string& str) { + stringstream s; + for (auto c : str) { + s << setfill('0') << setw(2) << hex << (static_cast(c) & 0xff); + } + return s.str(); +} + +TEST(SimpleValueTest, UnsignedValueSizes) { + // Check that unsigned integers encode to correct lengths, and that encodedSize() is correct. + vector> testCases{ + {0, 1}, + {1, 1}, + {23, 1}, + {24, 2}, + {255, 2}, + {256, 3}, + {65535, 3}, + {65536, 5}, + {4294967295, 5}, + {4294967296, 9}, + {std::numeric_limits::max(), 9}, + }; + for (auto& testCase : testCases) { + Uint val(testCase.first); + EXPECT_EQ(testCase.second, val.encodedSize()) << "Wrong size for value " << testCase.first; + EXPECT_EQ(val.encodedSize(), val.toString().size()) + << "encodedSize and encoding disagree for value " << testCase.first; + } +} + +TEST(SimpleValueTest, UnsignedValueEncodings) { + EXPECT_EQ("\x00"s, Uint(0u).toString()); + EXPECT_EQ("\x01"s, Uint(1u).toString()); + EXPECT_EQ("\x0a"s, Uint(10u).toString()); + EXPECT_EQ("\x17"s, Uint(23u).toString()); + EXPECT_EQ("\x18\x18"s, Uint(24u).toString()); + EXPECT_EQ("\x18\x19"s, Uint(25u).toString()); + EXPECT_EQ("\x18\x64"s, Uint(100u).toString()); + EXPECT_EQ("\x19\x03\xe8"s, Uint(1000u).toString()); + EXPECT_EQ("\x1a\x00\x0f\x42\x40"s, Uint(1000000u).toString()); + EXPECT_EQ("\x1b\x00\x00\x00\xe8\xd4\xa5\x10\x00"s, Uint(1000000000000u).toString()); + EXPECT_EQ("\x1B\x7f\xff\xff\xff\xff\xff\xff\xff"s, + Uint(std::numeric_limits::max()).toString()); +} + +TEST(SimpleValueTest, NegativeValueEncodings) { + EXPECT_EQ("\x20"s, Nint(-1).toString()); + EXPECT_EQ("\x28"s, Nint(-9).toString()); + EXPECT_EQ("\x29"s, Nint(-10).toString()); + EXPECT_EQ("\x36"s, Nint(-23).toString()); + EXPECT_EQ("\x37"s, Nint(-24).toString()); + EXPECT_EQ("\x38\x18"s, Nint(-25).toString()); + EXPECT_EQ("\x38\x62"s, Nint(-99).toString()); + EXPECT_EQ("\x38\x63"s, Nint(-100).toString()); + EXPECT_EQ("\x39\x03\xe6"s, Nint(-999).toString()); + EXPECT_EQ("\x39\x03\xe7"s, Nint(-1000).toString()); + EXPECT_EQ("\x3a\x00\x0f\x42\x3F"s, Nint(-1000000).toString()); + EXPECT_EQ("\x3b\x00\x00\x00\xe8\xd4\xa5\x0f\xff"s, Nint(-1000000000000).toString()); + EXPECT_EQ("\x3B\x7f\xff\xff\xff\xff\xff\xff\xff"s, + Nint(std::numeric_limits::min()).toString()); +} + +TEST(SimpleValueDeathTest, NegativeValueEncodings) { + EXPECT_DEATH(Nint(0), ""); + EXPECT_DEATH(Nint(1), ""); +} + +TEST(SimpleValueTest, BooleanEncodings) { + EXPECT_EQ("\xf4"s, Bool(false).toString()); + EXPECT_EQ("\xf5"s, Bool(true).toString()); +} + +TEST(SimpleValueTest, ByteStringEncodings) { + EXPECT_EQ("\x40", Bstr("").toString()); + EXPECT_EQ("\x41\x61", Bstr("a").toString()); + EXPECT_EQ("\x41\x41", Bstr("A").toString()); + EXPECT_EQ("\x44\x49\x45\x54\x46", Bstr("IETF").toString()); + EXPECT_EQ("\x42\x22\x5c", Bstr("\"\\").toString()); + EXPECT_EQ("\x42\xc3\xbc", Bstr("\xc3\xbc").toString()); + EXPECT_EQ("\x43\xe6\xb0\xb4", Bstr("\xe6\xb0\xb4").toString()); + EXPECT_EQ("\x44\xf0\x90\x85\x91", Bstr("\xf0\x90\x85\x91").toString()); + EXPECT_EQ("\x44\x01\x02\x03\x04", Bstr("\x01\x02\x03\x04").toString()); + EXPECT_EQ("\x44\x40\x40\x40\x40", Bstr("@@@@").toString()); +} + +TEST(SimpleValueTest, TextStringEncodings) { + EXPECT_EQ("\x60"s, Tstr("").toString()); + EXPECT_EQ("\x61\x61"s, Tstr("a").toString()); + EXPECT_EQ("\x61\x41"s, Tstr("A").toString()); + EXPECT_EQ("\x64\x49\x45\x54\x46"s, Tstr("IETF").toString()); + EXPECT_EQ("\x62\x22\x5c"s, Tstr("\"\\").toString()); + EXPECT_EQ("\x62\xc3\xbc"s, Tstr("\xc3\xbc").toString()); + EXPECT_EQ("\x63\xe6\xb0\xb4"s, Tstr("\xe6\xb0\xb4").toString()); + EXPECT_EQ("\x64\xf0\x90\x85\x91"s, Tstr("\xf0\x90\x85\x91").toString()); + EXPECT_EQ("\x64\x01\x02\x03\x04"s, Tstr("\x01\x02\x03\x04").toString()); +} + +TEST(IsIteratorPairOverTest, All) { + EXPECT_TRUE(( + details::is_iterator_pair_over, char>::value)); + EXPECT_TRUE((details::is_iterator_pair_over, + char>::value)); + EXPECT_TRUE((details::is_iterator_pair_over, + char>::value)); + EXPECT_TRUE((details::is_iterator_pair_over, char>::value)); + EXPECT_TRUE((details::is_iterator_pair_over, char>::value)); + EXPECT_TRUE((details::is_iterator_pair_over, char>::value)); + EXPECT_FALSE((details::is_iterator_pair_over, + uint8_t>::value)); + EXPECT_FALSE((details::is_iterator_pair_over, uint8_t>::value)); + EXPECT_TRUE((details::is_iterator_pair_over< + pair::iterator, vector::iterator>, uint8_t>::value)); + EXPECT_TRUE((details::is_iterator_pair_over< + pair::const_iterator, vector::iterator>, + uint8_t>::value)); + EXPECT_TRUE((details::is_iterator_pair_over< + pair::iterator, vector::const_iterator>, + uint8_t>::value)); + EXPECT_TRUE((details::is_iterator_pair_over, uint8_t>::value)); + EXPECT_TRUE((details::is_iterator_pair_over, uint8_t>::value)); + EXPECT_TRUE((details::is_iterator_pair_over, uint8_t>::value)); + EXPECT_FALSE((details::is_iterator_pair_over< + pair::iterator, vector::iterator>, char>::value)); + EXPECT_FALSE((details::is_iterator_pair_over, char>::value)); +} + +TEST(MakeEntryTest, Boolean) { + EXPECT_EQ("\xf4"s, details::makeItem(false)->toString()); +} + +TEST(MakeEntryTest, Integers) { + EXPECT_EQ("\x00"s, details::makeItem(static_cast(0))->toString()); + EXPECT_EQ("\x00"s, details::makeItem(static_cast(0))->toString()); + EXPECT_EQ("\x00"s, details::makeItem(static_cast(0))->toString()); + EXPECT_EQ("\x00"s, details::makeItem(static_cast(0))->toString()); + EXPECT_EQ("\x00"s, details::makeItem(static_cast(0))->toString()); + EXPECT_EQ("\x00"s, details::makeItem(static_cast(0))->toString()); + EXPECT_EQ("\x00"s, details::makeItem(static_cast(0))->toString()); + EXPECT_EQ("\x00"s, details::makeItem(static_cast(0))->toString()); + EXPECT_EQ("\x20"s, details::makeItem(static_cast(-1))->toString()); + EXPECT_EQ("\x20"s, details::makeItem(static_cast(-1))->toString()); + EXPECT_EQ("\x20"s, details::makeItem(static_cast(-1))->toString()); + EXPECT_EQ("\x20"s, details::makeItem(static_cast(-1))->toString()); + + EXPECT_EQ("\x1b\xff\xff\xff\xff\xff\xff\xff\xff"s, + details::makeItem(static_cast(std::numeric_limits::max())) + ->toString()); +} + +TEST(MakeEntryTest, StdStrings) { + string s1("hello"); + const string s2("hello"); + EXPECT_EQ("\x65\x68\x65\x6c\x6c\x6f"s, details::makeItem(s1)->toString()); // copy of string + EXPECT_EQ("\x65\x68\x65\x6c\x6c\x6f"s, + details::makeItem(s2)->toString()); // copy of const string + EXPECT_EQ("\x65\x68\x65\x6c\x6c\x6f"s, + details::makeItem(std::move(s1))->toString()); // move string + EXPECT_EQ(0U, s1.size()); // Prove string was moved, not copied. +} + +TEST(MakeEntryTest, StdStringViews) { + string_view s1("hello"); + const string_view s2("hello"); + EXPECT_EQ("\x65\x68\x65\x6c\x6c\x6f"s, details::makeItem(s1)->toString()); + EXPECT_EQ("\x65\x68\x65\x6c\x6c\x6f"s, details::makeItem(s2)->toString()); +} + +TEST(MakeEntryTest, CStrings) { + char s1[] = "hello"; + const char s2[] = "hello"; + const char* s3 = "hello"; + EXPECT_EQ("\x65\x68\x65\x6c\x6c\x6f"s, details::makeItem(s1)->toString()); + EXPECT_EQ("\x65\x68\x65\x6c\x6c\x6f"s, details::makeItem(s2)->toString()); + EXPECT_EQ("\x65\x68\x65\x6c\x6c\x6f"s, details::makeItem(s3)->toString()); +} + +TEST(MakeEntryTest, StringIteratorPairs) { + // Use iterators from string to prove that "real" iterators work + string s1 = "hello"s; + pair p1 = make_pair(s1.begin(), s1.end()); + + const pair p2 = p1; + EXPECT_EQ("\x65\x68\x65\x6c\x6c\x6f"s, details::makeItem(p1)->toString()); + EXPECT_EQ("\x65\x68\x65\x6c\x6c\x6f"s, details::makeItem(p2)->toString()); + + // Use char*s as iterators + const char* s2 = "hello"; + pair p3 = make_pair(s2, s2 + 5); + const pair p4 = p3; + EXPECT_EQ("\x65\x68\x65\x6c\x6c\x6f"s, details::makeItem(p3)->toString()); + EXPECT_EQ("\x65\x68\x65\x6c\x6c\x6f"s, details::makeItem(p4)->toString()); +} + +TEST(MakeEntryTest, ByteStrings) { + vector v1 = {0x00, 0x01, 0x02}; + const vector v2 = {0x00, 0x01, 0x02}; + EXPECT_EQ("\x43\x00\x01\x02"s, details::makeItem(v1)->toString()); // copy of vector + EXPECT_EQ("\x43\x00\x01\x02"s, details::makeItem(v2)->toString()); // copy of const vector + EXPECT_EQ("\x43\x00\x01\x02"s, details::makeItem(std::move(v1))->toString()); // move vector + EXPECT_EQ(0U, v1.size()); // Prove vector was moved, not copied. +} + +TEST(MakeEntryTest, ByteStringIteratorPairs) { + using vec = vector; + using iter = vec::iterator; + vec v1 = {0x00, 0x01, 0x02}; + pair p1 = make_pair(v1.begin(), v1.end()); + const pair p2 = make_pair(v1.begin(), v1.end()); + EXPECT_EQ("\x43\x00\x01\x02"s, details::makeItem(p1)->toString()); + EXPECT_EQ("\x43\x00\x01\x02"s, details::makeItem(p2)->toString()); + + // Use uint8_t*s as iterators + uint8_t v2[] = {0x00, 0x01, 0x02}; + uint8_t* v3 = v2; + pair p3 = make_pair(v2, v2 + 3); + const pair p4 = make_pair(v2, v2 + 3); + pair p5 = make_pair(v3, v3 + 3); + const pair p6 = make_pair(v3, v3 + 3); + EXPECT_EQ("\x43\x00\x01\x02"s, details::makeItem(p3)->toString()); + EXPECT_EQ("\x43\x00\x01\x02"s, details::makeItem(p4)->toString()); + EXPECT_EQ("\x43\x00\x01\x02"s, details::makeItem(p5)->toString()); + EXPECT_EQ("\x43\x00\x01\x02"s, details::makeItem(p6)->toString()); +} + +TEST(MakeEntryTest, ByteStringBuffers) { + uint8_t v1[] = {0x00, 0x01, 0x02}; + EXPECT_EQ("\x43\x00\x01\x02"s, details::makeItem(make_pair(v1, 3))->toString()); +} + +TEST(MakeEntryTest, ItemPointer) { + Uint* p1 = new Uint(0); + EXPECT_EQ("\x00"s, details::makeItem(p1)->toString()); + EXPECT_EQ("\x60"s, details::makeItem(new Tstr(string()))->toString()); +} + +TEST(MakeEntryTest, ItemReference) { + Tstr str("hello"s); + Tstr& strRef = str; + const Tstr& strConstRef = str; + EXPECT_EQ("\x65\x68\x65\x6c\x6c\x6f"s, details::makeItem(str)->toString()); + EXPECT_EQ("\x65\x68\x65\x6c\x6c\x6f"s, details::makeItem(strRef)->toString()); + EXPECT_EQ("\x65\x68\x65\x6c\x6c\x6f"s, details::makeItem(strConstRef)->toString()); + EXPECT_EQ("\x65\x68\x65\x6c\x6c\x6f"s, details::makeItem(std::move(str))->toString()); + EXPECT_EQ("\x60"s, details::makeItem(str)->toString()); // Prove that it moved + + EXPECT_EQ("\x00"s, details::makeItem(Uint(0))->toString()); + + EXPECT_EQ("\x43\x00\x01\x02"s, + details::makeItem(Bstr(vector{0x00, 0x01, 0x02}))->toString()); + + EXPECT_EQ("\x80"s, details::makeItem(Array())->toString()); + EXPECT_EQ("\xa0"s, details::makeItem(Map())->toString()); +} + +TEST(CompoundValueTest, ArrayOfInts) { + EXPECT_EQ("\x80"s, Array().toString()); + Array(Uint(0)).toString(); + + EXPECT_EQ("\x81\x00"s, Array(Uint(0U)).toString()); + EXPECT_EQ("\x82\x00\x01"s, Array(Uint(0), Uint(1)).toString()); + EXPECT_EQ("\x83\x00\x01\x38\x62"s, Array(Uint(0), Uint(1), Nint(-99)).toString()); + + EXPECT_EQ("\x81\x00"s, Array(0).toString()); + EXPECT_EQ("\x82\x00\x01"s, Array(0, 1).toString()); + EXPECT_EQ("\x83\x00\x01\x38\x62"s, Array(0, 1, -99).toString()); +} + +TEST(CompoundValueTest, MapOfInts) { + EXPECT_EQ("\xA0"s, Map().toString()); + EXPECT_EQ("\xA1\x00\x01"s, Map(Uint(0), Uint(1)).toString()); + // Maps with an odd number of arguments will fail to compile. Uncomment the next lines to test. + // EXPECT_EQ("\xA1\x00"s, Map(Int(0)).toString()); + // EXPECT_EQ("\xA1\x00\x01\x02"s, Map(Int(0), Int(1), Int(2)).toString()); +} + +TEST(CompoundValueTest, MixedArray) { + vector vec = {3, 2, 1}; + EXPECT_EQ("\x84\x01\x20\x43\x03\x02\x01\x65\x68\x65\x6C\x6C\x6F"s, + Array(Uint(1), Nint(-1), Bstr(vec), Tstr("hello")).toString()); + + EXPECT_EQ("\x84\x01\x20\x43\x03\x02\x01\x65\x68\x65\x6C\x6C\x6F"s, + Array(1, -1, vec, "hello").toString()); +} + +TEST(CompoundValueTest, MixedMap) { + vector vec = {3, 2, 1}; + EXPECT_EQ("\xA2\x01\x20\x43\x03\x02\x01\x65\x68\x65\x6C\x6C\x6F"s, + Map(Uint(1), Nint(-1), Bstr(vec), Tstr("hello")).toString()); + + EXPECT_EQ("\xA2\x01\x20\x43\x03\x02\x01\x65\x68\x65\x6C\x6C\x6F"s, + Map(1, -1, vec, "hello").toString()); +} + +TEST(CompoundValueTest, NestedStructures) { + vector vec = {3, 2, 1}; + + string expectedEncoding = + "\xA2\x66\x4F\x75\x74\x65\x72\x31\x82\xA2\x66\x49\x6E\x6E\x65\x72\x31\x18\x63\x66\x49" + "\x6E" + "\x6E\x65\x72\x32\x43\x03\x02\x01\x63\x66\x6F\x6F\x66\x4F\x75\x74\x65\x72\x32\x0A"s; + + // Do it with explicitly-created Items + EXPECT_EQ(expectedEncoding, + Map(Tstr("Outer1"), + Array( // + Map(Tstr("Inner1"), Uint(99), Tstr("Inner2"), Bstr(vec)), Tstr("foo")), + Tstr("Outer2"), // + Uint(10)) + .toString()); + EXPECT_EQ(3U, vec.size()); + + // Now just use convertible types + EXPECT_EQ(expectedEncoding, Map("Outer1", + Array(Map("Inner1", 99, // + "Inner2", vec), + "foo"), + "Outer2", 10) + .toString()); + EXPECT_EQ(3U, vec.size()); + + // Finally, do it with the .add() method. This is slightly less efficient, but has the + // advantage you can build a structure up incrementally, or somewhat fluently if you like. + // First, fluently. + EXPECT_EQ(expectedEncoding, Map().add("Outer1", Array().add(Map() // + .add("Inner1", 99) + .add("Inner2", vec)) + .add("foo")) + .add("Outer2", 10) + .toString()); + EXPECT_EQ(3U, vec.size()); + + // Next, more incrementally + Array arr; + arr.add(Map() // + .add("Inner1", 99) + .add("Inner2", vec)) + .add("foo"); + EXPECT_EQ(3U, vec.size()); + + Map m; + m.add("Outer1", std::move(arr)); // Moving is necessary; Map and Array cannot be copied. + m.add("Outer2", 10); + auto s = m.toString(); + EXPECT_EQ(expectedEncoding, s); +} + +TEST(EncodingMethodsTest, AllVariants) { + Map map; + map.add("key1", Array().add(Map() // + .add("key_a", 9999999) + .add("key_b", std::vector{0x01, 0x02, 0x03}) + .add("key_c", std::numeric_limits::max()) + .add("key_d", std::numeric_limits::min())) + .add("foo")) + .add("key2", true); + + std::vector buf; + buf.resize(map.encodedSize()); + + EXPECT_EQ(buf.data() + buf.size(), map.encode(buf.data(), buf.data() + buf.size())); + + EXPECT_EQ(buf, map.encode()); + + std::vector buf2; + map.encode(std::back_inserter(buf2)); + EXPECT_EQ(buf, buf2); + + auto iter = buf.begin(); + map.encode([&](uint8_t c) { EXPECT_EQ(c, *iter++); }); +} + +TEST(EncodingMethodsTest, UintWithTooShortBuf) { + Uint val(100000); + vector buf(val.encodedSize() - 1); + EXPECT_EQ(nullptr, val.encode(buf.data(), buf.data() + buf.size())); +} + +TEST(EncodingMethodsTest, TstrWithTooShortBuf) { + Tstr val("01234567890123456789012345"s); + vector buf(1); + EXPECT_EQ(nullptr, val.encode(buf.data(), buf.data() + buf.size())); + + buf.resize(val.encodedSize() - 1); + EXPECT_EQ(nullptr, val.encode(buf.data(), buf.data() + buf.size())); +} + +TEST(EncodingMethodsTest, BstrWithTooShortBuf) { + Bstr val("01234567890123456789012345"s); + vector buf(1); + EXPECT_EQ(nullptr, val.encode(buf.data(), buf.data() + buf.size())); + + buf.resize(val.encodedSize() - 1); + EXPECT_EQ(nullptr, val.encode(buf.data(), buf.data() + buf.size())); +} + +TEST(EncodingMethodsTest, ArrayWithTooShortBuf) { + Array val("a", 5, -100); + + std::vector buf(val.encodedSize() - 1); + EXPECT_EQ(nullptr, val.encode(buf.data(), buf.data() + buf.size())); +} + +TEST(EncodingMethodsTest, MapWithTooShortBuf) { + Map map; + map.add("key1", Array().add(Map() // + .add("key_a", 99) + .add("key_b", std::vector{0x01, 0x02, 0x03})) + .add("foo")) + .add("key2", true); + + std::vector buf(map.encodedSize() - 1); + EXPECT_EQ(nullptr, map.encode(buf.data(), buf.data() + buf.size())); +} + +TEST(EqualityTest, Uint) { + Uint val(99); + EXPECT_EQ(val, Uint(99)); + + EXPECT_NE(val, Uint(98)); + EXPECT_NE(val, Nint(-1)); + EXPECT_NE(val, Tstr("99")); + EXPECT_NE(val, Bstr("99")); + EXPECT_NE(val, Bool(false)); + EXPECT_NE(val, Array(99, 1)); + EXPECT_NE(val, Map(99, 1)); +} + +TEST(EqualityTest, Nint) { + Nint val(-1); + EXPECT_EQ(val, Nint(-1)); + + EXPECT_NE(val, Uint(99)); + EXPECT_NE(val, Nint(-4)); + EXPECT_NE(val, Tstr("99")); + EXPECT_NE(val, Bstr("99")); + EXPECT_NE(val, Bool(false)); + EXPECT_NE(val, Array(99)); + EXPECT_NE(val, Map(99, 1)); +} + +TEST(EqualityTest, Tstr) { + Tstr val("99"); + EXPECT_EQ(val, Tstr("99")); + + EXPECT_NE(val, Uint(99)); + EXPECT_NE(val, Nint(-1)); + EXPECT_NE(val, Nint(-4)); + EXPECT_NE(val, Tstr("98")); + EXPECT_NE(val, Bstr("99")); + EXPECT_NE(val, Bool(false)); + EXPECT_NE(val, Array(99, 1)); + EXPECT_NE(val, Map(99, 1)); +} + +TEST(EqualityTest, Bstr) { + Bstr val("99"); + EXPECT_EQ(val, Bstr("99")); + + EXPECT_NE(val, Uint(99)); + EXPECT_NE(val, Nint(-1)); + EXPECT_NE(val, Nint(-4)); + EXPECT_NE(val, Tstr("99")); + EXPECT_NE(val, Bstr("98")); + EXPECT_NE(val, Bool(false)); + EXPECT_NE(val, Array(99, 1)); + EXPECT_NE(val, Map(99, 1)); +} + +TEST(EqualityTest, Bool) { + Bool val(false); + EXPECT_EQ(val, Bool(false)); + + EXPECT_NE(val, Uint(99)); + EXPECT_NE(val, Nint(-1)); + EXPECT_NE(val, Nint(-4)); + EXPECT_NE(val, Tstr("99")); + EXPECT_NE(val, Bstr("98")); + EXPECT_NE(val, Bool(true)); + EXPECT_NE(val, Array(99, 1)); + EXPECT_NE(val, Map(99, 1)); +} + +TEST(EqualityTest, Array) { + Array val(99, 1); + EXPECT_EQ(val, Array(99, 1)); + + EXPECT_NE(val, Uint(99)); + EXPECT_NE(val, Nint(-1)); + EXPECT_NE(val, Nint(-4)); + EXPECT_NE(val, Tstr("99")); + EXPECT_NE(val, Bstr("98")); + EXPECT_NE(val, Bool(true)); + EXPECT_NE(val, Array(99, 2)); + EXPECT_NE(val, Array(98, 1)); + EXPECT_NE(val, Array(99, 1, 2)); + EXPECT_NE(val, Map(99, 1)); +} + +TEST(EqualityTest, Map) { + Map val(99, 1); + EXPECT_EQ(val, Map(99, 1)); + + EXPECT_NE(val, Uint(99)); + EXPECT_NE(val, Nint(-1)); + EXPECT_NE(val, Nint(-4)); + EXPECT_NE(val, Tstr("99")); + EXPECT_NE(val, Bstr("98")); + EXPECT_NE(val, Bool(true)); + EXPECT_NE(val, Array(99, 1)); + EXPECT_NE(val, Map(99, 2)); + EXPECT_NE(val, Map(99, 1, 99, 2)); +} + +TEST(ConvertTest, Uint) { + unique_ptr item = details::makeItem(10); + + EXPECT_EQ(UINT, item->type()); + EXPECT_NE(nullptr, item->asInt()); + EXPECT_NE(nullptr, item->asUint()); + EXPECT_EQ(nullptr, item->asNint()); + EXPECT_EQ(nullptr, item->asTstr()); + EXPECT_EQ(nullptr, item->asBstr()); + EXPECT_EQ(nullptr, item->asSimple()); + EXPECT_EQ(nullptr, item->asMap()); + EXPECT_EQ(nullptr, item->asArray()); + + EXPECT_EQ(10, item->asInt()->value()); + EXPECT_EQ(10, item->asUint()->value()); +} + +TEST(ConvertTest, Nint) { + unique_ptr item = details::makeItem(-10); + + EXPECT_EQ(NINT, item->type()); + EXPECT_NE(nullptr, item->asInt()); + EXPECT_EQ(nullptr, item->asUint()); + EXPECT_NE(nullptr, item->asNint()); + EXPECT_EQ(nullptr, item->asTstr()); + EXPECT_EQ(nullptr, item->asBstr()); + EXPECT_EQ(nullptr, item->asSimple()); + EXPECT_EQ(nullptr, item->asMap()); + EXPECT_EQ(nullptr, item->asArray()); + + EXPECT_EQ(-10, item->asInt()->value()); + EXPECT_EQ(-10, item->asNint()->value()); +} + +TEST(ConvertTest, Tstr) { + unique_ptr item = details::makeItem("hello"); + + EXPECT_EQ(TSTR, item->type()); + EXPECT_EQ(nullptr, item->asInt()); + EXPECT_EQ(nullptr, item->asUint()); + EXPECT_EQ(nullptr, item->asNint()); + EXPECT_NE(nullptr, item->asTstr()); + EXPECT_EQ(nullptr, item->asBstr()); + EXPECT_EQ(nullptr, item->asSimple()); + EXPECT_EQ(nullptr, item->asMap()); + EXPECT_EQ(nullptr, item->asArray()); + + EXPECT_EQ("hello"s, item->asTstr()->value()); +} + +TEST(ConvertTest, Bstr) { + vector vec{0x23, 0x24, 0x22}; + unique_ptr item = details::makeItem(vec); + + EXPECT_EQ(BSTR, item->type()); + EXPECT_EQ(nullptr, item->asInt()); + EXPECT_EQ(nullptr, item->asUint()); + EXPECT_EQ(nullptr, item->asNint()); + EXPECT_EQ(nullptr, item->asTstr()); + EXPECT_NE(nullptr, item->asBstr()); + EXPECT_EQ(nullptr, item->asSimple()); + EXPECT_EQ(nullptr, item->asMap()); + EXPECT_EQ(nullptr, item->asArray()); + + EXPECT_EQ(vec, item->asBstr()->value()); +} + +TEST(ConvertTest, Bool) { + unique_ptr item = details::makeItem(false); + + EXPECT_EQ(SIMPLE, item->type()); + EXPECT_EQ(nullptr, item->asInt()); + EXPECT_EQ(nullptr, item->asUint()); + EXPECT_EQ(nullptr, item->asNint()); + EXPECT_EQ(nullptr, item->asTstr()); + EXPECT_EQ(nullptr, item->asBstr()); + EXPECT_NE(nullptr, item->asSimple()); + EXPECT_EQ(nullptr, item->asMap()); + EXPECT_EQ(nullptr, item->asArray()); + + EXPECT_EQ(BOOLEAN, item->asSimple()->simpleType()); + EXPECT_NE(nullptr, item->asSimple()->asBool()); + + EXPECT_FALSE(item->asSimple()->asBool()->value()); +} + +TEST(ConvertTest, Map) { + unique_ptr item(new Map); + + EXPECT_EQ(MAP, item->type()); + EXPECT_EQ(nullptr, item->asInt()); + EXPECT_EQ(nullptr, item->asUint()); + EXPECT_EQ(nullptr, item->asNint()); + EXPECT_EQ(nullptr, item->asTstr()); + EXPECT_EQ(nullptr, item->asBstr()); + EXPECT_EQ(nullptr, item->asSimple()); + EXPECT_NE(nullptr, item->asMap()); + EXPECT_EQ(nullptr, item->asArray()); + + EXPECT_EQ(0U, item->asMap()->size()); +} + +TEST(ConvertTest, Array) { + unique_ptr item(new Array); + + EXPECT_EQ(ARRAY, item->type()); + EXPECT_EQ(nullptr, item->asInt()); + EXPECT_EQ(nullptr, item->asUint()); + EXPECT_EQ(nullptr, item->asNint()); + EXPECT_EQ(nullptr, item->asTstr()); + EXPECT_EQ(nullptr, item->asBstr()); + EXPECT_EQ(nullptr, item->asSimple()); + EXPECT_EQ(nullptr, item->asMap()); + EXPECT_NE(nullptr, item->asArray()); + + EXPECT_EQ(0U, item->asArray()->size()); +} + +class MockParseClient : public ParseClient { + public: + MOCK_METHOD4(item, ParseClient*(std::unique_ptr& item, const uint8_t* hdrBegin, + const uint8_t* valueBegin, const uint8_t* end)); + MOCK_METHOD4(itemEnd, ParseClient*(std::unique_ptr& item, const uint8_t* hdrBegin, + const uint8_t* valueBegin, const uint8_t* end)); + MOCK_METHOD2(error, void(const uint8_t* position, const std::string& errorMessage)); +}; + +MATCHER_P(IsType, value, std::string("Type ") + (negation ? "doesn't match" : "matches")) { + return arg->type() == value; +} + +MATCHER_P(MatchesItem, value, "") { + return arg && *arg == value; +} + +MATCHER_P(IsArrayOfSize, value, "") { + return arg->type() == ARRAY && arg->asArray()->size() == value; +} + +MATCHER_P(IsMapOfSize, value, "") { + return arg->type() == MAP && arg->asMap()->size() == value; +} + +TEST(StreamParseTest, Uint) { + MockParseClient mpc; + + Uint val(100); + auto encoded = val.encode(); + uint8_t* encBegin = encoded.data(); + uint8_t* encEnd = encoded.data() + encoded.size(); + + EXPECT_CALL(mpc, item(MatchesItem(val), encBegin, encEnd, encEnd)).WillOnce(Return(&mpc)); + EXPECT_CALL(mpc, itemEnd(_, _, _, _)).Times(0); + EXPECT_CALL(mpc, error(_, _)).Times(0); + + parse(encoded.data(), encoded.data() + encoded.size(), &mpc); +} + +TEST(StreamParseTest, Nint) { + MockParseClient mpc; + + Nint val(-10); + auto encoded = val.encode(); + uint8_t* encBegin = encoded.data(); + uint8_t* encEnd = encoded.data() + encoded.size(); + + EXPECT_CALL(mpc, item(MatchesItem(val), encBegin, encEnd, encEnd)).WillOnce(Return(&mpc)); + + EXPECT_CALL(mpc, itemEnd(_, _, _, _)).Times(0); + EXPECT_CALL(mpc, error(_, _)).Times(0); + + parse(encoded.data(), encoded.data() + encoded.size(), &mpc); +} + +TEST(StreamParseTest, Bool) { + MockParseClient mpc; + + Bool val(true); + auto encoded = val.encode(); + uint8_t* encBegin = encoded.data(); + uint8_t* encEnd = encoded.data() + encoded.size(); + + EXPECT_CALL(mpc, item(MatchesItem(val), encBegin, encEnd, encEnd)).WillOnce(Return(&mpc)); + EXPECT_CALL(mpc, itemEnd(_, _, _, _)).Times(0); + EXPECT_CALL(mpc, error(_, _)).Times(0); + + parse(encoded.data(), encoded.data() + encoded.size(), &mpc); +} + +TEST(StreamParseTest, Tstr) { + MockParseClient mpc; + + Tstr val("Hello"); + auto encoded = val.encode(); + uint8_t* encBegin = encoded.data(); + uint8_t* encEnd = encoded.data() + encoded.size(); + + EXPECT_CALL(mpc, item(MatchesItem(val), encBegin, encBegin + 1, encEnd)).WillOnce(Return(&mpc)); + EXPECT_CALL(mpc, itemEnd(_, _, _, _)).Times(0); + EXPECT_CALL(mpc, error(_, _)).Times(0); + + parse(encoded.data(), encoded.data() + encoded.size(), &mpc); +} + +TEST(StreamParseTest, Bstr) { + MockParseClient mpc; + + Bstr val("Hello"); + auto encoded = val.encode(); + uint8_t* encBegin = encoded.data(); + uint8_t* encEnd = encoded.data() + encoded.size(); + + EXPECT_CALL(mpc, item(MatchesItem(val), encBegin, encBegin + 1, encEnd)).WillOnce(Return(&mpc)); + EXPECT_CALL(mpc, itemEnd(_, _, _, _)).Times(0); + EXPECT_CALL(mpc, error(_, _)).Times(0); + + parse(encoded.data(), encoded.data() + encoded.size(), &mpc); +} + +TEST(StreamParseTest, Array) { + MockParseClient mpc; + + Array val("Hello", 4, Array(-9, "Goodbye"), std::numeric_limits::max()); + ASSERT_NE(val[2]->asArray(), nullptr); + const Array& interior = *(val[2]->asArray()); + auto encoded = val.encode(); + uint8_t* encBegin = encoded.data(); + uint8_t* encEnd = encoded.data() + encoded.size(); + + { + InSequence s; + const uint8_t* pos = encBegin; + EXPECT_CALL(mpc, item(IsArrayOfSize(val.size()), pos, pos + 1, pos + 1)) + .WillOnce(Return(&mpc)); + ++pos; + EXPECT_CALL(mpc, item(MatchesItem(ByRef(*val[0])), pos, pos + 1, pos + 6)) + .WillOnce(Return(&mpc)); + pos += 6; + EXPECT_CALL(mpc, item(MatchesItem(ByRef(*val[1])), pos, pos + 1, pos + 1)) + .WillOnce(Return(&mpc)); + ++pos; + const uint8_t* innerArrayBegin = pos; + EXPECT_CALL(mpc, item(IsArrayOfSize(interior.size()), pos, pos + 1, pos + 1)) + .WillOnce(Return(&mpc)); + ++pos; + EXPECT_CALL(mpc, item(MatchesItem(ByRef(*interior[0])), pos, pos + 1, pos + 1)) + .WillOnce(Return(&mpc)); + ++pos; + EXPECT_CALL(mpc, item(MatchesItem(ByRef(*interior[1])), pos, pos + 1, pos + 8)) + .WillOnce(Return(&mpc)); + pos += 8; + EXPECT_CALL(mpc, itemEnd(IsArrayOfSize(interior.size()), innerArrayBegin, + innerArrayBegin + 1, pos)) + .WillOnce(Return(&mpc)); + EXPECT_CALL(mpc, item(MatchesItem(ByRef(*val[3])), pos, pos + 9, pos + 9)) + .WillOnce(Return(&mpc)); + EXPECT_CALL(mpc, itemEnd(IsArrayOfSize(val.size()), encBegin, encBegin + 1, encEnd)) + .WillOnce(Return(&mpc)); + } + + EXPECT_CALL(mpc, error(_, _)) // + .Times(0); + + parse(encoded.data(), encoded.data() + encoded.size(), &mpc); +} + +TEST(StreamParseTest, Map) { + MockParseClient mpc; + + Map val("Hello", 4, Array(-9, "Goodbye"), std::numeric_limits::max()); + ASSERT_NE(val[1].first->asArray(), nullptr); + const Array& interior = *(val[1].first->asArray()); + auto encoded = val.encode(); + uint8_t* encBegin = encoded.data(); + uint8_t* encEnd = encoded.data() + encoded.size(); + + { + InSequence s; + const uint8_t* pos = encBegin; + EXPECT_CALL(mpc, item(_, pos, pos + 1, pos + 1)).WillOnce(Return(&mpc)); + ++pos; + EXPECT_CALL(mpc, item(MatchesItem(ByRef(*val[0].first)), pos, pos + 1, pos + 6)) + .WillOnce(Return(&mpc)); + pos += 6; + EXPECT_CALL(mpc, item(MatchesItem(ByRef(*val[0].second)), pos, pos + 1, pos + 1)) + .WillOnce(Return(&mpc)); + ++pos; + const uint8_t* innerArrayBegin = pos; + EXPECT_CALL(mpc, item(IsArrayOfSize(interior.size()), pos, pos + 1, pos + 1)) + .WillOnce(Return(&mpc)); + ++pos; + EXPECT_CALL(mpc, item(MatchesItem(ByRef(*interior[0])), pos, pos + 1, pos + 1)) + .WillOnce(Return(&mpc)); + ++pos; + EXPECT_CALL(mpc, item(MatchesItem(ByRef(*interior[1])), pos, pos + 1, pos + 8)) + .WillOnce(Return(&mpc)); + pos += 8; + EXPECT_CALL(mpc, itemEnd(IsArrayOfSize(interior.size()), innerArrayBegin, + innerArrayBegin + 1, pos)) + .WillOnce(Return(&mpc)); + EXPECT_CALL(mpc, item(MatchesItem(ByRef(*val[1].second)), pos, pos + 9, pos + 9)) + .WillOnce(Return(&mpc)); + EXPECT_CALL(mpc, itemEnd(IsMapOfSize(val.size()), encBegin, encBegin + 1, encEnd)) + .WillOnce(Return(&mpc)); + } + + EXPECT_CALL(mpc, error(_, _)) // + .Times(0); + + parse(encoded.data(), encoded.data() + encoded.size(), &mpc); +} + +TEST(StreamParseTest, Semantic) { + MockParseClient mpc; + + vector encoded; + auto iter = back_inserter(encoded); + encodeHeader(SEMANTIC, 0, iter); + Uint(999).encode(iter); + + EXPECT_CALL(mpc, item(_, _, _, _)).Times(0); + EXPECT_CALL(mpc, itemEnd(_, _, _, _)).Times(0); + EXPECT_CALL(mpc, error(encoded.data(), "Semantic tags not supported")); + + parse(encoded.data(), encoded.data() + encoded.size(), &mpc); +} + +TEST(FullParserTest, Uint) { + Uint val(10); + + auto [item, pos, message] = parse(val.encode()); + EXPECT_THAT(item, MatchesItem(val)); +} + +TEST(FullParserTest, Nint) { + Nint val(-10); + + auto [item, pos, message] = parse(val.encode()); + EXPECT_THAT(item, MatchesItem(val)); + + vector minNint = {0x3B, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + + std::tie(item, pos, message) = parse(minNint); + EXPECT_THAT(item, NotNull()); + EXPECT_EQ(item->asNint()->value(), std::numeric_limits::min()); +} + +TEST(FullParserTest, NintOutOfRange) { + vector outOfRangeNint = {0x3B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + + auto [item, pos, message] = parse(outOfRangeNint); + EXPECT_THAT(item, IsNull()); + EXPECT_EQ(pos, outOfRangeNint.data()); + EXPECT_EQ(message, "NINT values that don't fit in int64_t are not supported."); +} + +TEST(FullParserTest, Tstr) { + Tstr val("Hello"); + + auto [item, pos, message] = parse(val.encode()); + EXPECT_THAT(item, MatchesItem(val)); +} + +TEST(FullParserTest, Bstr) { + Bstr val("\x00\x01\0x02"s); + + auto [item, pos, message] = parse(val.encode()); + EXPECT_THAT(item, MatchesItem(val)); +} + +TEST(FullParserTest, Array) { + Array val("hello", -4, 3); + + auto encoded = val.encode(); + auto [item, pos, message] = parse(encoded); + EXPECT_THAT(item, MatchesItem(ByRef(val))); + EXPECT_EQ(pos, encoded.data() + encoded.size()); + EXPECT_EQ("", message); + + // We've already checked it all, but walk it just for fun. + ASSERT_NE(nullptr, item->asArray()); + const Array& arr = *(item->asArray()); + ASSERT_EQ(arr[0]->type(), TSTR); + EXPECT_EQ(arr[0]->asTstr()->value(), "hello"); +} + +TEST(FullParserTest, Map) { + Map val("hello", -4, 3, Bstr("hi")); + + auto [item, pos, message] = parse(val.encode()); + EXPECT_THAT(item, MatchesItem(ByRef(val))); +} + +TEST(FullParserTest, Complex) { + vector vec = {0x01, 0x02, 0x08, 0x03}; + Map val("Outer1", + Array(Map("Inner1", 99, // + "Inner2", vec), + "foo"), + "Outer2", 10); + + std::unique_ptr item; + const uint8_t* pos; + std::string message; + std::tie(item, pos, message) = parse(val.encode()); + EXPECT_THAT(item, MatchesItem(ByRef(val))); +} + +TEST(FullParserTest, IncompleteUint) { + Uint val(1000); + + auto encoding = val.encode(); + auto [item, pos, message] = parse(encoding.data(), encoding.size() - 1); + EXPECT_EQ(nullptr, item.get()); + EXPECT_EQ(encoding.data(), pos); + EXPECT_EQ("Need 2 byte(s) for length field, have 1.", message); +} + +TEST(FullParserTest, IncompleteString) { + Tstr val("hello"); + + auto encoding = val.encode(); + auto [item, pos, message] = parse(encoding.data(), encoding.size() - 2); + EXPECT_EQ(nullptr, item.get()); + EXPECT_EQ(encoding.data(), pos); + EXPECT_EQ("Need 5 byte(s) for text string, have 3.", message); +} + +TEST(FullParserTest, ArrayWithInsufficientEntries) { + Array val(1, 2, 3, 4); + + auto encoding = val.encode(); + auto [item, pos, message] = parse(encoding.data(), encoding.size() - 1); + EXPECT_EQ(nullptr, item.get()); + EXPECT_EQ(encoding.data(), pos); + EXPECT_EQ("Not enough entries for array.", message); +} + +TEST(FullParserTest, ArrayWithTruncatedEntry) { + Array val(1, 2, 3, 400000); + + auto encoding = val.encode(); + auto [item, pos, message] = parse(encoding.data(), encoding.size() - 1); + EXPECT_EQ(nullptr, item.get()); + EXPECT_EQ(encoding.data() + encoding.size() - 5, pos); + EXPECT_EQ("Need 4 byte(s) for length field, have 3.", message); +} + +TEST(FullParserTest, MapWithTruncatedEntry) { + Map val(1, 2, 300000, 4); + + auto encoding = val.encode(); + auto [item, pos, message] = parse(encoding.data(), encoding.size() - 2); + EXPECT_EQ(nullptr, item.get()); + EXPECT_EQ(encoding.data() + 3, pos); + EXPECT_EQ("Need 4 byte(s) for length field, have 3.", message); +} +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}