From f3e0600395fd518dd71e47b044ec1384262e75ef Mon Sep 17 00:00:00 2001 From: David Zeuthen Date: Tue, 4 Oct 2022 13:17:14 -0400 Subject: [PATCH] identity: Add support for ECDSA auth and don't require session encryption. This adds a new method which allows applications to use mdoc ECDSA authentication instead of mdoc MAC authentication. Additionally, also relax requirements on SessionTranscript so the APIs can be used even when mdoc session encryption isn't being used. Also add new VTS test to check for this. Since this is new API, bump API version to 5 and the Identity Credential feature version to 202301. Bug: 241912421 Test: atest VtsHalIdentityTargetTest Test: atest android.security.identity.cts Change-Id: I4085a89be0382c10f5449e13c6a92a46c74c225d --- identity/aidl/Android.bp | 8 +- .../identity/IIdentityCredential.aidl | 1 + .../identity/IIdentityCredential.aidl | 51 ++- .../identity/IPresentationSession.aidl | 11 + .../aidl/default/FakeSecureHardwareProxy.cpp | 31 +- .../aidl/default/FakeSecureHardwareProxy.h | 12 +- .../default/common/IdentityCredential.cpp | 107 +++--- .../aidl/default/common/IdentityCredential.h | 4 + .../default/common/PresentationSession.cpp | 30 +- .../aidl/default/common/PresentationSession.h | 4 +- .../aidl/default/common/SecureHardwareProxy.h | 12 +- identity/aidl/default/identity-default.xml | 2 +- .../aidl/default/libeic/EicPresentation.c | 270 ++++++++++------ .../aidl/default/libeic/EicPresentation.h | 27 +- identity/aidl/default/libeic/EicSession.c | 29 +- identity/aidl/default/libeic/EicSession.h | 4 + identity/aidl/vts/EndToEndTests.cpp | 306 +++++++++++++++++- 17 files changed, 708 insertions(+), 201 deletions(-) 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);