Merge "identity: Add support for ECDSA auth and don't require session encryption." am: 4c02ef2b4e

Original change: https://android-review.googlesource.com/c/platform/hardware/interfaces/+/2241333

Change-Id: Ia055667f3967492be14a7bcf0f330d7399f18711
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Treehugger Robot
2022-12-12 23:14:28 +00:00
committed by Automerger Merge Worker
17 changed files with 708 additions and 201 deletions

View File

@@ -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",
],
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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.
*

View File

@@ -596,10 +596,10 @@ bool FakeSecureHardwarePresentationProxy::startRetrieveEntries() {
return eicPresentationStartRetrieveEntries(&ctx_);
}
bool FakeSecureHardwarePresentationProxy::calcMacKey(
bool FakeSecureHardwarePresentationProxy::prepareDeviceAuthentication(
const vector<uint8_t>& sessionTranscript, const vector<uint8_t>& readerEphemeralPublicKey,
const vector<uint8_t>& 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<vector<uint8_t>> FakeSecureHardwarePresentationProxy::retrieveEntryValu
return content;
}
optional<pair<vector<uint8_t>, vector<uint8_t>>>
FakeSecureHardwarePresentationProxy::finishRetrievalWithSignature() {
if (!validateId(__func__)) {
return std::nullopt;
}
vector<uint8_t> mac(32);
size_t macSize = 32;
vector<uint8_t> 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<vector<uint8_t>> FakeSecureHardwarePresentationProxy::finishRetrieval() {
if (!validateId(__func__)) {
return std::nullopt;

View File

@@ -175,11 +175,11 @@ class FakeSecureHardwarePresentationProxy : public SecureHardwarePresentationPro
const vector<uint8_t>& requestMessage, int coseSignAlg,
const vector<uint8_t>& readerSignatureOfToBeSigned) override;
bool calcMacKey(const vector<uint8_t>& sessionTranscript,
const vector<uint8_t>& readerEphemeralPublicKey,
const vector<uint8_t>& signingKeyBlob, const string& docType,
unsigned int numNamespacesWithValues,
size_t expectedProofOfProvisioningSize) override;
bool prepareDeviceAuthentication(const vector<uint8_t>& sessionTranscript,
const vector<uint8_t>& readerEphemeralPublicKey,
const vector<uint8_t>& 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<vector<uint8_t>> finishRetrieval() override;
optional<pair<vector<uint8_t>, vector<uint8_t>>> finishRetrievalWithSignature() override;
optional<vector<uint8_t>> deleteCredential(const string& docType,
const vector<uint8_t>& challenge,
bool includeChallenge,

View File

@@ -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<uint8_t> 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<uint8_t>(readerPublicKey_.begin() + 1, readerPublicKey_.end());
}
vector<uint8_t> 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<uint8_t>&
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus IdentityCredential::finishRetrieval(vector<uint8_t>* outMac,
vector<uint8_t>* outDeviceNameSpaces) {
ndk::ScopedAStatus IdentityCredential::finishRetrievalWithSignature(
vector<uint8_t>* outMac, vector<uint8_t>* outDeviceNameSpaces,
vector<uint8_t>* outEcdsaSignature) {
ndk::ScopedAStatus status = ensureHwProxy();
if (!status.isOk()) {
return status;
@@ -948,17 +951,34 @@ ndk::ScopedAStatus IdentityCredential::finishRetrieval(vector<uint8_t>* outMac,
.c_str()));
}
optional<vector<uint8_t>> digestToBeMaced;
optional<vector<uint8_t>> 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<vector<uint8_t>> mac;
optional<vector<uint8_t>> 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<vector<uint8_t>> 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<uint8_t>* outMac,
}
mac = support::coseMacWithDigest(digestToBeMaced.value(), {} /* data */);
}
*outMac = mac.value_or(vector<uint8_t>({}));
optional<vector<uint8_t>> signature;
if (signatureToBeSigned && signatureToBeSigned.value().size() != 0) {
signature = support::coseSignEcDsaWithSignature(signatureToBeSigned.value(), {}, // data
{}); // certificateChain
}
if (outEcdsaSignature != nullptr) {
*outEcdsaSignature = signature.value_or(vector<uint8_t>({}));
}
*outDeviceNameSpaces = encodedDeviceNameSpaces;
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus IdentityCredential::finishRetrieval(vector<uint8_t>* outMac,
vector<uint8_t>* outDeviceNameSpaces) {
return finishRetrievalWithSignature(outMac, outDeviceNameSpaces, nullptr);
}
ndk::ScopedAStatus IdentityCredential::generateSigningKeyPair(
vector<uint8_t>* outSigningKeyBlob, Certificate* outSigningKeyCertificate) {
if (session_) {

View File

@@ -92,6 +92,10 @@ class IdentityCredential : public BnIdentityCredential {
ndk::ScopedAStatus updateCredential(
shared_ptr<IWritableIdentityCredential>* outWritableCredential) override;
ndk::ScopedAStatus finishRetrievalWithSignature(vector<uint8_t>* outMac,
vector<uint8_t>* outDeviceNameSpaces,
vector<uint8_t>* outEcdsaSignature) override;
private:
ndk::ScopedAStatus deleteCredentialCommon(const vector<uint8_t>& challenge,
bool includeChallenge,

View File

@@ -54,19 +54,6 @@ int PresentationSession::initialize() {
}
id_ = id.value();
optional<vector<uint8_t>> ephemeralKeyPriv = hwProxy_->getEphemeralKeyPair();
if (!ephemeralKeyPriv) {
LOG(ERROR) << "Error getting ephemeral private key for session";
return IIdentityCredentialStore::STATUS_FAILED;
}
optional<vector<uint8_t>> ephemeralKeyPair =
support::ecPrivateKeyToKeyPair(ephemeralKeyPriv.value());
if (!ephemeralKeyPair) {
LOG(ERROR) << "Error creating ephemeral key-pair";
return IIdentityCredentialStore::STATUS_FAILED;
}
ephemeralKeyPair_ = ephemeralKeyPair.value();
optional<uint64_t> authChallenge = hwProxy_->getAuthChallenge();
if (!authChallenge) {
LOG(ERROR) << "Error getting authChallenge for session";
@@ -78,6 +65,23 @@ int PresentationSession::initialize() {
}
ndk::ScopedAStatus PresentationSession::getEphemeralKeyPair(vector<uint8_t>* outKeyPair) {
if (ephemeralKeyPair_.size() == 0) {
optional<vector<uint8_t>> 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<vector<uint8_t>> 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();
}

View File

@@ -72,9 +72,11 @@ class PresentationSession : public BnPresentationSession {
// Set by initialize()
uint64_t id_;
vector<uint8_t> ephemeralKeyPair_;
uint64_t authChallenge_;
// Set by getEphemeralKeyPair()
vector<uint8_t> ephemeralKeyPair_;
// Set by setReaderEphemeralPublicKey()
vector<uint8_t> readerPublicKey_;

View File

@@ -194,11 +194,12 @@ class SecureHardwarePresentationProxy : public RefBase {
const vector<uint8_t>& requestMessage, int coseSignAlg,
const vector<uint8_t>& readerSignatureOfToBeSigned) = 0;
virtual bool calcMacKey(const vector<uint8_t>& sessionTranscript,
const vector<uint8_t>& readerEphemeralPublicKey,
const vector<uint8_t>& signingKeyBlob, const string& docType,
unsigned int numNamespacesWithValues,
size_t expectedProofOfProvisioningSize) = 0;
virtual bool prepareDeviceAuthentication(const vector<uint8_t>& sessionTranscript,
const vector<uint8_t>& readerEphemeralPublicKey,
const vector<uint8_t>& 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<int32_t>& accessControlProfileIds) = 0;
virtual optional<vector<uint8_t>> finishRetrieval();
virtual optional<pair<vector<uint8_t>, vector<uint8_t>>> finishRetrievalWithSignature();
virtual optional<vector<uint8_t>> deleteCredential(const string& docType,
const vector<uint8_t>& challenge,

View File

@@ -1,7 +1,7 @@
<manifest version="1.0" type="device">
<hal format="aidl">
<name>android.hardware.identity</name>
<version>4</version>
<version>5</version>
<interface>
<name>IIdentityCredentialStore</name>
<instance>default</instance>

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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

View File

@@ -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);

View File

@@ -441,8 +441,18 @@ TEST_P(EndToEndTests, createAndRetrieveCredential) {
}
vector<uint8_t> mac;
vector<uint8_t> ecdsaSignature;
vector<uint8_t> 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<uint8_t> encodedDeviceAuthentication =
cppbor::Array()
.add("DeviceAuthentication")
.add(sessionTranscript.clone())
.add(docType)
.add(cppbor::SemanticTag(24, deviceNameSpacesEncoded))
.encode();
vector<uint8_t> 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<uint8_t> encodedDeviceAuthentication =
cppbor::Array()
.add("DeviceAuthentication")
.add(sessionTranscript.clone())
.add(docType)
.add(cppbor::SemanticTag(24, deviceNameSpacesEncoded))
.encode();
vector<uint8_t> 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<uint8_t> encodedDeviceAuthentication =
cppbor::Array()
.add("DeviceAuthentication")
.add(sessionTranscript.clone())
.add(docType)
.add(cppbor::SemanticTag(24, deviceNameSpacesEncoded))
.encode();
vector<uint8_t> 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<test_utils::TestProfile> 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<test_utils::TestEntryData> testEntries = {
{"PersonalData", "Last name", string("Turing"), vector<int32_t>{0}},
{"PersonalData", "Birth date", string("19120623"), vector<int32_t>{0}},
{"PersonalData", "First name", string("Alan"), vector<int32_t>{0}},
};
const vector<int32_t> testEntriesEntryCounts = {3};
HardwareInformation hwInfo;
ASSERT_TRUE(credentialStore_->getHardwareInformation(&hwInfo).isOk());
string cborPretty;
sp<IWritableIdentityCredential> 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<vector<SecureAccessControlProfile>> 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<const test_utils::TestEntryData*, vector<vector<uint8_t>>> encryptedBlobs;
for (const auto& entry : testEntries) {
ASSERT_TRUE(test_utils::addEntry(writableCredential, entry, hwInfo.dataChunkSize,
encryptedBlobs, true));
}
vector<uint8_t> credentialData;
vector<uint8_t> proofOfProvisioningSignature;
ASSERT_TRUE(
writableCredential->finishAddingEntries(&credentialData, &proofOfProvisioningSignature)
.isOk());
// Validate the proofOfProvisioning which was returned
optional<vector<uint8_t>> 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<vector<uint8_t>> 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<vector<uint8_t>> exCredentialKeyPair =
support::ecPrivateKeyToKeyPair(exCredentialPrivKey);
ASSERT_TRUE(exCredentialKeyPair);
optional<vector<uint8_t>> exCredentialPubKey =
support::ecKeyPairGetPublicKey(exCredentialKeyPair.value());
ASSERT_TRUE(exCredentialPubKey);
ASSERT_EQ(exCredentialPubKey.value(), credentialPubKey.value());
sp<IIdentityCredential> 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<uint8_t>{1, 2, 3}))
.add(cppbor::Bstr(vector<uint8_t>{9, 8, 7, 6})));
vector<uint8_t> sessionTranscriptEncoded = sessionTranscript.encode();
// Generate the key that will be used to sign AuthenticatedData.
vector<uint8_t> signingKeyBlob;
Certificate signingKeyCertificate;
ASSERT_TRUE(credential->generateSigningKeyPair(&signingKeyBlob, &signingKeyCertificate).isOk());
optional<vector<uint8_t>> signingPubKey =
support::certificateChainGetTopMostKey(signingKeyCertificate.encodedCertificate);
EXPECT_TRUE(signingPubKey);
test_utils::verifyAuthKeyCertificate(signingKeyCertificate.encodedCertificate);
vector<RequestNamespace> 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<vector<uint8_t>>& encryptedChunks = it->second;
vector<uint8_t> content;
for (const auto& encryptedChunk : encryptedChunks) {
vector<uint8_t> chunk;
ASSERT_TRUE(credential->retrieveEntryValue(encryptedChunk, &chunk).isOk());
content.insert(content.end(), chunk.begin(), chunk.end());
}
EXPECT_EQ(content, entry.valueCbor);
}
vector<uint8_t> mac;
vector<uint8_t> ecdsaSignature;
vector<uint8_t> 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<uint8_t> encodedDeviceAuthentication =
cppbor::Array()
.add("DeviceAuthentication")
.add(sessionTranscript.clone())
.add(docType)
.add(cppbor::SemanticTag(24, deviceNameSpacesEncoded))
.encode();
vector<uint8_t> deviceAuthenticationBytes =
cppbor::SemanticTag(24, encodedDeviceAuthentication).encode();
EXPECT_TRUE(support::coseCheckEcDsaSignature(ecdsaSignature,
deviceAuthenticationBytes, // Detached content
signingPubKey.value()));
}
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(EndToEndTests);