diff --git a/identity/aidl/Android.bp b/identity/aidl/Android.bp index 20904731a1..6a25e628dc 100644 --- a/identity/aidl/Android.bp +++ b/identity/aidl/Android.bp @@ -18,7 +18,7 @@ aidl_interface { "android.hardware.security.rkp-V3", ], stability: "vintf", - frozen: true, + frozen: false, backend: { java: { platform_apis: true, @@ -67,20 +67,20 @@ aidl_interface { cc_defaults { name: "identity_use_latest_hal_aidl_ndk_static", static_libs: [ - "android.hardware.identity-V4-ndk", + "android.hardware.identity-V5-ndk", ], } cc_defaults { name: "identity_use_latest_hal_aidl_ndk_shared", shared_libs: [ - "android.hardware.identity-V4-ndk", + "android.hardware.identity-V5-ndk", ], } cc_defaults { name: "identity_use_latest_hal_aidl_cpp_static", static_libs: [ - "android.hardware.identity-V4-cpp", + "android.hardware.identity-V5-cpp", ], } diff --git a/identity/aidl/aidl_api/android.hardware.identity/current/android/hardware/identity/IIdentityCredential.aidl b/identity/aidl/aidl_api/android.hardware.identity/current/android/hardware/identity/IIdentityCredential.aidl index 5065641109..4f2fe0bbe2 100644 --- a/identity/aidl/aidl_api/android.hardware.identity/current/android/hardware/identity/IIdentityCredential.aidl +++ b/identity/aidl/aidl_api/android.hardware.identity/current/android/hardware/identity/IIdentityCredential.aidl @@ -51,4 +51,5 @@ interface IIdentityCredential { byte[] deleteCredentialWithChallenge(in byte[] challenge); byte[] proveOwnership(in byte[] challenge); android.hardware.identity.IWritableIdentityCredential updateCredential(); + @SuppressWarnings(value={"out-array"}) void finishRetrievalWithSignature(out byte[] mac, out byte[] deviceNameSpaces, out byte[] ecdsaSignature); } diff --git a/identity/aidl/android/hardware/identity/IIdentityCredential.aidl b/identity/aidl/android/hardware/identity/IIdentityCredential.aidl index 82b0a83fc7..abdb00bb5c 100644 --- a/identity/aidl/android/hardware/identity/IIdentityCredential.aidl +++ b/identity/aidl/android/hardware/identity/IIdentityCredential.aidl @@ -194,7 +194,8 @@ interface IIdentityCredential { * is permissible for this to be empty in which case the readerSignature parameter * must also be empty. If this is not the case, the call fails with STATUS_FAILED. * - * If the SessionTranscript CBOR is not empty, the X and Y coordinates of the public + * If mdoc session encryption is used (e.g. createEphemeralKeyPair() has been called) + * and 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 the CBOR. Each of these coordinates must appear encoded * with the most significant bits first and use the exact amount of bits indicated by @@ -239,8 +240,8 @@ interface IIdentityCredential { * and remove the corresponding requests from the counts. */ void startRetrieval(in SecureAccessControlProfile[] accessControlProfiles, - in HardwareAuthToken authToken, in byte[] itemsRequest, in byte[] signingKeyBlob, - in byte[] sessionTranscript, in byte[] readerSignature, in int[] requestCounts); + in HardwareAuthToken authToken, in byte[] itemsRequest, in byte[] signingKeyBlob, + in byte[] sessionTranscript, in byte[] readerSignature, in int[] requestCounts); /** * Starts retrieving an entry, subject to access control requirements. Entries must be @@ -271,8 +272,8 @@ interface IIdentityCredential { * is given and this profile wasn't passed to startRetrieval() this call fails * with STATUS_INVALID_DATA. */ - void startRetrieveEntryValue(in @utf8InCpp String nameSpace, in @utf8InCpp String name, - in int entrySize, in int[] accessControlProfileIds); + void startRetrieveEntryValue(in @utf8InCpp String nameSpace, + in @utf8InCpp String name, in int entrySize, in int[] accessControlProfileIds); /** * Retrieves an entry value, or part of one, if the entry value is larger than gcmChunkSize. @@ -293,11 +294,13 @@ interface IIdentityCredential { * returned data. * * If signingKeyBlob or the sessionTranscript parameter passed to startRetrieval() is - * empty then the returned MAC will be empty. + * empty or if mdoc session encryption is not being used (e.g. if createEphemeralKeyPair() + * was not called) then the returned MAC will be empty. * - * @param out mac is empty if signingKeyBlob or the sessionTranscript passed to - * startRetrieval() is empty. Otherwise it is a COSE_Mac0 with empty payload - * and the detached content is set to DeviceAuthenticationBytes as defined below. + * @param out mac is empty if signingKeyBlob, or the sessionTranscript passed to + * startRetrieval() is empty, or if mdoc session encryption is not being used (e.g. if + * createEphemeralKeyPair() was not called). Otherwise it is a COSE_Mac0 with empty + * payload and the detached content is set to DeviceAuthenticationBytes as defined below. * This code is produced by using the key agreement and key derivation function * from the ciphersuite with the authentication private key and the reader * ephemeral public key to compute a shared message authentication code (MAC) @@ -407,13 +410,13 @@ interface IIdentityCredential { */ void setRequestedNamespaces(in RequestNamespace[] requestNamespaces); - /** - * Sets the VerificationToken. This method must be called before startRetrieval() is - * called. This token uses the same challenge as returned by createAuthChallenge(). - * - * @param verificationToken - * The verification token. This token is only valid if the timestamp field is non-zero. - */ + /** + * Sets the VerificationToken. This method must be called before startRetrieval() is + * called. This token uses the same challenge as returned by createAuthChallenge(). + * + * @param verificationToken + * The verification token. This token is only valid if the timestamp field is non-zero. + */ void setVerificationToken(in VerificationToken verificationToken); /** @@ -485,4 +488,20 @@ interface IIdentityCredential { * @return an IWritableIdentityCredential */ IWritableIdentityCredential updateCredential(); + + /** + * Like finishRetrieval() but also returns an ECDSA signature in addition to the MAC. + * + * See section 9.1.3.6 of ISO/IEC 18013-5:2021 for details of how the signature is calculated. + * + * Unlike MACing, an ECDSA signature will be returned even if mdoc session encryption isn't + * being used. + * + * This method was introduced in API version 5. + * + * @param ecdsaSignature a COSE_Sign1 signature described above. + */ + @SuppressWarnings(value={"out-array"}) + void finishRetrievalWithSignature( + out byte[] mac, out byte[] deviceNameSpaces, out byte[] ecdsaSignature); } diff --git a/identity/aidl/android/hardware/identity/IPresentationSession.aidl b/identity/aidl/android/hardware/identity/IPresentationSession.aidl index b0449f0bba..0a06740e31 100644 --- a/identity/aidl/android/hardware/identity/IPresentationSession.aidl +++ b/identity/aidl/android/hardware/identity/IPresentationSession.aidl @@ -70,6 +70,17 @@ interface IPresentationSession { * * This can be empty but if it's non-empty it must be valid CBOR. * + * If mdoc session encryption is used (e.g. getEphemeralKeyPair() has been called) + * and 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 the CBOR. Each of these coordinates must appear encoded + * with the most significant bits first and use the exact amount of bits indicated by + * the key size of the ephemeral keys. For example, if the ephemeral key is using the + * P-256 curve then the 32 bytes for the X coordinate encoded with the most significant + * bits first must appear somewhere in the CBOR and ditto for the 32 bytes for the Y + * coordinate. If this is not satisfied, the call fails with + * STATUS_EPHEMERAL_PUBLIC_KEY_NOT_FOUND. + * * This method may only be called once per instance. If called more than once, STATUS_FAILED * must be returned. * diff --git a/identity/aidl/default/FakeSecureHardwareProxy.cpp b/identity/aidl/default/FakeSecureHardwareProxy.cpp index 9b9a749427..8551ab7b6d 100644 --- a/identity/aidl/default/FakeSecureHardwareProxy.cpp +++ b/identity/aidl/default/FakeSecureHardwareProxy.cpp @@ -596,10 +596,10 @@ bool FakeSecureHardwarePresentationProxy::startRetrieveEntries() { return eicPresentationStartRetrieveEntries(&ctx_); } -bool FakeSecureHardwarePresentationProxy::calcMacKey( +bool FakeSecureHardwarePresentationProxy::prepareDeviceAuthentication( const vector& sessionTranscript, const vector& readerEphemeralPublicKey, const vector& signingKeyBlob, const string& docType, - unsigned int numNamespacesWithValues, size_t expectedProofOfProvisioningSize) { + unsigned int numNamespacesWithValues, size_t expectedDeviceNamespacesSize) { if (!validateId(__func__)) { return false; } @@ -608,10 +608,10 @@ bool FakeSecureHardwarePresentationProxy::calcMacKey( eicDebug("Unexpected size %zd of signingKeyBlob, expected 60", signingKeyBlob.size()); return false; } - return eicPresentationCalcMacKey(&ctx_, sessionTranscript.data(), sessionTranscript.size(), - readerEphemeralPublicKey.data(), signingKeyBlob.data(), - docType.c_str(), docType.size(), numNamespacesWithValues, - expectedProofOfProvisioningSize); + return eicPresentationPrepareDeviceAuthentication( + &ctx_, sessionTranscript.data(), sessionTranscript.size(), + readerEphemeralPublicKey.data(), readerEphemeralPublicKey.size(), signingKeyBlob.data(), + docType.c_str(), docType.size(), numNamespacesWithValues, expectedDeviceNamespacesSize); } AccessCheckResult FakeSecureHardwarePresentationProxy::startRetrieveEntryValue( @@ -673,6 +673,25 @@ optional> FakeSecureHardwarePresentationProxy::retrieveEntryValu return content; } +optional, vector>> +FakeSecureHardwarePresentationProxy::finishRetrievalWithSignature() { + if (!validateId(__func__)) { + return std::nullopt; + } + + vector mac(32); + size_t macSize = 32; + vector ecdsaSignature(EIC_ECDSA_P256_SIGNATURE_SIZE); + size_t ecdsaSignatureSize = EIC_ECDSA_P256_SIGNATURE_SIZE; + if (!eicPresentationFinishRetrievalWithSignature(&ctx_, mac.data(), &macSize, + ecdsaSignature.data(), &ecdsaSignatureSize)) { + return std::nullopt; + } + mac.resize(macSize); + ecdsaSignature.resize(ecdsaSignatureSize); + return std::make_pair(mac, ecdsaSignature); +} + optional> FakeSecureHardwarePresentationProxy::finishRetrieval() { if (!validateId(__func__)) { return std::nullopt; diff --git a/identity/aidl/default/FakeSecureHardwareProxy.h b/identity/aidl/default/FakeSecureHardwareProxy.h index 2512074b5f..b56ab9344c 100644 --- a/identity/aidl/default/FakeSecureHardwareProxy.h +++ b/identity/aidl/default/FakeSecureHardwareProxy.h @@ -175,11 +175,11 @@ class FakeSecureHardwarePresentationProxy : public SecureHardwarePresentationPro const vector& requestMessage, int coseSignAlg, const vector& readerSignatureOfToBeSigned) override; - bool calcMacKey(const vector& sessionTranscript, - const vector& readerEphemeralPublicKey, - const vector& signingKeyBlob, const string& docType, - unsigned int numNamespacesWithValues, - size_t expectedProofOfProvisioningSize) override; + bool prepareDeviceAuthentication(const vector& sessionTranscript, + const vector& readerEphemeralPublicKey, + const vector& signingKeyBlob, const string& docType, + unsigned int numNamespacesWithValues, + size_t expectedDeviceNamespacesSize) override; AccessCheckResult startRetrieveEntryValue( const string& nameSpace, const string& name, unsigned int newNamespaceNumEntries, @@ -191,6 +191,8 @@ class FakeSecureHardwarePresentationProxy : public SecureHardwarePresentationPro optional> finishRetrieval() override; + optional, vector>> finishRetrievalWithSignature() override; + optional> deleteCredential(const string& docType, const vector& challenge, bool includeChallenge, diff --git a/identity/aidl/default/common/IdentityCredential.cpp b/identity/aidl/default/common/IdentityCredential.cpp index ff80752ee7..4c3b7b2e51 100644 --- a/identity/aidl/default/common/IdentityCredential.cpp +++ b/identity/aidl/default/common/IdentityCredential.cpp @@ -457,17 +457,16 @@ ndk::ScopedAStatus IdentityCredential::startRetrieval( } if (session_) { - // If presenting in a session, the TA has already done this check. - + // If presenting in a session, the TA has already done the check for (X, Y) as done + // below, see eicSessionSetSessionTranscript(). } else { - // 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. + // If mdoc session encryption is in use, 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. - // - // Would be nice to move this check to the TA. - if (sessionTranscript.size() > 0) { + if (sessionTranscript.size() > 0 && ephemeralPublicKey_.size() > 0) { auto [getXYSuccess, ePubX, ePubY] = support::ecPublicKeyGetXandY(ephemeralPublicKey_); if (!getXYSuccess) { return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage( @@ -608,33 +607,36 @@ ndk::ScopedAStatus IdentityCredential::startRetrieval( // Finally, pass info so the HMAC key can be derived and the TA can start // creating the DeviceNameSpaces CBOR... if (!session_) { - if (sessionTranscript_.size() > 0 && readerPublicKey_.size() > 0 && - signingKeyBlob.size() > 0) { - // We expect the reader ephemeral public key to be same size and curve - // as the ephemeral key we generated (e.g. P-256 key), otherwise ECDH - // won't work. So its length should be 65 bytes and it should be - // starting with 0x04. - if (readerPublicKey_.size() != 65 || readerPublicKey_[0] != 0x04) { - return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage( - IIdentityCredentialStore::STATUS_FAILED, - "Reader public key is not in expected format")); + if (sessionTranscript_.size() > 0 && signingKeyBlob.size() > 0) { + vector eReaderKeyP256; + if (readerPublicKey_.size() > 0) { + // If set, we expect the reader ephemeral public key to be same size and curve + // as the ephemeral key we generated (e.g. P-256 key), otherwise ECDH won't + // work. So its length should be 65 bytes and it should be starting with 0x04. + if (readerPublicKey_.size() != 65 || readerPublicKey_[0] != 0x04) { + return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage( + IIdentityCredentialStore::STATUS_FAILED, + "Reader public key is not in expected format")); + } + eReaderKeyP256 = + vector(readerPublicKey_.begin() + 1, readerPublicKey_.end()); } - vector pubKeyP256(readerPublicKey_.begin() + 1, readerPublicKey_.end()); - if (!hwProxy_->calcMacKey(sessionTranscript_, pubKeyP256, signingKeyBlob, docType_, - numNamespacesWithValues, expectedDeviceNameSpacesSize_)) { + if (!hwProxy_->prepareDeviceAuthentication( + sessionTranscript_, eReaderKeyP256, signingKeyBlob, docType_, + numNamespacesWithValues, expectedDeviceNameSpacesSize_)) { return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage( IIdentityCredentialStore::STATUS_FAILED, "Error starting retrieving entries")); } } } else { - if (session_->getSessionTranscript().size() > 0 && - session_->getReaderEphemeralPublicKey().size() > 0 && signingKeyBlob.size() > 0) { + if (session_->getSessionTranscript().size() > 0 && signingKeyBlob.size() > 0) { // Don't actually pass the reader ephemeral public key in, the TA will get // it from the session object. // - if (!hwProxy_->calcMacKey(sessionTranscript_, {}, signingKeyBlob, docType_, - numNamespacesWithValues, expectedDeviceNameSpacesSize_)) { + if (!hwProxy_->prepareDeviceAuthentication(sessionTranscript_, {}, signingKeyBlob, + docType_, numNamespacesWithValues, + expectedDeviceNameSpacesSize_)) { return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage( IIdentityCredentialStore::STATUS_FAILED, "Error starting retrieving entries")); @@ -924,8 +926,9 @@ ndk::ScopedAStatus IdentityCredential::retrieveEntryValue(const vector& return ndk::ScopedAStatus::ok(); } -ndk::ScopedAStatus IdentityCredential::finishRetrieval(vector* outMac, - vector* outDeviceNameSpaces) { +ndk::ScopedAStatus IdentityCredential::finishRetrievalWithSignature( + vector* outMac, vector* outDeviceNameSpaces, + vector* outEcdsaSignature) { ndk::ScopedAStatus status = ensureHwProxy(); if (!status.isOk()) { return status; @@ -948,17 +951,34 @@ ndk::ScopedAStatus IdentityCredential::finishRetrieval(vector* outMac, .c_str())); } + optional> digestToBeMaced; + optional> signatureToBeSigned; + + // This relies on the fact that binder calls never pass a nullptr + // for out parameters. Hence if it's null here we know this was + // called from finishRetrieval() below. + if (outEcdsaSignature == nullptr) { + digestToBeMaced = hwProxy_->finishRetrieval(); + if (!digestToBeMaced) { + return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage( + IIdentityCredentialStore::STATUS_INVALID_DATA, + "Error generating digestToBeMaced")); + } + } else { + auto macAndSignature = hwProxy_->finishRetrievalWithSignature(); + if (!macAndSignature) { + return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage( + IIdentityCredentialStore::STATUS_INVALID_DATA, + "Error generating digestToBeMaced and signatureToBeSigned")); + } + digestToBeMaced = macAndSignature->first; + signatureToBeSigned = macAndSignature->second; + } + // If the TA calculated a MAC (it might not have), format it as a COSE_Mac0 // - optional> mac; - optional> digestToBeMaced = hwProxy_->finishRetrieval(); - - // The MAC not being set means an error occurred. - if (!digestToBeMaced) { - return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage( - IIdentityCredentialStore::STATUS_INVALID_DATA, "Error generating digestToBeMaced")); - } // Size 0 means that the MAC isn't set. If it's set, it has to be 32 bytes. + optional> mac; if (digestToBeMaced.value().size() != 0) { if (digestToBeMaced.value().size() != 32) { return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage( @@ -967,12 +987,27 @@ ndk::ScopedAStatus IdentityCredential::finishRetrieval(vector* outMac, } mac = support::coseMacWithDigest(digestToBeMaced.value(), {} /* data */); } - *outMac = mac.value_or(vector({})); + + optional> signature; + if (signatureToBeSigned && signatureToBeSigned.value().size() != 0) { + signature = support::coseSignEcDsaWithSignature(signatureToBeSigned.value(), {}, // data + {}); // certificateChain + } + if (outEcdsaSignature != nullptr) { + *outEcdsaSignature = signature.value_or(vector({})); + } + *outDeviceNameSpaces = encodedDeviceNameSpaces; + return ndk::ScopedAStatus::ok(); } +ndk::ScopedAStatus IdentityCredential::finishRetrieval(vector* outMac, + vector* outDeviceNameSpaces) { + return finishRetrievalWithSignature(outMac, outDeviceNameSpaces, nullptr); +} + ndk::ScopedAStatus IdentityCredential::generateSigningKeyPair( vector* outSigningKeyBlob, Certificate* outSigningKeyCertificate) { if (session_) { diff --git a/identity/aidl/default/common/IdentityCredential.h b/identity/aidl/default/common/IdentityCredential.h index 592982991d..1e0cd64094 100644 --- a/identity/aidl/default/common/IdentityCredential.h +++ b/identity/aidl/default/common/IdentityCredential.h @@ -92,6 +92,10 @@ class IdentityCredential : public BnIdentityCredential { ndk::ScopedAStatus updateCredential( shared_ptr* outWritableCredential) override; + ndk::ScopedAStatus finishRetrievalWithSignature(vector* outMac, + vector* outDeviceNameSpaces, + vector* outEcdsaSignature) override; + private: ndk::ScopedAStatus deleteCredentialCommon(const vector& challenge, bool includeChallenge, diff --git a/identity/aidl/default/common/PresentationSession.cpp b/identity/aidl/default/common/PresentationSession.cpp index 2eb7f2ea16..cf5b06698e 100644 --- a/identity/aidl/default/common/PresentationSession.cpp +++ b/identity/aidl/default/common/PresentationSession.cpp @@ -54,19 +54,6 @@ int PresentationSession::initialize() { } id_ = id.value(); - optional> ephemeralKeyPriv = hwProxy_->getEphemeralKeyPair(); - if (!ephemeralKeyPriv) { - LOG(ERROR) << "Error getting ephemeral private key for session"; - return IIdentityCredentialStore::STATUS_FAILED; - } - optional> ephemeralKeyPair = - support::ecPrivateKeyToKeyPair(ephemeralKeyPriv.value()); - if (!ephemeralKeyPair) { - LOG(ERROR) << "Error creating ephemeral key-pair"; - return IIdentityCredentialStore::STATUS_FAILED; - } - ephemeralKeyPair_ = ephemeralKeyPair.value(); - optional authChallenge = hwProxy_->getAuthChallenge(); if (!authChallenge) { LOG(ERROR) << "Error getting authChallenge for session"; @@ -78,6 +65,23 @@ int PresentationSession::initialize() { } ndk::ScopedAStatus PresentationSession::getEphemeralKeyPair(vector* outKeyPair) { + if (ephemeralKeyPair_.size() == 0) { + optional> ephemeralKeyPriv = hwProxy_->getEphemeralKeyPair(); + if (!ephemeralKeyPriv) { + LOG(ERROR) << "Error getting ephemeral private key for session"; + return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage( + IIdentityCredentialStore::STATUS_FAILED, + "Error getting ephemeral private key for session")); + } + optional> ephemeralKeyPair = + support::ecPrivateKeyToKeyPair(ephemeralKeyPriv.value()); + if (!ephemeralKeyPair) { + LOG(ERROR) << "Error creating ephemeral key-pair"; + return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage( + IIdentityCredentialStore::STATUS_FAILED, "Error creating ephemeral key-pair")); + } + ephemeralKeyPair_ = ephemeralKeyPair.value(); + } *outKeyPair = ephemeralKeyPair_; return ndk::ScopedAStatus::ok(); } diff --git a/identity/aidl/default/common/PresentationSession.h b/identity/aidl/default/common/PresentationSession.h index 4cb174a82c..b3d46f9837 100644 --- a/identity/aidl/default/common/PresentationSession.h +++ b/identity/aidl/default/common/PresentationSession.h @@ -72,9 +72,11 @@ class PresentationSession : public BnPresentationSession { // Set by initialize() uint64_t id_; - vector ephemeralKeyPair_; uint64_t authChallenge_; + // Set by getEphemeralKeyPair() + vector ephemeralKeyPair_; + // Set by setReaderEphemeralPublicKey() vector readerPublicKey_; diff --git a/identity/aidl/default/common/SecureHardwareProxy.h b/identity/aidl/default/common/SecureHardwareProxy.h index 9f63ad809b..6463318314 100644 --- a/identity/aidl/default/common/SecureHardwareProxy.h +++ b/identity/aidl/default/common/SecureHardwareProxy.h @@ -194,11 +194,12 @@ class SecureHardwarePresentationProxy : public RefBase { const vector& requestMessage, int coseSignAlg, const vector& readerSignatureOfToBeSigned) = 0; - virtual bool calcMacKey(const vector& sessionTranscript, - const vector& readerEphemeralPublicKey, - const vector& signingKeyBlob, const string& docType, - unsigned int numNamespacesWithValues, - size_t expectedProofOfProvisioningSize) = 0; + virtual bool prepareDeviceAuthentication(const vector& sessionTranscript, + const vector& readerEphemeralPublicKey, + const vector& signingKeyBlob, + const string& docType, + unsigned int numNamespacesWithValues, + size_t expectedDeviceNamespacesSize) = 0; virtual AccessCheckResult startRetrieveEntryValue( const string& nameSpace, const string& name, unsigned int newNamespaceNumEntries, @@ -209,6 +210,7 @@ class SecureHardwarePresentationProxy : public RefBase { const vector& accessControlProfileIds) = 0; virtual optional> finishRetrieval(); + virtual optional, vector>> finishRetrievalWithSignature(); virtual optional> deleteCredential(const string& docType, const vector& challenge, diff --git a/identity/aidl/default/identity-default.xml b/identity/aidl/default/identity-default.xml index cc0ddc7d51..d0d43afa1a 100644 --- a/identity/aidl/default/identity-default.xml +++ b/identity/aidl/default/identity-default.xml @@ -1,7 +1,7 @@ android.hardware.identity - 4 + 5 IIdentityCredentialStore default diff --git a/identity/aidl/default/libeic/EicPresentation.c b/identity/aidl/default/libeic/EicPresentation.c index 104a559697..23fd0b307b 100644 --- a/identity/aidl/default/libeic/EicPresentation.c +++ b/identity/aidl/default/libeic/EicPresentation.c @@ -557,87 +557,11 @@ bool eicPresentationValidateAccessControlProfile(EicPresentation* ctx, int id, return true; } -bool eicPresentationCalcMacKey(EicPresentation* ctx, const uint8_t* sessionTranscript, - size_t sessionTranscriptSize, - const uint8_t readerEphemeralPublicKey[EIC_P256_PUB_KEY_SIZE], - const uint8_t signingKeyBlob[60], const char* docType, - size_t docTypeLength, unsigned int numNamespacesWithValues, - size_t expectedDeviceNamespacesSize) { - 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; - } - readerEphemeralPublicKey = session->readerEphemeralPublicKey; - } - - uint8_t signingKeyPriv[EIC_P256_PRIV_KEY_SIZE]; - if (!eicOpsDecryptAes128Gcm(ctx->storageKey, signingKeyBlob, 60, (const uint8_t*)docType, - docTypeLength, signingKeyPriv)) { - eicDebug("Error decrypting signingKeyBlob"); - return false; - } - - uint8_t sharedSecret[EIC_P256_COORDINATE_SIZE]; - if (!eicOpsEcdh(readerEphemeralPublicKey, signingKeyPriv, 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)); - ctx->buildCbor = true; - - // 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)); - +// 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 @@ -674,26 +598,148 @@ bool eicPresentationCalcMacKey(EicPresentation* ctx, const uint8_t* sessionTrans dabCalculatedSize += calculatedSize; // Begin the bytestring for DeviceAuthenticationBytes; - eicCborBegin(&ctx->cbor, EIC_CBOR_MAJOR_TYPE_BYTE_STRING, dabCalculatedSize); + eicCborBegin(cbor, EIC_CBOR_MAJOR_TYPE_BYTE_STRING, dabCalculatedSize); - eicCborAppendSemantic(&ctx->cbor, EIC_CBOR_SEMANTIC_TAG_ENCODED_CBOR); + eicCborAppendSemantic(cbor, EIC_CBOR_SEMANTIC_TAG_ENCODED_CBOR); // Begins the bytestring for DeviceAuthentication; - eicCborBegin(&ctx->cbor, EIC_CBOR_MAJOR_TYPE_BYTE_STRING, calculatedSize); + eicCborBegin(cbor, EIC_CBOR_MAJOR_TYPE_BYTE_STRING, calculatedSize); - eicCborAppendArray(&ctx->cbor, 4); - eicCborAppendStringZ(&ctx->cbor, "DeviceAuthentication"); - eicCborAppend(&ctx->cbor, sessionTranscript, sessionTranscriptSize); - eicCborAppendString(&ctx->cbor, docType, docTypeLength); + 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(&ctx->cbor, EIC_CBOR_SEMANTIC_TAG_ENCODED_CBOR); - eicCborBegin(&ctx->cbor, EIC_CBOR_MAJOR_TYPE_BYTE_STRING, expectedDeviceNamespacesSize); - ctx->expectedCborSizeAtEnd = expectedDeviceNamespacesSize + ctx->cbor.size; + 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; - eicCborAppendMap(&ctx->cbor, numNamespacesWithValues); return true; } @@ -702,6 +748,7 @@ bool eicPresentationStartRetrieveEntries(EicPresentation* ctx) { // state objects here. ctx->requestMessageValidated = false; ctx->buildCbor = false; + ctx->buildCborEcdsa = false; ctx->accessControlProfileMaskValidated = 0; ctx->accessControlProfileMaskUsesReaderAuth = 0; ctx->accessControlProfileMaskFailedReaderAuth = 0; @@ -724,6 +771,9 @@ EicAccessCheckResult eicPresentationStartRetrieveEntryValue( 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 @@ -778,6 +828,7 @@ EicAccessCheckResult eicPresentationStartRetrieveEntryValue( if (result == EIC_ACCESS_CHECK_RESULT_OK) { eicCborAppendString(&ctx->cbor, name, nameLength); + eicCborAppendString(&ctx->cborEcdsa, name, nameLength); ctx->accessCheckOk = true; } return result; @@ -821,6 +872,7 @@ bool eicPresentationRetrieveEntryValue(EicPresentation* ctx, const uint8_t* encr } eicCborAppend(&ctx->cbor, content, encryptedContentSize - 28); + eicCborAppend(&ctx->cborEcdsa, content, encryptedContentSize - 28); return true; } @@ -842,6 +894,40 @@ bool eicPresentationFinishRetrieval(EicPresentation* ctx, uint8_t* digestToBeMac 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; } diff --git a/identity/aidl/default/libeic/EicPresentation.h b/identity/aidl/default/libeic/EicPresentation.h index a031890e58..cd3162af76 100644 --- a/identity/aidl/default/libeic/EicPresentation.h +++ b/identity/aidl/default/libeic/EicPresentation.h @@ -76,6 +76,7 @@ typedef struct { // aren't. bool requestMessageValidated; bool buildCbor; + bool buildCborEcdsa; // Set to true initialized as a test credential. bool testCredential; @@ -101,6 +102,12 @@ typedef struct { size_t expectedCborSizeAtEnd; EicCbor cbor; + + // The selected DeviceKey / AuthKey + uint8_t deviceKeyPriv[EIC_P256_PRIV_KEY_SIZE]; + + EicCbor cborEcdsa; + size_t expectedCborEcdsaSizeAtEnd; } EicPresentation; // If sessionId is zero (EIC_PRESENTATION_ID_UNSET), the presentation object is not associated @@ -214,14 +221,13 @@ typedef enum { EIC_ACCESS_CHECK_RESULT_READER_AUTHENTICATION_FAILED, } EicAccessCheckResult; -// Passes enough information to calculate the MACing key +// Passes enough information to calculate the MACing key and/or prepare ECDSA signing // -bool eicPresentationCalcMacKey(EicPresentation* ctx, const uint8_t* sessionTranscript, - size_t sessionTranscriptSize, - const uint8_t readerEphemeralPublicKey[EIC_P256_PUB_KEY_SIZE], - const uint8_t signingKeyBlob[60], const char* docType, - size_t docTypeLength, unsigned int numNamespacesWithValues, - size_t expectedDeviceNamespacesSize); +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); // The scratchSpace should be set to a buffer at least 512 bytes (ideally 1024 // bytes, the bigger the better). It's done this way to avoid allocating stack @@ -253,6 +259,13 @@ bool eicPresentationRetrieveEntryValue(EicPresentation* ctx, const uint8_t* encr bool eicPresentationFinishRetrieval(EicPresentation* ctx, uint8_t* digestToBeMaced, size_t* digestToBeMacedSize); +// Like eicPresentationFinishRetrieval() but also returns an ECDSA signature. +// +bool eicPresentationFinishRetrievalWithSignature(EicPresentation* ctx, uint8_t* digestToBeMaced, + size_t* digestToBeMacedSize, + uint8_t* signatureOfToBeSigned, + size_t* signatureOfToBeSignedSize); + // The data returned in |signatureOfToBeSigned| contains the ECDSA signature of // the ToBeSigned CBOR from RFC 8051 "4.4. Signing and Verification Process" // where content is set to the ProofOfDeletion CBOR. diff --git a/identity/aidl/default/libeic/EicSession.c b/identity/aidl/default/libeic/EicSession.c index d0c7a0d77e..e44fa68a1e 100644 --- a/identity/aidl/default/libeic/EicSession.c +++ b/identity/aidl/default/libeic/EicSession.c @@ -84,30 +84,35 @@ bool eicSessionGetAuthChallenge(EicSession* ctx, uint64_t* outAuthChallenge) { bool eicSessionGetEphemeralKeyPair(EicSession* ctx, uint8_t ephemeralPrivateKey[EIC_P256_PRIV_KEY_SIZE]) { eicMemCpy(ephemeralPrivateKey, ctx->ephemeralPrivateKey, EIC_P256_PRIV_KEY_SIZE); + ctx->getEphemeralKeyPairCalled = true; return true; } bool eicSessionSetReaderEphemeralPublicKey( EicSession* ctx, const uint8_t readerEphemeralPublicKey[EIC_P256_PUB_KEY_SIZE]) { eicMemCpy(ctx->readerEphemeralPublicKey, readerEphemeralPublicKey, EIC_P256_PUB_KEY_SIZE); + ctx->readerEphemeralPublicKeySize = EIC_P256_PUB_KEY_SIZE; return true; } bool eicSessionSetSessionTranscript(EicSession* ctx, const uint8_t* sessionTranscript, size_t sessionTranscriptSize) { - // Only accept the SessionTranscript if X and Y from the ephemeral key - // we created is somewhere in SessionTranscript... + // If mdoc session encryption is in use, only accept the + // SessionTranscript if X and Y from the ephemeral key we created + // is somewhere in SessionTranscript... // - if (eicMemMem(sessionTranscript, sessionTranscriptSize, ctx->ephemeralPublicKey, - EIC_P256_PUB_KEY_SIZE / 2) == NULL) { - eicDebug("Error finding X from ephemeralPublicKey in sessionTranscript"); - return false; - } - if (eicMemMem(sessionTranscript, sessionTranscriptSize, - ctx->ephemeralPublicKey + EIC_P256_PUB_KEY_SIZE / 2, - EIC_P256_PUB_KEY_SIZE / 2) == NULL) { - eicDebug("Error finding Y from ephemeralPublicKey in sessionTranscript"); - return false; + if (ctx->getEphemeralKeyPairCalled) { + if (eicMemMem(sessionTranscript, sessionTranscriptSize, ctx->ephemeralPublicKey, + EIC_P256_PUB_KEY_SIZE / 2) == NULL) { + eicDebug("Error finding X from ephemeralPublicKey in sessionTranscript"); + return false; + } + if (eicMemMem(sessionTranscript, sessionTranscriptSize, + ctx->ephemeralPublicKey + EIC_P256_PUB_KEY_SIZE / 2, + EIC_P256_PUB_KEY_SIZE / 2) == NULL) { + eicDebug("Error finding Y from ephemeralPublicKey in sessionTranscript"); + return false; + } } // To save space we only store the SHA-256 of SessionTranscript diff --git a/identity/aidl/default/libeic/EicSession.h b/identity/aidl/default/libeic/EicSession.h index 0303dae1c3..ae9babf21a 100644 --- a/identity/aidl/default/libeic/EicSession.h +++ b/identity/aidl/default/libeic/EicSession.h @@ -31,6 +31,9 @@ typedef struct { // A non-zero number unique for this EicSession instance uint32_t id; + // Set to true iff eicSessionGetEphemeralKeyPair() has been called. + bool getEphemeralKeyPairCalled; + // The challenge generated at construction time by eicSessionInit(). uint64_t authChallenge; @@ -41,6 +44,7 @@ typedef struct { uint8_t sessionTranscriptSha256[EIC_SHA256_DIGEST_SIZE]; + size_t readerEphemeralPublicKeySize; } EicSession; bool eicSessionInit(EicSession* ctx); diff --git a/identity/aidl/vts/EndToEndTests.cpp b/identity/aidl/vts/EndToEndTests.cpp index 67db915d7c..ae9035b35b 100644 --- a/identity/aidl/vts/EndToEndTests.cpp +++ b/identity/aidl/vts/EndToEndTests.cpp @@ -441,8 +441,18 @@ TEST_P(EndToEndTests, createAndRetrieveCredential) { } vector mac; + vector ecdsaSignature; vector deviceNameSpacesEncoded; - ASSERT_TRUE(credential->finishRetrieval(&mac, &deviceNameSpacesEncoded).isOk()); + // API version 5 (feature version 202301) returns both MAC and ECDSA signature. + if (halApiVersion_ >= 5) { + ASSERT_TRUE(credential + ->finishRetrievalWithSignature(&mac, &deviceNameSpacesEncoded, + &ecdsaSignature) + .isOk()); + ASSERT_GT(ecdsaSignature.size(), 0); + } else { + ASSERT_TRUE(credential->finishRetrieval(&mac, &deviceNameSpacesEncoded).isOk()); + } cborPretty = cppbor::prettyPrint(deviceNameSpacesEncoded, 32, {}); ASSERT_EQ( "{\n" @@ -475,6 +485,21 @@ TEST_P(EndToEndTests, createAndRetrieveCredential) { ASSERT_TRUE(calculatedMac); EXPECT_EQ(mac, calculatedMac); + if (ecdsaSignature.size() > 0) { + vector encodedDeviceAuthentication = + cppbor::Array() + .add("DeviceAuthentication") + .add(sessionTranscript.clone()) + .add(docType) + .add(cppbor::SemanticTag(24, deviceNameSpacesEncoded)) + .encode(); + vector deviceAuthenticationBytes = + cppbor::SemanticTag(24, encodedDeviceAuthentication).encode(); + EXPECT_TRUE(support::coseCheckEcDsaSignature(ecdsaSignature, + deviceAuthenticationBytes, // Detached content + signingPubKey.value())); + } + // Also perform an additional empty request. This is what mDL applications // are envisioned to do - one call to get the data elements, another to get // an empty DeviceSignedItems and corresponding MAC. @@ -486,7 +511,16 @@ TEST_P(EndToEndTests, createAndRetrieveCredential) { signingKeyBlob, sessionTranscriptEncoded, {}, // readerSignature, testEntriesEntryCounts) .isOk()); - ASSERT_TRUE(credential->finishRetrieval(&mac, &deviceNameSpacesEncoded).isOk()); + // API version 5 (feature version 202301) returns both MAC and ECDSA signature. + if (halApiVersion_ >= 5) { + ASSERT_TRUE(credential + ->finishRetrievalWithSignature(&mac, &deviceNameSpacesEncoded, + &ecdsaSignature) + .isOk()); + ASSERT_GT(ecdsaSignature.size(), 0); + } else { + ASSERT_TRUE(credential->finishRetrieval(&mac, &deviceNameSpacesEncoded).isOk()); + } cborPretty = cppbor::prettyPrint(deviceNameSpacesEncoded, 32, {}); ASSERT_EQ("{}", cborPretty); // Calculate DeviceAuthentication and MAC (MACing key hasn't changed) @@ -497,6 +531,21 @@ TEST_P(EndToEndTests, createAndRetrieveCredential) { ASSERT_TRUE(calculatedMac); EXPECT_EQ(mac, calculatedMac); + if (ecdsaSignature.size() > 0) { + vector encodedDeviceAuthentication = + cppbor::Array() + .add("DeviceAuthentication") + .add(sessionTranscript.clone()) + .add(docType) + .add(cppbor::SemanticTag(24, deviceNameSpacesEncoded)) + .encode(); + vector deviceAuthenticationBytes = + cppbor::SemanticTag(24, encodedDeviceAuthentication).encode(); + EXPECT_TRUE(support::coseCheckEcDsaSignature(ecdsaSignature, + deviceAuthenticationBytes, // Detached content + signingPubKey.value())); + } + // Some mDL apps might send a request but with a single empty // namespace. Check that too. RequestNamespace emptyRequestNS; @@ -508,7 +557,16 @@ TEST_P(EndToEndTests, createAndRetrieveCredential) { signingKeyBlob, sessionTranscriptEncoded, {}, // readerSignature, testEntriesEntryCounts) .isOk()); - ASSERT_TRUE(credential->finishRetrieval(&mac, &deviceNameSpacesEncoded).isOk()); + // API version 5 (feature version 202301) returns both MAC and ECDSA signature. + if (halApiVersion_ >= 5) { + ASSERT_TRUE(credential + ->finishRetrievalWithSignature(&mac, &deviceNameSpacesEncoded, + &ecdsaSignature) + .isOk()); + ASSERT_GT(ecdsaSignature.size(), 0); + } else { + ASSERT_TRUE(credential->finishRetrieval(&mac, &deviceNameSpacesEncoded).isOk()); + } cborPretty = cppbor::prettyPrint(deviceNameSpacesEncoded, 32, {}); ASSERT_EQ("{}", cborPretty); // Calculate DeviceAuthentication and MAC (MACing key hasn't changed) @@ -518,6 +576,248 @@ TEST_P(EndToEndTests, createAndRetrieveCredential) { eMacKey.value()); // EMacKey ASSERT_TRUE(calculatedMac); EXPECT_EQ(mac, calculatedMac); + + if (ecdsaSignature.size() > 0) { + vector encodedDeviceAuthentication = + cppbor::Array() + .add("DeviceAuthentication") + .add(sessionTranscript.clone()) + .add(docType) + .add(cppbor::SemanticTag(24, deviceNameSpacesEncoded)) + .encode(); + vector deviceAuthenticationBytes = + cppbor::SemanticTag(24, encodedDeviceAuthentication).encode(); + EXPECT_TRUE(support::coseCheckEcDsaSignature(ecdsaSignature, + deviceAuthenticationBytes, // Detached content + signingPubKey.value())); + } +} + +TEST_P(EndToEndTests, noSessionEncryption) { + if (halApiVersion_ < 5) { + GTEST_SKIP() << "Need HAL API version 5, have " << halApiVersion_; + } + + const vector testProfiles = {// Profile 0 (no authentication) + {0, {}, false, 0}}; + + HardwareAuthToken authToken; + VerificationToken verificationToken; + authToken.challenge = 0; + authToken.userId = 0; + authToken.authenticatorId = 0; + authToken.authenticatorType = ::android::hardware::keymaster::HardwareAuthenticatorType::NONE; + authToken.timestamp.milliSeconds = 0; + authToken.mac.clear(); + verificationToken.challenge = 0; + verificationToken.timestamp.milliSeconds = 0; + verificationToken.securityLevel = ::android::hardware::keymaster::SecurityLevel::SOFTWARE; + verificationToken.mac.clear(); + + // Here's the actual test data: + const vector testEntries = { + {"PersonalData", "Last name", string("Turing"), vector{0}}, + {"PersonalData", "Birth date", string("19120623"), vector{0}}, + {"PersonalData", "First name", string("Alan"), vector{0}}, + }; + const vector testEntriesEntryCounts = {3}; + HardwareInformation hwInfo; + ASSERT_TRUE(credentialStore_->getHardwareInformation(&hwInfo).isOk()); + + string cborPretty; + sp writableCredential; + ASSERT_TRUE(test_utils::setupWritableCredential(writableCredential, credentialStore_, + true /* testCredential */)); + + string challenge = "attestationChallenge"; + test_utils::AttestationData attData(writableCredential, challenge, + {1} /* atteestationApplicationId */); + ASSERT_TRUE(attData.result.isOk()) + << attData.result.exceptionCode() << "; " << attData.result.exceptionMessage() << endl; + + // This is kinda of a hack but we need to give the size of + // ProofOfProvisioning that we'll expect to receive. + const int32_t expectedProofOfProvisioningSize = 230; + // OK to fail, not available in v1 HAL + writableCredential->setExpectedProofOfProvisioningSize(expectedProofOfProvisioningSize); + ASSERT_TRUE( + writableCredential->startPersonalization(testProfiles.size(), testEntriesEntryCounts) + .isOk()); + + optional> secureProfiles = + test_utils::addAccessControlProfiles(writableCredential, testProfiles); + ASSERT_TRUE(secureProfiles); + + // 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) { + ASSERT_TRUE(test_utils::addEntry(writableCredential, entry, hwInfo.dataChunkSize, + encryptedBlobs, true)); + } + + vector credentialData; + vector proofOfProvisioningSignature; + ASSERT_TRUE( + writableCredential->finishAddingEntries(&credentialData, &proofOfProvisioningSignature) + .isOk()); + + // Validate the proofOfProvisioning which was returned + optional> proofOfProvisioning = + support::coseSignGetPayload(proofOfProvisioningSignature); + ASSERT_TRUE(proofOfProvisioning); + cborPretty = cppbor::prettyPrint(proofOfProvisioning.value(), 32, {"readerCertificate"}); + EXPECT_EQ( + "[\n" + " 'ProofOfProvisioning',\n" + " 'org.iso.18013-5.2019.mdl',\n" + " [\n" + " {\n" + " 'id' : 0,\n" + " },\n" + " ],\n" + " {\n" + " 'PersonalData' : [\n" + " {\n" + " 'name' : 'Last name',\n" + " 'value' : 'Turing',\n" + " 'accessControlProfiles' : [0, ],\n" + " },\n" + " {\n" + " 'name' : 'Birth date',\n" + " 'value' : '19120623',\n" + " 'accessControlProfiles' : [0, ],\n" + " },\n" + " {\n" + " 'name' : 'First name',\n" + " 'value' : 'Alan',\n" + " 'accessControlProfiles' : [0, ],\n" + " },\n" + " ],\n" + " },\n" + " true,\n" + "]", + cborPretty); + + optional> credentialPubKey = support::certificateChainGetTopMostKey( + attData.attestationCertificate[0].encodedCertificate); + ASSERT_TRUE(credentialPubKey); + EXPECT_TRUE(support::coseCheckEcDsaSignature(proofOfProvisioningSignature, + {}, // Additional data + credentialPubKey.value())); + writableCredential = nullptr; + + // Extract doctype, storage key, and credentialPrivKey from credentialData... this works + // only because we asked for a test-credential meaning that the HBK is all zeroes. + auto [exSuccess, exDocType, exStorageKey, exCredentialPrivKey, exSha256Pop] = + extractFromTestCredentialData(credentialData); + + ASSERT_TRUE(exSuccess); + ASSERT_EQ(exDocType, "org.iso.18013-5.2019.mdl"); + // ... check that the public key derived from the private key matches what was + // in the certificate. + optional> exCredentialKeyPair = + support::ecPrivateKeyToKeyPair(exCredentialPrivKey); + ASSERT_TRUE(exCredentialKeyPair); + optional> exCredentialPubKey = + support::ecKeyPairGetPublicKey(exCredentialKeyPair.value()); + ASSERT_TRUE(exCredentialPubKey); + ASSERT_EQ(exCredentialPubKey.value(), credentialPubKey.value()); + + sp credential; + ASSERT_TRUE(credentialStore_ + ->getCredential( + CipherSuite::CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256, + credentialData, &credential) + .isOk()); + ASSERT_NE(credential, nullptr); + + // Calculate sessionTranscript, make something that resembles what you'd use for + // an over-the-Internet presentation not using mdoc session encryption. + cppbor::Array sessionTranscript = + cppbor::Array() + .add(cppbor::Null()) // DeviceEngagementBytes isn't used. + .add(cppbor::Null()) // EReaderKeyBytes isn't used. + .add(cppbor::Array() // Proprietary handover structure follows. + .add(cppbor::Tstr("TestHandover")) + .add(cppbor::Bstr(vector{1, 2, 3})) + .add(cppbor::Bstr(vector{9, 8, 7, 6}))); + vector sessionTranscriptEncoded = sessionTranscript.encode(); + + // Generate the key that will be used to sign AuthenticatedData. + vector signingKeyBlob; + Certificate signingKeyCertificate; + ASSERT_TRUE(credential->generateSigningKeyPair(&signingKeyBlob, &signingKeyCertificate).isOk()); + optional> signingPubKey = + support::certificateChainGetTopMostKey(signingKeyCertificate.encodedCertificate); + EXPECT_TRUE(signingPubKey); + test_utils::verifyAuthKeyCertificate(signingKeyCertificate.encodedCertificate); + + vector requestedNamespaces = test_utils::buildRequestNamespaces(testEntries); + ASSERT_TRUE(credential->setRequestedNamespaces(requestedNamespaces).isOk()); + ASSERT_TRUE(credential->setVerificationToken(verificationToken).isOk()); + Status status = credential->startRetrieval( + secureProfiles.value(), authToken, {} /* itemsRequestBytes*/, signingKeyBlob, + sessionTranscriptEncoded, {} /* readerSignature */, testEntriesEntryCounts); + ASSERT_TRUE(status.isOk()) << status.exceptionCode() << ": " << status.exceptionMessage(); + + for (const auto& entry : testEntries) { + ASSERT_TRUE(credential + ->startRetrieveEntryValue(entry.nameSpace, entry.name, + entry.valueCbor.size(), entry.profileIds) + .isOk()); + + auto it = encryptedBlobs.find(&entry); + ASSERT_NE(it, encryptedBlobs.end()); + const vector>& encryptedChunks = it->second; + + vector content; + for (const auto& encryptedChunk : encryptedChunks) { + vector chunk; + ASSERT_TRUE(credential->retrieveEntryValue(encryptedChunk, &chunk).isOk()); + content.insert(content.end(), chunk.begin(), chunk.end()); + } + EXPECT_EQ(content, entry.valueCbor); + } + + vector mac; + vector ecdsaSignature; + vector deviceNameSpacesEncoded; + status = credential->finishRetrievalWithSignature(&mac, &deviceNameSpacesEncoded, + &ecdsaSignature); + ASSERT_TRUE(status.isOk()) << status.exceptionCode() << ": " << status.exceptionMessage(); + // MACing should NOT work since we're not using session encryption + ASSERT_EQ(0, mac.size()); + + // ECDSA signatures should work, however. Check this. + ASSERT_GT(ecdsaSignature.size(), 0); + + cborPretty = cppbor::prettyPrint(deviceNameSpacesEncoded, 32, {}); + ASSERT_EQ( + "{\n" + " 'PersonalData' : {\n" + " 'Last name' : 'Turing',\n" + " 'Birth date' : '19120623',\n" + " 'First name' : 'Alan',\n" + " },\n" + "}", + cborPretty); + + string docType = "org.iso.18013-5.2019.mdl"; + + vector encodedDeviceAuthentication = + cppbor::Array() + .add("DeviceAuthentication") + .add(sessionTranscript.clone()) + .add(docType) + .add(cppbor::SemanticTag(24, deviceNameSpacesEncoded)) + .encode(); + vector deviceAuthenticationBytes = + cppbor::SemanticTag(24, encodedDeviceAuthentication).encode(); + EXPECT_TRUE(support::coseCheckEcDsaSignature(ecdsaSignature, + deviceAuthenticationBytes, // Detached content + signingPubKey.value())); } GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(EndToEndTests);