| /* |
| * 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. |
| */ |
| |
| #include "EicPresentation.h" |
| #include "EicCommon.h" |
| #include "EicSession.h" |
| |
| #include <inttypes.h> |
| |
| // Global used for assigning ids for presentation objects. |
| // |
| static uint32_t gPresentationLastIdAssigned = 0; |
| |
| bool eicPresentationInit(EicPresentation* ctx, uint32_t sessionId, bool testCredential, |
| const char* docType, size_t docTypeLength, |
| const uint8_t* encryptedCredentialKeys, |
| size_t encryptedCredentialKeysSize) { |
| uint8_t credentialKeys[EIC_CREDENTIAL_KEYS_CBOR_SIZE_FEATURE_VERSION_202101]; |
| bool expectPopSha256 = false; |
| |
| // For feature version 202009 it's 52 bytes long and for feature version 202101 it's 86 |
| // bytes (the additional data is the ProofOfProvisioning SHA-256). We need |
| // to support loading all feature versions. |
| // |
| if (encryptedCredentialKeysSize == EIC_CREDENTIAL_KEYS_CBOR_SIZE_FEATURE_VERSION_202009 + 28) { |
| /* do nothing */ |
| } else if (encryptedCredentialKeysSize == EIC_CREDENTIAL_KEYS_CBOR_SIZE_FEATURE_VERSION_202101 + 28) { |
| expectPopSha256 = true; |
| } else { |
| eicDebug("Unexpected size %zd for encryptedCredentialKeys", encryptedCredentialKeysSize); |
| return false; |
| } |
| |
| eicMemSet(ctx, '\0', sizeof(EicPresentation)); |
| ctx->sessionId = sessionId; |
| |
| if (!eicNextId(&gPresentationLastIdAssigned)) { |
| eicDebug("Error getting id for object"); |
| return false; |
| } |
| ctx->id = gPresentationLastIdAssigned; |
| |
| if (!eicOpsDecryptAes128Gcm(eicOpsGetHardwareBoundKey(testCredential), encryptedCredentialKeys, |
| encryptedCredentialKeysSize, |
| // DocType is the additionalAuthenticatedData |
| (const uint8_t*)docType, docTypeLength, credentialKeys)) { |
| eicDebug("Error decrypting CredentialKeys"); |
| return false; |
| } |
| |
| // It's supposed to look like this; |
| // |
| // Feature version 202009: |
| // |
| // CredentialKeys = [ |
| // bstr, ; storageKey, a 128-bit AES key |
| // bstr, ; credentialPrivKey, the private key for credentialKey |
| // ] |
| // |
| // Feature version 202101: |
| // |
| // CredentialKeys = [ |
| // bstr, ; storageKey, a 128-bit AES key |
| // bstr, ; credentialPrivKey, the private key for credentialKey |
| // bstr ; proofOfProvisioning SHA-256 |
| // ] |
| // |
| // where storageKey is 16 bytes, credentialPrivateKey is 32 bytes, and proofOfProvisioning |
| // SHA-256 is 32 bytes. |
| // |
| if (credentialKeys[0] != (expectPopSha256 ? 0x83 : 0x82) || // array of two or three elements |
| credentialKeys[1] != 0x50 || // 16-byte bstr |
| credentialKeys[18] != 0x58 || credentialKeys[19] != 0x20) { // 32-byte bstr |
| eicDebug("Invalid CBOR for CredentialKeys"); |
| return false; |
| } |
| if (expectPopSha256) { |
| if (credentialKeys[52] != 0x58 || credentialKeys[53] != 0x20) { // 32-byte bstr |
| eicDebug("Invalid CBOR for CredentialKeys"); |
| return false; |
| } |
| } |
| eicMemCpy(ctx->storageKey, credentialKeys + 2, EIC_AES_128_KEY_SIZE); |
| eicMemCpy(ctx->credentialPrivateKey, credentialKeys + 20, EIC_P256_PRIV_KEY_SIZE); |
| ctx->testCredential = testCredential; |
| if (expectPopSha256) { |
| eicMemCpy(ctx->proofOfProvisioningSha256, credentialKeys + 54, EIC_SHA256_DIGEST_SIZE); |
| } |
| |
| eicDebug("Initialized presentation with id %" PRIu32, ctx->id); |
| return true; |
| } |
| |
| bool eicPresentationShutdown(EicPresentation* ctx) { |
| if (ctx->id == 0) { |
| eicDebug("Trying to shut down presentation with id 0"); |
| return false; |
| } |
| eicDebug("Shut down presentation with id %" PRIu32, ctx->id); |
| eicMemSet(ctx, '\0', sizeof(EicPresentation)); |
| return true; |
| } |
| |
| bool eicPresentationGetId(EicPresentation* ctx, uint32_t* outId) { |
| *outId = ctx->id; |
| return true; |
| } |
| |
| bool eicPresentationGenerateSigningKeyPair(EicPresentation* ctx, const char* docType, |
| size_t docTypeLength, time_t now, |
| uint8_t* publicKeyCert, size_t* publicKeyCertSize, |
| uint8_t signingKeyBlob[60]) { |
| uint8_t signingKeyPriv[EIC_P256_PRIV_KEY_SIZE]; |
| uint8_t signingKeyPub[EIC_P256_PUB_KEY_SIZE]; |
| uint8_t cborBuf[64]; |
| |
| // Generate the ProofOfBinding CBOR to include in the X.509 certificate in |
| // IdentityCredentialAuthenticationKeyExtension CBOR. This CBOR is defined |
| // by the following CDDL |
| // |
| // ProofOfBinding = [ |
| // "ProofOfBinding", |
| // bstr, // Contains the SHA-256 of ProofOfProvisioning |
| // ] |
| // |
| // This array may grow in the future if other information needs to be |
| // conveyed. |
| // |
| // The bytes of ProofOfBinding is is represented as an OCTET_STRING |
| // and stored at OID 1.3.6.1.4.1.11129.2.1.26. |
| // |
| |
| EicCbor cbor; |
| eicCborInit(&cbor, cborBuf, sizeof cborBuf); |
| eicCborAppendArray(&cbor, 2); |
| eicCborAppendStringZ(&cbor, "ProofOfBinding"); |
| eicCborAppendByteString(&cbor, ctx->proofOfProvisioningSha256, EIC_SHA256_DIGEST_SIZE); |
| if (cbor.size > sizeof(cborBuf)) { |
| eicDebug("Exceeded buffer size"); |
| return false; |
| } |
| const uint8_t* proofOfBinding = cborBuf; |
| size_t proofOfBindingSize = cbor.size; |
| |
| if (!eicOpsCreateEcKey(signingKeyPriv, signingKeyPub)) { |
| eicDebug("Error creating signing key"); |
| return false; |
| } |
| |
| const int secondsInOneYear = 365 * 24 * 60 * 60; |
| time_t validityNotBefore = now; |
| time_t validityNotAfter = now + secondsInOneYear; // One year from now. |
| if (!eicOpsSignEcKey(signingKeyPub, ctx->credentialPrivateKey, 1, |
| "Android Identity Credential Key", // issuer CN |
| "Android Identity Credential Authentication Key", // subject CN |
| validityNotBefore, validityNotAfter, proofOfBinding, proofOfBindingSize, |
| publicKeyCert, publicKeyCertSize)) { |
| eicDebug("Error creating certificate for signing key"); |
| return false; |
| } |
| |
| uint8_t nonce[12]; |
| if (!eicOpsRandom(nonce, 12)) { |
| eicDebug("Error getting random"); |
| return false; |
| } |
| if (!eicOpsEncryptAes128Gcm(ctx->storageKey, nonce, signingKeyPriv, sizeof(signingKeyPriv), |
| // DocType is the additionalAuthenticatedData |
| (const uint8_t*)docType, docTypeLength, signingKeyBlob)) { |
| eicDebug("Error encrypting signing key"); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool eicPresentationCreateEphemeralKeyPair(EicPresentation* ctx, |
| uint8_t ephemeralPrivateKey[EIC_P256_PRIV_KEY_SIZE]) { |
| uint8_t ephemeralPublicKey[EIC_P256_PUB_KEY_SIZE]; |
| if (!eicOpsCreateEcKey(ctx->ephemeralPrivateKey, ephemeralPublicKey)) { |
| eicDebug("Error creating ephemeral key"); |
| return false; |
| } |
| eicMemCpy(ephemeralPrivateKey, ctx->ephemeralPrivateKey, EIC_P256_PRIV_KEY_SIZE); |
| return true; |
| } |
| |
| bool eicPresentationCreateAuthChallenge(EicPresentation* ctx, uint64_t* authChallenge) { |
| do { |
| if (!eicOpsRandom((uint8_t*)&(ctx->authChallenge), sizeof(uint64_t))) { |
| eicDebug("Failed generating random challenge"); |
| return false; |
| } |
| } while (ctx->authChallenge == EIC_KM_AUTH_CHALLENGE_UNSET); |
| eicDebug("Created auth challenge %" PRIu64, ctx->authChallenge); |
| *authChallenge = ctx->authChallenge; |
| return true; |
| } |
| |
| // From "COSE Algorithms" registry |
| // |
| #define COSE_ALG_ECDSA_256 -7 |
| |
| bool eicPresentationValidateRequestMessage(EicPresentation* ctx, const uint8_t* sessionTranscript, |
| size_t sessionTranscriptSize, |
| const uint8_t* requestMessage, size_t requestMessageSize, |
| int coseSignAlg, |
| const uint8_t* readerSignatureOfToBeSigned, |
| size_t readerSignatureOfToBeSignedSize) { |
| if (ctx->sessionId != 0) { |
| EicSession* session = eicSessionGetForId(ctx->sessionId); |
| if (session == NULL) { |
| eicDebug("Error looking up session for sessionId %" PRIu32, ctx->sessionId); |
| return false; |
| } |
| EicSha256Ctx sha256; |
| uint8_t sessionTranscriptSha256[EIC_SHA256_DIGEST_SIZE]; |
| eicOpsSha256Init(&sha256); |
| eicOpsSha256Update(&sha256, sessionTranscript, sessionTranscriptSize); |
| eicOpsSha256Final(&sha256, sessionTranscriptSha256); |
| if (eicCryptoMemCmp(sessionTranscriptSha256, session->sessionTranscriptSha256, |
| EIC_SHA256_DIGEST_SIZE) != 0) { |
| eicDebug("SessionTranscript mismatch"); |
| return false; |
| } |
| } |
| |
| if (ctx->readerPublicKeySize == 0) { |
| eicDebug("No public key for reader"); |
| return false; |
| } |
| |
| // Right now we only support ECDSA with SHA-256 (e.g. ES256). |
| // |
| if (coseSignAlg != COSE_ALG_ECDSA_256) { |
| eicDebug( |
| "COSE Signature algorithm for reader signature is %d, " |
| "only ECDSA with SHA-256 is supported right now", |
| coseSignAlg); |
| return false; |
| } |
| |
| // What we're going to verify is the COSE ToBeSigned structure which |
| // looks like the following: |
| // |
| // Sig_structure = [ |
| // context : "Signature" / "Signature1" / "CounterSignature", |
| // body_protected : empty_or_serialized_map, |
| // ? sign_protected : empty_or_serialized_map, |
| // external_aad : bstr, |
| // payload : bstr |
| // ] |
| // |
| // So we're going to build that CBOR... |
| // |
| EicCbor cbor; |
| eicCborInit(&cbor, NULL, 0); |
| eicCborAppendArray(&cbor, 4); |
| eicCborAppendStringZ(&cbor, "Signature1"); |
| |
| // The COSE Encoded protected headers is just a single field with |
| // COSE_LABEL_ALG (1) -> coseSignAlg (e.g. -7). For simplicitly we just |
| // hard-code the CBOR encoding: |
| static const uint8_t coseEncodedProtectedHeaders[] = {0xa1, 0x01, 0x26}; |
| eicCborAppendByteString(&cbor, coseEncodedProtectedHeaders, |
| sizeof(coseEncodedProtectedHeaders)); |
| |
| // External_aad is the empty bstr |
| static const uint8_t externalAad[0] = {}; |
| eicCborAppendByteString(&cbor, externalAad, sizeof(externalAad)); |
| |
| // For the payload, the _encoded_ form follows here. We handle this by simply |
| // opening a bstr, and then writing the CBOR. This requires us to know the |
| // size of said bstr, ahead of time... the CBOR to be written is |
| // |
| // ReaderAuthentication = [ |
| // "ReaderAuthentication", |
| // SessionTranscript, |
| // ItemsRequestBytes |
| // ] |
| // |
| // ItemsRequestBytes = #6.24(bstr .cbor ItemsRequest) |
| // |
| // ReaderAuthenticationBytes = #6.24(bstr .cbor ReaderAuthentication) |
| // |
| // which is easily calculated below |
| // |
| size_t calculatedSize = 0; |
| calculatedSize += 1; // Array of size 3 |
| calculatedSize += 1; // "ReaderAuthentication" less than 24 bytes |
| calculatedSize += sizeof("ReaderAuthentication") - 1; // Don't include trailing NUL |
| calculatedSize += sessionTranscriptSize; // Already CBOR encoded |
| calculatedSize += 2; // Semantic tag EIC_CBOR_SEMANTIC_TAG_ENCODED_CBOR (24) |
| calculatedSize += 1 + eicCborAdditionalLengthBytesFor(requestMessageSize); |
| calculatedSize += requestMessageSize; |
| |
| // However note that we're authenticating ReaderAuthenticationBytes which |
| // is a tagged bstr of the bytes of ReaderAuthentication. So need to get |
| // that in front. |
| size_t rabCalculatedSize = 0; |
| rabCalculatedSize += 2; // Semantic tag EIC_CBOR_SEMANTIC_TAG_ENCODED_CBOR (24) |
| rabCalculatedSize += 1 + eicCborAdditionalLengthBytesFor(calculatedSize); |
| rabCalculatedSize += calculatedSize; |
| |
| // Begin the bytestring for ReaderAuthenticationBytes; |
| eicCborBegin(&cbor, EIC_CBOR_MAJOR_TYPE_BYTE_STRING, rabCalculatedSize); |
| |
| eicCborAppendSemantic(&cbor, EIC_CBOR_SEMANTIC_TAG_ENCODED_CBOR); |
| |
| // Begins the bytestring for ReaderAuthentication; |
| eicCborBegin(&cbor, EIC_CBOR_MAJOR_TYPE_BYTE_STRING, calculatedSize); |
| |
| // And now that we know the size, let's fill it in... |
| // |
| size_t payloadOffset = cbor.size; |
| eicCborBegin(&cbor, EIC_CBOR_MAJOR_TYPE_ARRAY, 3); |
| eicCborAppendStringZ(&cbor, "ReaderAuthentication"); |
| eicCborAppend(&cbor, sessionTranscript, sessionTranscriptSize); |
| eicCborAppendSemantic(&cbor, EIC_CBOR_SEMANTIC_TAG_ENCODED_CBOR); |
| eicCborBegin(&cbor, EIC_CBOR_MAJOR_TYPE_BYTE_STRING, requestMessageSize); |
| eicCborAppend(&cbor, requestMessage, requestMessageSize); |
| |
| if (cbor.size != payloadOffset + calculatedSize) { |
| eicDebug("CBOR size is %zd but we expected %zd", cbor.size, payloadOffset + calculatedSize); |
| return false; |
| } |
| uint8_t toBeSignedDigest[EIC_SHA256_DIGEST_SIZE]; |
| eicCborFinal(&cbor, toBeSignedDigest); |
| |
| if (!eicOpsEcDsaVerifyWithPublicKey( |
| toBeSignedDigest, EIC_SHA256_DIGEST_SIZE, readerSignatureOfToBeSigned, |
| readerSignatureOfToBeSignedSize, ctx->readerPublicKey, ctx->readerPublicKeySize)) { |
| eicDebug("Request message is not signed by public key"); |
| return false; |
| } |
| ctx->requestMessageValidated = true; |
| return true; |
| } |
| |
| // Validates the next certificate in the reader certificate chain. |
| bool eicPresentationPushReaderCert(EicPresentation* ctx, const uint8_t* certX509, |
| size_t certX509Size) { |
| // If we had a previous certificate, use its public key to validate this certificate. |
| if (ctx->readerPublicKeySize > 0) { |
| if (!eicOpsX509CertSignedByPublicKey(certX509, certX509Size, ctx->readerPublicKey, |
| ctx->readerPublicKeySize)) { |
| eicDebug("Certificate is not signed by public key in the previous certificate"); |
| return false; |
| } |
| } |
| |
| // Store the key of this certificate, this is used to validate the next certificate |
| // and also ACPs with certificates that use the same public key... |
| ctx->readerPublicKeySize = EIC_PRESENTATION_MAX_READER_PUBLIC_KEY_SIZE; |
| if (!eicOpsX509GetPublicKey(certX509, certX509Size, ctx->readerPublicKey, |
| &ctx->readerPublicKeySize)) { |
| eicDebug("Error extracting public key from certificate"); |
| return false; |
| } |
| if (ctx->readerPublicKeySize == 0) { |
| eicDebug("Zero-length public key in certificate"); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static bool getChallenge(EicPresentation* ctx, uint64_t* outAuthChallenge) { |
| // Use authChallenge from session if applicable. |
| *outAuthChallenge = ctx->authChallenge; |
| if (ctx->sessionId != 0) { |
| EicSession* session = eicSessionGetForId(ctx->sessionId); |
| if (session == NULL) { |
| eicDebug("Error looking up session for sessionId %" PRIu32, ctx->sessionId); |
| return false; |
| } |
| *outAuthChallenge = session->authChallenge; |
| } |
| return true; |
| } |
| |
| bool eicPresentationSetAuthToken(EicPresentation* ctx, uint64_t challenge, uint64_t secureUserId, |
| uint64_t authenticatorId, int hardwareAuthenticatorType, |
| uint64_t timeStamp, const uint8_t* mac, size_t macSize, |
| uint64_t verificationTokenChallenge, |
| uint64_t verificationTokenTimestamp, |
| int verificationTokenSecurityLevel, |
| const uint8_t* verificationTokenMac, |
| size_t verificationTokenMacSize) { |
| uint64_t authChallenge; |
| if (!getChallenge(ctx, &authChallenge)) { |
| return false; |
| } |
| |
| // It doesn't make sense to accept any tokens if eicPresentationCreateAuthChallenge() |
| // was never called. |
| if (authChallenge == EIC_KM_AUTH_CHALLENGE_UNSET) { |
| eicDebug("Trying to validate tokens when no auth-challenge was previously generated"); |
| return false; |
| } |
| // At least the verification-token must have the same challenge as what was generated. |
| if (verificationTokenChallenge != authChallenge) { |
| eicDebug("Challenge in verification token does not match the challenge " |
| "previously generated"); |
| return false; |
| } |
| if (!eicOpsValidateAuthToken( |
| challenge, secureUserId, authenticatorId, hardwareAuthenticatorType, timeStamp, mac, |
| macSize, verificationTokenChallenge, verificationTokenTimestamp, |
| verificationTokenSecurityLevel, verificationTokenMac, verificationTokenMacSize)) { |
| eicDebug("Error validating authToken"); |
| return false; |
| } |
| ctx->authTokenChallenge = challenge; |
| ctx->authTokenSecureUserId = secureUserId; |
| ctx->authTokenTimestamp = timeStamp; |
| ctx->verificationTokenTimestamp = verificationTokenTimestamp; |
| return true; |
| } |
| |
| static bool checkUserAuth(EicPresentation* ctx, bool userAuthenticationRequired, int timeoutMillis, |
| uint64_t secureUserId) { |
| if (!userAuthenticationRequired) { |
| return true; |
| } |
| |
| if (secureUserId != ctx->authTokenSecureUserId) { |
| eicDebug("secureUserId in profile differs from userId in authToken"); |
| return false; |
| } |
| |
| // Only ACP with auth-on-every-presentation - those with timeout == 0 - need the |
| // challenge to match... |
| if (timeoutMillis == 0) { |
| uint64_t authChallenge; |
| if (!getChallenge(ctx, &authChallenge)) { |
| return false; |
| } |
| |
| if (ctx->authTokenChallenge != authChallenge) { |
| eicDebug("Challenge in authToken (%" PRIu64 |
| ") doesn't match the challenge " |
| "that was created (%" PRIu64 ") for this session", |
| ctx->authTokenChallenge, authChallenge); |
| return false; |
| } |
| } |
| |
| uint64_t now = ctx->verificationTokenTimestamp; |
| if (ctx->authTokenTimestamp > now) { |
| eicDebug("Timestamp in authToken is in the future"); |
| return false; |
| } |
| |
| if (timeoutMillis > 0) { |
| if (now > ctx->authTokenTimestamp + timeoutMillis) { |
| eicDebug("Deadline for authToken is in the past"); |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| static bool checkReaderAuth(EicPresentation* ctx, const uint8_t* readerCertificate, |
| size_t readerCertificateSize) { |
| uint8_t publicKey[EIC_PRESENTATION_MAX_READER_PUBLIC_KEY_SIZE]; |
| size_t publicKeySize; |
| |
| if (readerCertificateSize == 0) { |
| return true; |
| } |
| |
| // Remember in this case certificate equality is done by comparing public |
| // keys, not bitwise comparison of the certificates. |
| // |
| publicKeySize = EIC_PRESENTATION_MAX_READER_PUBLIC_KEY_SIZE; |
| if (!eicOpsX509GetPublicKey(readerCertificate, readerCertificateSize, publicKey, |
| &publicKeySize)) { |
| eicDebug("Error extracting public key from certificate"); |
| return false; |
| } |
| if (publicKeySize == 0) { |
| eicDebug("Zero-length public key in certificate"); |
| return false; |
| } |
| |
| if ((ctx->readerPublicKeySize != publicKeySize) || |
| (eicCryptoMemCmp(ctx->readerPublicKey, publicKey, ctx->readerPublicKeySize) != 0)) { |
| return false; |
| } |
| return true; |
| } |
| |
| // Note: This function returns false _only_ if an error occurred check for access, _not_ |
| // whether access is granted. Whether access is granted is returned in |accessGranted|. |
| // |
| bool eicPresentationValidateAccessControlProfile(EicPresentation* ctx, int id, |
| const uint8_t* readerCertificate, |
| size_t readerCertificateSize, |
| bool userAuthenticationRequired, int timeoutMillis, |
| uint64_t secureUserId, const uint8_t mac[28], |
| bool* accessGranted, |
| uint8_t* scratchSpace, |
| size_t scratchSpaceSize) { |
| *accessGranted = false; |
| if (id < 0 || id >= 32) { |
| eicDebug("id value of %d is out of allowed range [0, 32[", id); |
| return false; |
| } |
| |
| // Validate the MAC |
| EicCbor cborBuilder; |
| eicCborInit(&cborBuilder, scratchSpace, scratchSpaceSize); |
| if (!eicCborCalcAccessControl(&cborBuilder, id, readerCertificate, readerCertificateSize, |
| userAuthenticationRequired, timeoutMillis, secureUserId)) { |
| return false; |
| } |
| if (!eicOpsDecryptAes128Gcm(ctx->storageKey, mac, 28, cborBuilder.buffer, cborBuilder.size, |
| NULL)) { |
| eicDebug("MAC for AccessControlProfile doesn't match"); |
| return false; |
| } |
| |
| bool passedUserAuth = |
| checkUserAuth(ctx, userAuthenticationRequired, timeoutMillis, secureUserId); |
| bool passedReaderAuth = checkReaderAuth(ctx, readerCertificate, readerCertificateSize); |
| |
| ctx->accessControlProfileMaskValidated |= (1U << id); |
| if (readerCertificateSize > 0) { |
| ctx->accessControlProfileMaskUsesReaderAuth |= (1U << id); |
| } |
| if (!passedReaderAuth) { |
| ctx->accessControlProfileMaskFailedReaderAuth |= (1U << id); |
| } |
| if (!passedUserAuth) { |
| ctx->accessControlProfileMaskFailedUserAuth |= (1U << id); |
| } |
| |
| if (passedUserAuth && passedReaderAuth) { |
| *accessGranted = true; |
| eicDebug("Access granted for id %d", id); |
| } |
| return true; |
| } |
| |
| // Helper used to append the DeviceAuthencation prelude, used for both MACing and ECDSA signing. |
| static size_t appendDeviceAuthentication(EicCbor* cbor, const uint8_t* sessionTranscript, |
| size_t sessionTranscriptSize, const char* docType, |
| size_t docTypeLength, |
| size_t expectedDeviceNamespacesSize) { |
| // For the payload, the _encoded_ form follows here. We handle this by simply |
| // opening a bstr, and then writing the CBOR. This requires us to know the |
| // size of said bstr, ahead of time... the CBOR to be written is |
| // |
| // DeviceAuthentication = [ |
| // "DeviceAuthentication", |
| // SessionTranscript, |
| // DocType, ; DocType as used in Documents structure in OfflineResponse |
| // DeviceNameSpacesBytes |
| // ] |
| // |
| // DeviceNameSpacesBytes = #6.24(bstr .cbor DeviceNameSpaces) |
| // |
| // DeviceAuthenticationBytes = #6.24(bstr .cbor DeviceAuthentication) |
| // |
| // which is easily calculated below |
| // |
| size_t calculatedSize = 0; |
| calculatedSize += 1; // Array of size 4 |
| calculatedSize += 1; // "DeviceAuthentication" less than 24 bytes |
| calculatedSize += sizeof("DeviceAuthentication") - 1; // Don't include trailing NUL |
| calculatedSize += sessionTranscriptSize; // Already CBOR encoded |
| calculatedSize += 1 + eicCborAdditionalLengthBytesFor(docTypeLength) + docTypeLength; |
| calculatedSize += 2; // Semantic tag EIC_CBOR_SEMANTIC_TAG_ENCODED_CBOR (24) |
| calculatedSize += 1 + eicCborAdditionalLengthBytesFor(expectedDeviceNamespacesSize); |
| calculatedSize += expectedDeviceNamespacesSize; |
| |
| // However note that we're authenticating DeviceAuthenticationBytes which |
| // is a tagged bstr of the bytes of DeviceAuthentication. So need to get |
| // that in front. |
| size_t dabCalculatedSize = 0; |
| dabCalculatedSize += 2; // Semantic tag EIC_CBOR_SEMANTIC_TAG_ENCODED_CBOR (24) |
| dabCalculatedSize += 1 + eicCborAdditionalLengthBytesFor(calculatedSize); |
| dabCalculatedSize += calculatedSize; |
| |
| // Begin the bytestring for DeviceAuthenticationBytes; |
| eicCborBegin(cbor, EIC_CBOR_MAJOR_TYPE_BYTE_STRING, dabCalculatedSize); |
| |
| eicCborAppendSemantic(cbor, EIC_CBOR_SEMANTIC_TAG_ENCODED_CBOR); |
| |
| // Begins the bytestring for DeviceAuthentication; |
| eicCborBegin(cbor, EIC_CBOR_MAJOR_TYPE_BYTE_STRING, calculatedSize); |
| |
| eicCborAppendArray(cbor, 4); |
| eicCborAppendStringZ(cbor, "DeviceAuthentication"); |
| eicCborAppend(cbor, sessionTranscript, sessionTranscriptSize); |
| eicCborAppendString(cbor, docType, docTypeLength); |
| |
| // For the payload, the _encoded_ form follows here. We handle this by simply |
| // opening a bstr, and then writing the CBOR. This requires us to know the |
| // size of said bstr, ahead of time. |
| eicCborAppendSemantic(cbor, EIC_CBOR_SEMANTIC_TAG_ENCODED_CBOR); |
| eicCborBegin(cbor, EIC_CBOR_MAJOR_TYPE_BYTE_STRING, expectedDeviceNamespacesSize); |
| size_t expectedCborSizeAtEnd = expectedDeviceNamespacesSize + cbor->size; |
| |
| return expectedCborSizeAtEnd; |
| } |
| |
| bool eicPresentationPrepareDeviceAuthentication( |
| EicPresentation* ctx, const uint8_t* sessionTranscript, size_t sessionTranscriptSize, |
| const uint8_t* readerEphemeralPublicKey, size_t readerEphemeralPublicKeySize, |
| const uint8_t signingKeyBlob[60], const char* docType, size_t docTypeLength, |
| unsigned int numNamespacesWithValues, size_t expectedDeviceNamespacesSize) { |
| if (ctx->sessionId != 0) { |
| if (readerEphemeralPublicKeySize != 0) { |
| eicDebug("In a session but readerEphemeralPublicKeySize is non-zero"); |
| return false; |
| } |
| EicSession* session = eicSessionGetForId(ctx->sessionId); |
| if (session == NULL) { |
| eicDebug("Error looking up session for sessionId %" PRIu32, ctx->sessionId); |
| return false; |
| } |
| EicSha256Ctx sha256; |
| uint8_t sessionTranscriptSha256[EIC_SHA256_DIGEST_SIZE]; |
| eicOpsSha256Init(&sha256); |
| eicOpsSha256Update(&sha256, sessionTranscript, sessionTranscriptSize); |
| eicOpsSha256Final(&sha256, sessionTranscriptSha256); |
| if (eicCryptoMemCmp(sessionTranscriptSha256, session->sessionTranscriptSha256, |
| EIC_SHA256_DIGEST_SIZE) != 0) { |
| eicDebug("SessionTranscript mismatch"); |
| return false; |
| } |
| readerEphemeralPublicKey = session->readerEphemeralPublicKey; |
| readerEphemeralPublicKeySize = session->readerEphemeralPublicKeySize; |
| } |
| |
| // Stash the decrypted DeviceKey in context since we'll need it later in |
| // eicPresentationFinishRetrievalWithSignature() |
| if (!eicOpsDecryptAes128Gcm(ctx->storageKey, signingKeyBlob, 60, (const uint8_t*)docType, |
| docTypeLength, ctx->deviceKeyPriv)) { |
| eicDebug("Error decrypting signingKeyBlob"); |
| return false; |
| } |
| |
| // We can only do MACing if EReaderKey has been set... it might not have been set if for |
| // example mdoc session encryption isn't in use. In that case we can still do ECDSA |
| if (readerEphemeralPublicKeySize > 0) { |
| if (readerEphemeralPublicKeySize != EIC_P256_PUB_KEY_SIZE) { |
| eicDebug("Unexpected size %zd for readerEphemeralPublicKeySize", |
| readerEphemeralPublicKeySize); |
| return false; |
| } |
| |
| uint8_t sharedSecret[EIC_P256_COORDINATE_SIZE]; |
| if (!eicOpsEcdh(readerEphemeralPublicKey, ctx->deviceKeyPriv, sharedSecret)) { |
| eicDebug("ECDH failed"); |
| return false; |
| } |
| |
| EicCbor cbor; |
| eicCborInit(&cbor, NULL, 0); |
| eicCborAppendSemantic(&cbor, EIC_CBOR_SEMANTIC_TAG_ENCODED_CBOR); |
| eicCborAppendByteString(&cbor, sessionTranscript, sessionTranscriptSize); |
| uint8_t salt[EIC_SHA256_DIGEST_SIZE]; |
| eicCborFinal(&cbor, salt); |
| |
| const uint8_t info[7] = {'E', 'M', 'a', 'c', 'K', 'e', 'y'}; |
| uint8_t derivedKey[32]; |
| if (!eicOpsHkdf(sharedSecret, EIC_P256_COORDINATE_SIZE, salt, sizeof(salt), info, |
| sizeof(info), derivedKey, sizeof(derivedKey))) { |
| eicDebug("HKDF failed"); |
| return false; |
| } |
| |
| eicCborInitHmacSha256(&ctx->cbor, NULL, 0, derivedKey, sizeof(derivedKey)); |
| |
| // What we're going to calculate the HMAC-SHA256 is the COSE ToBeMaced |
| // structure which looks like the following: |
| // |
| // MAC_structure = [ |
| // context : "MAC" / "MAC0", |
| // protected : empty_or_serialized_map, |
| // external_aad : bstr, |
| // payload : bstr |
| // ] |
| // |
| eicCborAppendArray(&ctx->cbor, 4); |
| eicCborAppendStringZ(&ctx->cbor, "MAC0"); |
| |
| // The COSE Encoded protected headers is just a single field with |
| // COSE_LABEL_ALG (1) -> COSE_ALG_HMAC_256_256 (5). For simplicitly we just |
| // hard-code the CBOR encoding: |
| static const uint8_t coseEncodedProtectedHeaders[] = {0xa1, 0x01, 0x05}; |
| eicCborAppendByteString(&ctx->cbor, coseEncodedProtectedHeaders, |
| sizeof(coseEncodedProtectedHeaders)); |
| |
| // We currently don't support Externally Supplied Data (RFC 8152 section 4.3) |
| // so external_aad is the empty bstr |
| static const uint8_t externalAad[0] = {}; |
| eicCborAppendByteString(&ctx->cbor, externalAad, sizeof(externalAad)); |
| |
| // Append DeviceAuthentication prelude and open the DeviceSigned map... |
| ctx->expectedCborSizeAtEnd = |
| appendDeviceAuthentication(&ctx->cbor, sessionTranscript, sessionTranscriptSize, |
| docType, docTypeLength, expectedDeviceNamespacesSize); |
| eicCborAppendMap(&ctx->cbor, numNamespacesWithValues); |
| ctx->buildCbor = true; |
| } |
| |
| // Now do the same for ECDSA signatures... |
| // |
| eicCborInit(&ctx->cborEcdsa, NULL, 0); |
| eicCborAppendArray(&ctx->cborEcdsa, 4); |
| eicCborAppendStringZ(&ctx->cborEcdsa, "Signature1"); |
| static const uint8_t coseEncodedProtectedHeadersEcdsa[] = {0xa1, 0x01, 0x26}; |
| eicCborAppendByteString(&ctx->cborEcdsa, coseEncodedProtectedHeadersEcdsa, |
| sizeof(coseEncodedProtectedHeadersEcdsa)); |
| static const uint8_t externalAadEcdsa[0] = {}; |
| eicCborAppendByteString(&ctx->cborEcdsa, externalAadEcdsa, sizeof(externalAadEcdsa)); |
| |
| // Append DeviceAuthentication prelude and open the DeviceSigned map... |
| ctx->expectedCborEcdsaSizeAtEnd = |
| appendDeviceAuthentication(&ctx->cborEcdsa, sessionTranscript, sessionTranscriptSize, |
| docType, docTypeLength, expectedDeviceNamespacesSize); |
| eicCborAppendMap(&ctx->cborEcdsa, numNamespacesWithValues); |
| ctx->buildCborEcdsa = true; |
| |
| return true; |
| } |
| |
| bool eicPresentationStartRetrieveEntries(EicPresentation* ctx) { |
| // HAL may use this object multiple times to retrieve data so need to reset various |
| // state objects here. |
| ctx->requestMessageValidated = false; |
| ctx->buildCbor = false; |
| ctx->buildCborEcdsa = false; |
| ctx->accessControlProfileMaskValidated = 0; |
| ctx->accessControlProfileMaskUsesReaderAuth = 0; |
| ctx->accessControlProfileMaskFailedReaderAuth = 0; |
| ctx->accessControlProfileMaskFailedUserAuth = 0; |
| ctx->readerPublicKeySize = 0; |
| return true; |
| } |
| |
| EicAccessCheckResult eicPresentationStartRetrieveEntryValue( |
| EicPresentation* ctx, const char* nameSpace, size_t nameSpaceLength, |
| const char* name, size_t nameLength, |
| unsigned int newNamespaceNumEntries, int32_t entrySize, |
| const uint8_t* accessControlProfileIds, size_t numAccessControlProfileIds, |
| uint8_t* scratchSpace, size_t scratchSpaceSize) { |
| (void)entrySize; |
| uint8_t* additionalDataCbor = scratchSpace; |
| size_t additionalDataCborBufferSize = scratchSpaceSize; |
| size_t additionalDataCborSize; |
| |
| if (newNamespaceNumEntries > 0) { |
| eicCborAppendString(&ctx->cbor, nameSpace, nameSpaceLength); |
| eicCborAppendMap(&ctx->cbor, newNamespaceNumEntries); |
| |
| eicCborAppendString(&ctx->cborEcdsa, nameSpace, nameSpaceLength); |
| eicCborAppendMap(&ctx->cborEcdsa, newNamespaceNumEntries); |
| } |
| |
| // We'll need to calc and store a digest of additionalData to check that it's the same |
| // additionalData being passed in for every eicPresentationRetrieveEntryValue() call... |
| // |
| ctx->accessCheckOk = false; |
| if (!eicCborCalcEntryAdditionalData(accessControlProfileIds, numAccessControlProfileIds, |
| nameSpace, nameSpaceLength, name, nameLength, |
| additionalDataCbor, additionalDataCborBufferSize, |
| &additionalDataCborSize, |
| ctx->additionalDataSha256)) { |
| return EIC_ACCESS_CHECK_RESULT_FAILED; |
| } |
| |
| if (numAccessControlProfileIds == 0) { |
| return EIC_ACCESS_CHECK_RESULT_NO_ACCESS_CONTROL_PROFILES; |
| } |
| |
| // Access is granted if at least one of the profiles grants access. |
| // |
| // If an item is configured without any profiles, access is denied. |
| // |
| EicAccessCheckResult result = EIC_ACCESS_CHECK_RESULT_FAILED; |
| for (size_t n = 0; n < numAccessControlProfileIds; n++) { |
| int id = accessControlProfileIds[n]; |
| uint32_t idBitMask = (1 << id); |
| |
| // If the access control profile wasn't validated, this is an error and we |
| // fail immediately. |
| bool validated = ((ctx->accessControlProfileMaskValidated & idBitMask) != 0); |
| if (!validated) { |
| eicDebug("No ACP for profile id %d", id); |
| return EIC_ACCESS_CHECK_RESULT_FAILED; |
| } |
| |
| // Otherwise, we _did_ validate the profile. If none of the checks |
| // failed, we're done |
| bool failedUserAuth = ((ctx->accessControlProfileMaskFailedUserAuth & idBitMask) != 0); |
| bool failedReaderAuth = ((ctx->accessControlProfileMaskFailedReaderAuth & idBitMask) != 0); |
| if (!failedUserAuth && !failedReaderAuth) { |
| result = EIC_ACCESS_CHECK_RESULT_OK; |
| break; |
| } |
| // One of the checks failed, convey which one |
| if (failedUserAuth) { |
| result = EIC_ACCESS_CHECK_RESULT_USER_AUTHENTICATION_FAILED; |
| } else { |
| result = EIC_ACCESS_CHECK_RESULT_READER_AUTHENTICATION_FAILED; |
| } |
| } |
| eicDebug("Result %d for name %s", result, name); |
| |
| if (result == EIC_ACCESS_CHECK_RESULT_OK) { |
| eicCborAppendString(&ctx->cbor, name, nameLength); |
| eicCborAppendString(&ctx->cborEcdsa, name, nameLength); |
| ctx->accessCheckOk = true; |
| } |
| return result; |
| } |
| |
| // Note: |content| must be big enough to hold |encryptedContentSize| - 28 bytes. |
| bool eicPresentationRetrieveEntryValue(EicPresentation* ctx, const uint8_t* encryptedContent, |
| size_t encryptedContentSize, uint8_t* content, |
| const char* nameSpace, size_t nameSpaceLength, |
| const char* name, size_t nameLength, |
| const uint8_t* accessControlProfileIds, |
| size_t numAccessControlProfileIds, |
| uint8_t* scratchSpace, |
| size_t scratchSpaceSize) { |
| uint8_t* additionalDataCbor = scratchSpace; |
| size_t additionalDataCborBufferSize = scratchSpaceSize; |
| size_t additionalDataCborSize; |
| |
| uint8_t calculatedSha256[EIC_SHA256_DIGEST_SIZE]; |
| if (!eicCborCalcEntryAdditionalData(accessControlProfileIds, numAccessControlProfileIds, |
| nameSpace, nameSpaceLength, name, nameLength, |
| additionalDataCbor, additionalDataCborBufferSize, |
| &additionalDataCborSize, |
| calculatedSha256)) { |
| return false; |
| } |
| |
| if (eicCryptoMemCmp(calculatedSha256, ctx->additionalDataSha256, EIC_SHA256_DIGEST_SIZE) != 0) { |
| eicDebug("SHA-256 mismatch of additionalData"); |
| return false; |
| } |
| if (!ctx->accessCheckOk) { |
| eicDebug("Attempting to retrieve a value for which access is not granted"); |
| return false; |
| } |
| |
| if (!eicOpsDecryptAes128Gcm(ctx->storageKey, encryptedContent, encryptedContentSize, |
| additionalDataCbor, additionalDataCborSize, content)) { |
| eicDebug("Error decrypting content"); |
| return false; |
| } |
| |
| eicCborAppend(&ctx->cbor, content, encryptedContentSize - 28); |
| eicCborAppend(&ctx->cborEcdsa, content, encryptedContentSize - 28); |
| |
| return true; |
| } |
| |
| bool eicPresentationFinishRetrieval(EicPresentation* ctx, uint8_t* digestToBeMaced, |
| size_t* digestToBeMacedSize) { |
| if (!ctx->buildCbor) { |
| *digestToBeMacedSize = 0; |
| return true; |
| } |
| if (*digestToBeMacedSize != 32) { |
| return false; |
| } |
| |
| // This verifies that the correct expectedDeviceNamespacesSize value was |
| // passed in at eicPresentationCalcMacKey() time. |
| if (ctx->cbor.size != ctx->expectedCborSizeAtEnd) { |
| eicDebug("CBOR size is %zd, was expecting %zd", ctx->cbor.size, ctx->expectedCborSizeAtEnd); |
| return false; |
| } |
| eicCborFinal(&ctx->cbor, digestToBeMaced); |
| |
| return true; |
| } |
| |
| bool eicPresentationFinishRetrievalWithSignature(EicPresentation* ctx, uint8_t* digestToBeMaced, |
| size_t* digestToBeMacedSize, |
| uint8_t* signatureOfToBeSigned, |
| size_t* signatureOfToBeSignedSize) { |
| if (!eicPresentationFinishRetrieval(ctx, digestToBeMaced, digestToBeMacedSize)) { |
| return false; |
| } |
| |
| if (!ctx->buildCborEcdsa) { |
| *signatureOfToBeSignedSize = 0; |
| return true; |
| } |
| if (*signatureOfToBeSignedSize != EIC_ECDSA_P256_SIGNATURE_SIZE) { |
| return false; |
| } |
| |
| // This verifies that the correct expectedDeviceNamespacesSize value was |
| // passed in at eicPresentationCalcMacKey() time. |
| if (ctx->cborEcdsa.size != ctx->expectedCborEcdsaSizeAtEnd) { |
| eicDebug("CBOR ECDSA size is %zd, was expecting %zd", ctx->cborEcdsa.size, |
| ctx->expectedCborEcdsaSizeAtEnd); |
| return false; |
| } |
| uint8_t cborSha256[EIC_SHA256_DIGEST_SIZE]; |
| eicCborFinal(&ctx->cborEcdsa, cborSha256); |
| if (!eicOpsEcDsa(ctx->deviceKeyPriv, cborSha256, signatureOfToBeSigned)) { |
| eicDebug("Error signing DeviceAuthentication"); |
| return false; |
| } |
| eicDebug("set the signature"); |
| return true; |
| } |
| |
| bool eicPresentationDeleteCredential(EicPresentation* ctx, const char* docType, size_t docTypeLength, |
| const uint8_t* challenge, size_t challengeSize, |
| bool includeChallenge, |
| size_t proofOfDeletionCborSize, |
| uint8_t signatureOfToBeSigned[EIC_ECDSA_P256_SIGNATURE_SIZE]) { |
| EicCbor cbor; |
| |
| eicCborInit(&cbor, NULL, 0); |
| |
| // What we're going to sign is the COSE ToBeSigned structure which |
| // looks like the following: |
| // |
| // Sig_structure = [ |
| // context : "Signature" / "Signature1" / "CounterSignature", |
| // body_protected : empty_or_serialized_map, |
| // ? sign_protected : empty_or_serialized_map, |
| // external_aad : bstr, |
| // payload : bstr |
| // ] |
| // |
| eicCborAppendArray(&cbor, 4); |
| eicCborAppendStringZ(&cbor, "Signature1"); |
| |
| // The COSE Encoded protected headers is just a single field with |
| // COSE_LABEL_ALG (1) -> COSE_ALG_ECSDA_256 (-7). For simplicitly we just |
| // hard-code the CBOR encoding: |
| static const uint8_t coseEncodedProtectedHeaders[] = {0xa1, 0x01, 0x26}; |
| eicCborAppendByteString(&cbor, coseEncodedProtectedHeaders, |
| sizeof(coseEncodedProtectedHeaders)); |
| |
| // We currently don't support Externally Supplied Data (RFC 8152 section 4.3) |
| // so external_aad is the empty bstr |
| static const uint8_t externalAad[0] = {}; |
| eicCborAppendByteString(&cbor, externalAad, sizeof(externalAad)); |
| |
| // For the payload, the _encoded_ form follows here. We handle this by simply |
| // opening a bstr, and then writing the CBOR. This requires us to know the |
| // size of said bstr, ahead of time. |
| eicCborBegin(&cbor, EIC_CBOR_MAJOR_TYPE_BYTE_STRING, proofOfDeletionCborSize); |
| |
| // Finally, the CBOR that we're actually signing. |
| eicCborAppendArray(&cbor, includeChallenge ? 4 : 3); |
| eicCborAppendStringZ(&cbor, "ProofOfDeletion"); |
| eicCborAppendString(&cbor, docType, docTypeLength); |
| if (includeChallenge) { |
| eicCborAppendByteString(&cbor, challenge, challengeSize); |
| } |
| eicCborAppendBool(&cbor, ctx->testCredential); |
| |
| uint8_t cborSha256[EIC_SHA256_DIGEST_SIZE]; |
| eicCborFinal(&cbor, cborSha256); |
| if (!eicOpsEcDsa(ctx->credentialPrivateKey, cborSha256, signatureOfToBeSigned)) { |
| eicDebug("Error signing proofOfDeletion"); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool eicPresentationProveOwnership(EicPresentation* ctx, const char* docType, |
| size_t docTypeLength, bool testCredential, |
| const uint8_t* challenge, size_t challengeSize, |
| size_t proofOfOwnershipCborSize, |
| uint8_t signatureOfToBeSigned[EIC_ECDSA_P256_SIGNATURE_SIZE]) { |
| EicCbor cbor; |
| |
| eicCborInit(&cbor, NULL, 0); |
| |
| // What we're going to sign is the COSE ToBeSigned structure which |
| // looks like the following: |
| // |
| // Sig_structure = [ |
| // context : "Signature" / "Signature1" / "CounterSignature", |
| // body_protected : empty_or_serialized_map, |
| // ? sign_protected : empty_or_serialized_map, |
| // external_aad : bstr, |
| // payload : bstr |
| // ] |
| // |
| eicCborAppendArray(&cbor, 4); |
| eicCborAppendStringZ(&cbor, "Signature1"); |
| |
| // The COSE Encoded protected headers is just a single field with |
| // COSE_LABEL_ALG (1) -> COSE_ALG_ECSDA_256 (-7). For simplicitly we just |
| // hard-code the CBOR encoding: |
| static const uint8_t coseEncodedProtectedHeaders[] = {0xa1, 0x01, 0x26}; |
| eicCborAppendByteString(&cbor, coseEncodedProtectedHeaders, |
| sizeof(coseEncodedProtectedHeaders)); |
| |
| // We currently don't support Externally Supplied Data (RFC 8152 section 4.3) |
| // so external_aad is the empty bstr |
| static const uint8_t externalAad[0] = {}; |
| eicCborAppendByteString(&cbor, externalAad, sizeof(externalAad)); |
| |
| // For the payload, the _encoded_ form follows here. We handle this by simply |
| // opening a bstr, and then writing the CBOR. This requires us to know the |
| // size of said bstr, ahead of time. |
| eicCborBegin(&cbor, EIC_CBOR_MAJOR_TYPE_BYTE_STRING, proofOfOwnershipCborSize); |
| |
| // Finally, the CBOR that we're actually signing. |
| eicCborAppendArray(&cbor, 4); |
| eicCborAppendStringZ(&cbor, "ProofOfOwnership"); |
| eicCborAppendString(&cbor, docType, docTypeLength); |
| eicCborAppendByteString(&cbor, challenge, challengeSize); |
| eicCborAppendBool(&cbor, testCredential); |
| |
| uint8_t cborSha256[EIC_SHA256_DIGEST_SIZE]; |
| eicCborFinal(&cbor, cborSha256); |
| if (!eicOpsEcDsa(ctx->credentialPrivateKey, cborSha256, signatureOfToBeSigned)) { |
| eicDebug("Error signing proofOfDeletion"); |
| return false; |
| } |
| |
| return true; |
| } |