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(); +}