platform_hardware_interfaces/identity/aidl/default/common/IdentityCredential.cpp
David Zeuthen f3e0600395 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
2022-12-09 02:57:09 -05:00

1061 lines
46 KiB
C++

/*
* Copyright 2019, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define LOG_TAG "IdentityCredential"
#include "IdentityCredential.h"
#include "IdentityCredentialStore.h"
#include <android/hardware/identity/support/IdentityCredentialSupport.h>
#include <string.h>
#include <android-base/logging.h>
#include <android-base/stringprintf.h>
#include <cppbor.h>
#include <cppbor_parse.h>
#include "FakeSecureHardwareProxy.h"
#include "WritableIdentityCredential.h"
namespace aidl::android::hardware::identity {
using ::aidl::android::hardware::keymaster::Timestamp;
using ::android::base::StringPrintf;
using ::std::optional;
using namespace ::android::hardware::identity;
int IdentityCredential::initialize() {
if (credentialData_.size() == 0) {
LOG(ERROR) << "CredentialData is empty";
return IIdentityCredentialStore::STATUS_INVALID_DATA;
}
auto [item, _, message] = cppbor::parse(credentialData_);
if (item == nullptr) {
LOG(ERROR) << "CredentialData is not valid CBOR: " << message;
return IIdentityCredentialStore::STATUS_INVALID_DATA;
}
const cppbor::Array* arrayItem = item->asArray();
if (arrayItem == nullptr || arrayItem->size() != 3) {
LOG(ERROR) << "CredentialData is not an array with three elements";
return IIdentityCredentialStore::STATUS_INVALID_DATA;
}
const cppbor::Tstr* docTypeItem = (*arrayItem)[0]->asTstr();
const cppbor::Bool* testCredentialItem =
((*arrayItem)[1]->asSimple() != nullptr ? ((*arrayItem)[1]->asSimple()->asBool())
: nullptr);
const cppbor::Bstr* encryptedCredentialKeysItem = (*arrayItem)[2]->asBstr();
if (docTypeItem == nullptr || testCredentialItem == nullptr ||
encryptedCredentialKeysItem == nullptr) {
LOG(ERROR) << "CredentialData unexpected item types";
return IIdentityCredentialStore::STATUS_INVALID_DATA;
}
docType_ = docTypeItem->value();
testCredential_ = testCredentialItem->value();
encryptedCredentialKeys_ = encryptedCredentialKeysItem->value();
// If in a session, delay the initialization of the proxy.
//
if (!session_) {
ndk::ScopedAStatus status = ensureHwProxy();
if (!status.isOk()) {
LOG(ERROR) << "Error initializing hw proxy";
return IIdentityCredentialStore::STATUS_FAILED;
}
}
return IIdentityCredentialStore::STATUS_OK;
}
ndk::ScopedAStatus IdentityCredential::ensureHwProxy() {
if (hwProxy_) {
return ndk::ScopedAStatus::ok();
}
hwProxy_ = hwProxyFactory_->createPresentationProxy();
if (!hwProxy_) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED, "Error creating hw proxy"));
}
uint64_t sessionId = session_ ? session_->getSessionId() : EIC_PRESENTATION_ID_UNSET;
if (!hwProxy_->initialize(sessionId, testCredential_, docType_, encryptedCredentialKeys_)) {
hwProxy_.clear();
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED, "Error initializing hw proxy"));
}
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus IdentityCredential::deleteCredential(
vector<uint8_t>* outProofOfDeletionSignature) {
return deleteCredentialCommon({}, false, outProofOfDeletionSignature);
}
ndk::ScopedAStatus IdentityCredential::deleteCredentialWithChallenge(
const vector<uint8_t>& challenge, vector<uint8_t>* outProofOfDeletionSignature) {
return deleteCredentialCommon(challenge, true, outProofOfDeletionSignature);
}
ndk::ScopedAStatus IdentityCredential::deleteCredentialCommon(
const vector<uint8_t>& challenge, bool includeChallenge,
vector<uint8_t>* outProofOfDeletionSignature) {
if (session_) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED, "Cannot be called in a session"));
}
ndk::ScopedAStatus status = ensureHwProxy();
if (!status.isOk()) {
return status;
}
if (challenge.size() > 32) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_DATA, "Challenge too big"));
}
cppbor::Array array = {"ProofOfDeletion", docType_, testCredential_};
if (includeChallenge) {
array = {"ProofOfDeletion", docType_, challenge, testCredential_};
}
vector<uint8_t> proofOfDeletionCbor = array.encode();
vector<uint8_t> podDigest = support::sha256(proofOfDeletionCbor);
optional<vector<uint8_t>> signatureOfToBeSigned = hwProxy_->deleteCredential(
docType_, challenge, includeChallenge, proofOfDeletionCbor.size());
if (!signatureOfToBeSigned) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED, "Error signing ProofOfDeletion"));
}
optional<vector<uint8_t>> signature =
support::coseSignEcDsaWithSignature(signatureOfToBeSigned.value(),
proofOfDeletionCbor, // data
{}); // certificateChain
if (!signature) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED, "Error signing data"));
}
*outProofOfDeletionSignature = signature.value();
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus IdentityCredential::proveOwnership(
const vector<uint8_t>& challenge, vector<uint8_t>* outProofOfOwnershipSignature) {
if (session_) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED, "Cannot be called in a session"));
}
ndk::ScopedAStatus status = ensureHwProxy();
if (!status.isOk()) {
return status;
}
if (challenge.size() > 32) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_DATA, "Challenge too big"));
}
cppbor::Array array;
array = {"ProofOfOwnership", docType_, challenge, testCredential_};
vector<uint8_t> proofOfOwnershipCbor = array.encode();
vector<uint8_t> podDigest = support::sha256(proofOfOwnershipCbor);
optional<vector<uint8_t>> signatureOfToBeSigned = hwProxy_->proveOwnership(
docType_, testCredential_, challenge, proofOfOwnershipCbor.size());
if (!signatureOfToBeSigned) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED, "Error signing ProofOfOwnership"));
}
optional<vector<uint8_t>> signature =
support::coseSignEcDsaWithSignature(signatureOfToBeSigned.value(),
proofOfOwnershipCbor, // data
{}); // certificateChain
if (!signature) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED, "Error signing data"));
}
*outProofOfOwnershipSignature = signature.value();
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus IdentityCredential::createEphemeralKeyPair(vector<uint8_t>* outKeyPair) {
if (session_) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED, "Cannot be called in a session"));
}
ndk::ScopedAStatus status = ensureHwProxy();
if (!status.isOk()) {
return status;
}
optional<vector<uint8_t>> ephemeralPriv = hwProxy_->createEphemeralKeyPair();
if (!ephemeralPriv) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED, "Error creating ephemeral key"));
}
optional<vector<uint8_t>> keyPair = support::ecPrivateKeyToKeyPair(ephemeralPriv.value());
if (!keyPair) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED, "Error creating ephemeral key-pair"));
}
// Stash public key of this key-pair for later check in startRetrieval().
optional<vector<uint8_t>> publicKey = support::ecKeyPairGetPublicKey(keyPair.value());
if (!publicKey) {
LOG(ERROR) << "Error getting public part of ephemeral key pair";
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED,
"Error getting public part of ephemeral key pair"));
}
ephemeralPublicKey_ = publicKey.value();
*outKeyPair = keyPair.value();
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus IdentityCredential::setReaderEphemeralPublicKey(
const vector<uint8_t>& publicKey) {
if (session_) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED, "Cannot be called in a session"));
}
readerPublicKey_ = publicKey;
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus IdentityCredential::createAuthChallenge(int64_t* outChallenge) {
if (session_) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED, "Cannot be called in a session"));
}
ndk::ScopedAStatus status = ensureHwProxy();
if (!status.isOk()) {
return status;
}
optional<uint64_t> challenge = hwProxy_->createAuthChallenge();
if (!challenge) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED, "Error generating challenge"));
}
*outChallenge = challenge.value();
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus IdentityCredential::setRequestedNamespaces(
const vector<RequestNamespace>& requestNamespaces) {
requestNamespaces_ = requestNamespaces;
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus IdentityCredential::setVerificationToken(
const VerificationToken& verificationToken) {
verificationToken_ = verificationToken;
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus IdentityCredential::startRetrieval(
const vector<SecureAccessControlProfile>& accessControlProfiles,
const HardwareAuthToken& authToken, const vector<uint8_t>& itemsRequest,
const vector<uint8_t>& signingKeyBlob, const vector<uint8_t>& sessionTranscript,
const vector<uint8_t>& readerSignature, const vector<int32_t>& requestCounts) {
ndk::ScopedAStatus status = ensureHwProxy();
if (!status.isOk()) {
return status;
}
// If in a session, ensure the passed-in session transcript matches the
// session transcript from the session.
if (session_) {
if (sessionTranscript != session_->getSessionTranscript()) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_SESSION_TRANSCRIPT_MISMATCH,
"In a session and passed-in SessionTranscript doesn't match the one "
"from the session"));
}
}
if (numStartRetrievalCalls_ > 0) {
if (sessionTranscript_ != sessionTranscript) {
LOG(ERROR) << "Session Transcript changed";
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_SESSION_TRANSCRIPT_MISMATCH,
"Passed-in SessionTranscript doesn't match previously used SessionTranscript"));
}
}
sessionTranscript_ = sessionTranscript;
// This resets various state in the TA...
if (!hwProxy_->startRetrieveEntries()) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED, "Error starting retrieving entries"));
}
optional<vector<uint8_t>> signatureOfToBeSigned;
if (readerSignature.size() > 0) {
signatureOfToBeSigned = support::coseSignGetSignature(readerSignature);
if (!signatureOfToBeSigned) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_READER_SIGNATURE_CHECK_FAILED,
"Error extracting signatureOfToBeSigned from COSE_Sign1"));
}
}
// Feed the auth token to secure hardware only if they're valid.
if (authToken.timestamp.milliSeconds != 0) {
if (!hwProxy_->setAuthToken(
authToken.challenge, authToken.userId, authToken.authenticatorId,
int(authToken.authenticatorType), authToken.timestamp.milliSeconds,
authToken.mac, verificationToken_.challenge,
verificationToken_.timestamp.milliSeconds,
int(verificationToken_.securityLevel), verificationToken_.mac)) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_DATA, "Invalid Auth Token"));
}
}
// We'll be feeding ACPs interleaved with certificates from the reader
// certificate chain...
vector<SecureAccessControlProfile> remainingAcps = accessControlProfiles;
// ... and we'll use those ACPs to build up a 32-bit mask indicating which
// of the possible 32 ACPs grants access.
uint32_t accessControlProfileMask = 0;
// If there is a signature, validate that it was made with the top-most key in the
// certificate chain embedded in the COSE_Sign1 structure.
optional<vector<uint8_t>> readerCertificateChain;
if (readerSignature.size() > 0) {
readerCertificateChain = support::coseSignGetX5Chain(readerSignature);
if (!readerCertificateChain) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_READER_SIGNATURE_CHECK_FAILED,
"Unable to get reader certificate chain from COSE_Sign1"));
}
// First, feed all the reader certificates to the secure hardware. We start
// at the end..
optional<vector<vector<uint8_t>>> splitCerts =
support::certificateChainSplit(readerCertificateChain.value());
if (!splitCerts || splitCerts.value().size() == 0) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_READER_SIGNATURE_CHECK_FAILED,
"Error splitting certificate chain from COSE_Sign1"));
}
for (ssize_t n = splitCerts.value().size() - 1; n >= 0; --n) {
const vector<uint8_t>& x509Cert = splitCerts.value()[n];
if (!hwProxy_->pushReaderCert(x509Cert)) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_READER_SIGNATURE_CHECK_FAILED,
StringPrintf("Error validating reader certificate %zd", n).c_str()));
}
// If we have ACPs for that particular certificate, send them to the
// TA right now...
//
// Remember in this case certificate equality is done by comparing public keys,
// not bitwise comparison of the certificates.
//
optional<vector<uint8_t>> x509CertPubKey =
support::certificateChainGetTopMostKey(x509Cert);
if (!x509CertPubKey) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED,
StringPrintf("Error getting public key from reader certificate %zd", n)
.c_str()));
}
vector<SecureAccessControlProfile>::iterator it = remainingAcps.begin();
while (it != remainingAcps.end()) {
const SecureAccessControlProfile& profile = *it;
if (profile.readerCertificate.encodedCertificate.size() == 0) {
++it;
continue;
}
optional<vector<uint8_t>> profilePubKey = support::certificateChainGetTopMostKey(
profile.readerCertificate.encodedCertificate);
if (!profilePubKey) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED,
"Error getting public key from profile"));
}
if (profilePubKey.value() == x509CertPubKey.value()) {
optional<bool> res = hwProxy_->validateAccessControlProfile(
profile.id, profile.readerCertificate.encodedCertificate,
profile.userAuthenticationRequired, profile.timeoutMillis,
profile.secureUserId, profile.mac);
if (!res) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_DATA,
"Error validating access control profile"));
}
if (res.value()) {
accessControlProfileMask |= (1 << profile.id);
}
it = remainingAcps.erase(it);
} else {
++it;
}
}
}
// ... then pass the request message and have the TA check it's signed by the
// key in last certificate we pushed.
if (sessionTranscript.size() > 0 && itemsRequest.size() > 0 && readerSignature.size() > 0) {
optional<vector<uint8_t>> tbsSignature = support::coseSignGetSignature(readerSignature);
if (!tbsSignature) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_READER_SIGNATURE_CHECK_FAILED,
"Error extracting toBeSigned from COSE_Sign1"));
}
optional<int> coseSignAlg = support::coseSignGetAlg(readerSignature);
if (!coseSignAlg) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_READER_SIGNATURE_CHECK_FAILED,
"Error extracting signature algorithm from COSE_Sign1"));
}
if (!hwProxy_->validateRequestMessage(sessionTranscript, itemsRequest,
coseSignAlg.value(), tbsSignature.value())) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_READER_SIGNATURE_CHECK_FAILED,
"readerMessage is not signed by top-level certificate"));
}
}
}
// Feed remaining access control profiles...
for (const SecureAccessControlProfile& profile : remainingAcps) {
optional<bool> res = hwProxy_->validateAccessControlProfile(
profile.id, profile.readerCertificate.encodedCertificate,
profile.userAuthenticationRequired, profile.timeoutMillis, profile.secureUserId,
profile.mac);
if (!res) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_DATA,
"Error validating access control profile"));
}
if (res.value()) {
accessControlProfileMask |= (1 << profile.id);
}
}
if (session_) {
// If presenting in a session, the TA has already done the check for (X, Y) as done
// below, see eicSessionSetSessionTranscript().
} else {
// 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.
if (sessionTranscript.size() > 0 && ephemeralPublicKey_.size() > 0) {
auto [getXYSuccess, ePubX, ePubY] = support::ecPublicKeyGetXandY(ephemeralPublicKey_);
if (!getXYSuccess) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_EPHEMERAL_PUBLIC_KEY_NOT_FOUND,
"Error extracting X and Y from ePub"));
}
if (sessionTranscript.size() > 0 &&
!(memmem(sessionTranscript.data(), sessionTranscript.size(), ePubX.data(),
ePubX.size()) != nullptr &&
memmem(sessionTranscript.data(), sessionTranscript.size(), ePubY.data(),
ePubY.size()) != nullptr)) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_EPHEMERAL_PUBLIC_KEY_NOT_FOUND,
"Did not find ephemeral public key's X and Y coordinates in "
"SessionTranscript (make sure leading zeroes are not used)"));
}
}
}
// itemsRequest: If non-empty, contains request data that may be signed by the
// reader. The content can be defined in the way appropriate for the
// credential, but there are three requirements that must be met to work with
// this HAL:
if (itemsRequest.size() > 0) {
// 1. The content must be a CBOR-encoded structure.
auto [item, _, message] = cppbor::parse(itemsRequest);
if (item == nullptr) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_ITEMS_REQUEST_MESSAGE,
"Error decoding CBOR in itemsRequest"));
}
// 2. The CBOR structure must be a map.
const cppbor::Map* map = item->asMap();
if (map == nullptr) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_ITEMS_REQUEST_MESSAGE,
"itemsRequest is not a CBOR map"));
}
// 3. The map must contain a key "nameSpaces" whose value contains a map, as described in
// the example below.
//
// NameSpaces = {
// + NameSpace => DataElements ; Requested data elements for each NameSpace
// }
//
// NameSpace = tstr
//
// DataElements = {
// + DataElement => IntentToRetain
// }
//
// DataElement = tstr
// IntentToRetain = bool
//
// Here's an example of an |itemsRequest| CBOR value satisfying above requirements 1.
// through 3.:
//
// {
// 'docType' : 'org.iso.18013-5.2019',
// 'nameSpaces' : {
// 'org.iso.18013-5.2019' : {
// 'Last name' : false,
// 'Birth date' : false,
// 'First name' : false,
// 'Home address' : true
// },
// 'org.aamva.iso.18013-5.2019' : {
// 'Real Id' : false
// }
// }
// }
//
const cppbor::Map* nsMap = nullptr;
for (size_t n = 0; n < map->size(); n++) {
const auto& [keyItem, valueItem] = (*map)[n];
if (keyItem->type() == cppbor::TSTR && keyItem->asTstr()->value() == "nameSpaces" &&
valueItem->type() == cppbor::MAP) {
nsMap = valueItem->asMap();
break;
}
}
if (nsMap == nullptr) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_ITEMS_REQUEST_MESSAGE,
"No nameSpaces map in top-most map"));
}
for (size_t n = 0; n < nsMap->size(); n++) {
auto& [nsKeyItem, nsValueItem] = (*nsMap)[n];
const cppbor::Tstr* nsKey = nsKeyItem->asTstr();
const cppbor::Map* nsInnerMap = nsValueItem->asMap();
if (nsKey == nullptr || nsInnerMap == nullptr) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_ITEMS_REQUEST_MESSAGE,
"Type mismatch in nameSpaces map"));
}
string requestedNamespace = nsKey->value();
set<string> requestedKeys;
for (size_t m = 0; m < nsInnerMap->size(); m++) {
const auto& [innerMapKeyItem, innerMapValueItem] = (*nsInnerMap)[m];
const cppbor::Tstr* nameItem = innerMapKeyItem->asTstr();
const cppbor::Simple* simple = innerMapValueItem->asSimple();
const cppbor::Bool* intentToRetainItem =
(simple != nullptr) ? simple->asBool() : nullptr;
if (nameItem == nullptr || intentToRetainItem == nullptr) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_ITEMS_REQUEST_MESSAGE,
"Type mismatch in value in nameSpaces map"));
}
requestedKeys.insert(nameItem->value());
}
requestedNameSpacesAndNames_[requestedNamespace] = requestedKeys;
}
}
deviceNameSpacesMap_ = cppbor::Map();
currentNameSpaceDeviceNameSpacesMap_ = cppbor::Map();
requestCountsRemaining_ = requestCounts;
currentNameSpace_ = "";
itemsRequest_ = itemsRequest;
signingKeyBlob_ = signingKeyBlob;
// calculate the size of DeviceNameSpaces. We need to know it ahead of time.
calcDeviceNameSpacesSize(accessControlProfileMask);
// Count the number of non-empty namespaces
size_t numNamespacesWithValues = 0;
for (size_t n = 0; n < expectedNumEntriesPerNamespace_.size(); n++) {
if (expectedNumEntriesPerNamespace_[n] > 0) {
numNamespacesWithValues += 1;
}
}
// 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 && 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());
}
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 && signingKeyBlob.size() > 0) {
// Don't actually pass the reader ephemeral public key in, the TA will get
// it from the session object.
//
if (!hwProxy_->prepareDeviceAuthentication(sessionTranscript_, {}, signingKeyBlob,
docType_, numNamespacesWithValues,
expectedDeviceNameSpacesSize_)) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED,
"Error starting retrieving entries"));
}
}
}
numStartRetrievalCalls_ += 1;
return ndk::ScopedAStatus::ok();
}
size_t cborNumBytesForLength(size_t length) {
if (length < 24) {
return 0;
} else if (length <= 0xff) {
return 1;
} else if (length <= 0xffff) {
return 2;
} else if (length <= 0xffffffff) {
return 4;
}
return 8;
}
size_t cborNumBytesForTstr(const string& value) {
return 1 + cborNumBytesForLength(value.size()) + value.size();
}
void IdentityCredential::calcDeviceNameSpacesSize(uint32_t accessControlProfileMask) {
/*
* This is how DeviceNameSpaces is defined:
*
* DeviceNameSpaces = {
* * NameSpace => DeviceSignedItems
* }
* DeviceSignedItems = {
* + DataItemName => DataItemValue
* }
*
* Namespace = tstr
* DataItemName = tstr
* DataItemValue = any
*
* This function will calculate its length using knowledge of how CBOR is
* encoded.
*/
size_t ret = 0;
vector<unsigned int> numEntriesPerNamespace;
for (const RequestNamespace& rns : requestNamespaces_) {
vector<RequestDataItem> itemsToInclude;
for (const RequestDataItem& rdi : rns.items) {
// If we have a CBOR request message, skip if item isn't in it
if (itemsRequest_.size() > 0) {
const auto& it = requestedNameSpacesAndNames_.find(rns.namespaceName);
if (it == requestedNameSpacesAndNames_.end()) {
continue;
}
const set<string>& dataItemNames = it->second;
if (dataItemNames.find(rdi.name) == dataItemNames.end()) {
continue;
}
}
// Access is granted if at least one of the profiles grants access.
//
// If an item is configured without any profiles, access is denied.
//
bool authorized = false;
for (auto id : rdi.accessControlProfileIds) {
if (accessControlProfileMask & (1 << id)) {
authorized = true;
break;
}
}
if (!authorized) {
continue;
}
itemsToInclude.push_back(rdi);
}
numEntriesPerNamespace.push_back(itemsToInclude.size());
// If no entries are to be in the namespace, we don't include it in
// the CBOR...
if (itemsToInclude.size() == 0) {
continue;
}
// Key: NameSpace
ret += cborNumBytesForTstr(rns.namespaceName);
// Value: Open the DeviceSignedItems map
ret += 1 + cborNumBytesForLength(itemsToInclude.size());
for (const RequestDataItem& item : itemsToInclude) {
// Key: DataItemName
ret += cborNumBytesForTstr(item.name);
// Value: DataItemValue - entryData.size is the length of serialized CBOR so we use
// that.
ret += item.size;
}
}
// Now that we know the number of namespaces with values, we know how many
// bytes the DeviceNamespaces map in the beginning is going to take up.
ret += 1 + cborNumBytesForLength(numEntriesPerNamespace.size());
expectedDeviceNameSpacesSize_ = ret;
expectedNumEntriesPerNamespace_ = numEntriesPerNamespace;
}
ndk::ScopedAStatus IdentityCredential::startRetrieveEntryValue(
const string& nameSpace, const string& name, int32_t entrySize,
const vector<int32_t>& accessControlProfileIds) {
ndk::ScopedAStatus status = ensureHwProxy();
if (!status.isOk()) {
return status;
}
if (name.empty()) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_DATA, "Name cannot be empty"));
}
if (nameSpace.empty()) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_DATA, "Name space cannot be empty"));
}
if (requestCountsRemaining_.size() == 0) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_DATA,
"No more name spaces left to go through"));
}
bool newNamespace;
if (currentNameSpace_ == "") {
// First call.
currentNameSpace_ = nameSpace;
newNamespace = true;
}
if (nameSpace == currentNameSpace_) {
// Same namespace.
if (requestCountsRemaining_[0] == 0) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_DATA,
"No more entries to be retrieved in current name space"));
}
requestCountsRemaining_[0] -= 1;
} else {
// New namespace.
if (requestCountsRemaining_[0] != 0) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_DATA,
"Moved to new name space but one or more entries need to be retrieved "
"in current name space"));
}
if (currentNameSpaceDeviceNameSpacesMap_.size() > 0) {
deviceNameSpacesMap_.add(currentNameSpace_,
std::move(currentNameSpaceDeviceNameSpacesMap_));
}
currentNameSpaceDeviceNameSpacesMap_ = cppbor::Map();
requestCountsRemaining_.erase(requestCountsRemaining_.begin());
currentNameSpace_ = nameSpace;
newNamespace = true;
}
// It's permissible to have an empty itemsRequest... but if non-empty you can
// only request what was specified in said itemsRequest. Enforce that.
if (itemsRequest_.size() > 0) {
const auto& it = requestedNameSpacesAndNames_.find(nameSpace);
if (it == requestedNameSpacesAndNames_.end()) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_NOT_IN_REQUEST_MESSAGE,
"Name space was not requested in startRetrieval"));
}
const set<string>& dataItemNames = it->second;
if (dataItemNames.find(name) == dataItemNames.end()) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_NOT_IN_REQUEST_MESSAGE,
"Data item name in name space was not requested in startRetrieval"));
}
}
unsigned int newNamespaceNumEntries = 0;
if (newNamespace) {
if (expectedNumEntriesPerNamespace_.size() == 0) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_DATA,
"No more populated name spaces left to go through"));
}
newNamespaceNumEntries = expectedNumEntriesPerNamespace_[0];
expectedNumEntriesPerNamespace_.erase(expectedNumEntriesPerNamespace_.begin());
}
// Access control is enforced in the secure hardware.
//
// ... except for STATUS_NOT_IN_REQUEST_MESSAGE, that's handled above (TODO:
// consolidate).
//
AccessCheckResult res = hwProxy_->startRetrieveEntryValue(
nameSpace, name, newNamespaceNumEntries, entrySize, accessControlProfileIds);
switch (res) {
case AccessCheckResult::kOk:
/* Do nothing. */
break;
case AccessCheckResult::kFailed:
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED,
"Access control check failed (failed)"));
break;
case AccessCheckResult::kNoAccessControlProfiles:
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_NO_ACCESS_CONTROL_PROFILES,
"Access control check failed (no access control profiles)"));
break;
case AccessCheckResult::kUserAuthenticationFailed:
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_USER_AUTHENTICATION_FAILED,
"Access control check failed (user auth)"));
break;
case AccessCheckResult::kReaderAuthenticationFailed:
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_READER_AUTHENTICATION_FAILED,
"Access control check failed (reader auth)"));
break;
}
currentName_ = name;
currentAccessControlProfileIds_ = accessControlProfileIds;
entryRemainingBytes_ = entrySize;
entryValue_.resize(0);
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus IdentityCredential::retrieveEntryValue(const vector<uint8_t>& encryptedContent,
vector<uint8_t>* outContent) {
ndk::ScopedAStatus status = ensureHwProxy();
if (!status.isOk()) {
return status;
}
optional<vector<uint8_t>> content = hwProxy_->retrieveEntryValue(
encryptedContent, currentNameSpace_, currentName_, currentAccessControlProfileIds_);
if (!content) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_DATA, "Error decrypting data"));
}
size_t chunkSize = content.value().size();
if (chunkSize > entryRemainingBytes_) {
LOG(ERROR) << "Retrieved chunk of size " << chunkSize
<< " is bigger than remaining space of size " << entryRemainingBytes_;
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_DATA,
"Retrieved chunk is bigger than remaining space"));
}
entryRemainingBytes_ -= chunkSize;
if (entryRemainingBytes_ > 0) {
if (chunkSize != IdentityCredentialStore::kGcmChunkSize) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_DATA,
"Retrieved non-final chunk of size which isn't kGcmChunkSize"));
}
}
entryValue_.insert(entryValue_.end(), content.value().begin(), content.value().end());
if (entryRemainingBytes_ == 0) {
auto [entryValueItem, _, message] = cppbor::parse(entryValue_);
if (entryValueItem == nullptr) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_DATA,
"Retrieved data which is invalid CBOR"));
}
currentNameSpaceDeviceNameSpacesMap_.add(currentName_, std::move(entryValueItem));
}
*outContent = content.value();
return ndk::ScopedAStatus::ok();
}
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;
}
if (currentNameSpaceDeviceNameSpacesMap_.size() > 0) {
deviceNameSpacesMap_.add(currentNameSpace_,
std::move(currentNameSpaceDeviceNameSpacesMap_));
}
vector<uint8_t> encodedDeviceNameSpaces = deviceNameSpacesMap_.encode();
if (encodedDeviceNameSpaces.size() != expectedDeviceNameSpacesSize_) {
LOG(ERROR) << "encodedDeviceNameSpaces is " << encodedDeviceNameSpaces.size() << " bytes, "
<< "was expecting " << expectedDeviceNameSpacesSize_;
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_INVALID_DATA,
StringPrintf(
"Unexpected CBOR size %zd for encodedDeviceNameSpaces, was expecting %zd",
encodedDeviceNameSpaces.size(), expectedDeviceNameSpacesSize_)
.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
//
// 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(
IIdentityCredentialStore::STATUS_INVALID_DATA,
"Unexpected size for digestToBeMaced"));
}
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_) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED, "Cannot be called in a session"));
}
ndk::ScopedAStatus status = ensureHwProxy();
if (!status.isOk()) {
return status;
}
time_t now = time(NULL);
optional<pair<vector<uint8_t>, vector<uint8_t>>> pair =
hwProxy_->generateSigningKeyPair(docType_, now);
if (!pair) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED, "Error creating signingKey"));
}
*outSigningKeyCertificate = Certificate();
outSigningKeyCertificate->encodedCertificate = pair->first;
*outSigningKeyBlob = pair->second;
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus IdentityCredential::updateCredential(
shared_ptr<IWritableIdentityCredential>* outWritableCredential) {
if (session_) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED, "Cannot be called in a session"));
}
sp<SecureHardwareProvisioningProxy> provisioningHwProxy =
hwProxyFactory_->createProvisioningProxy();
if (!provisioningHwProxy) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED, "Error creating provisioning proxy"));
}
shared_ptr<WritableIdentityCredential> wc =
ndk::SharedRefBase::make<WritableIdentityCredential>(
provisioningHwProxy, docType_, testCredential_, hardwareInformation_);
if (!wc->initializeForUpdate(encryptedCredentialKeys_)) {
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
IIdentityCredentialStore::STATUS_FAILED,
"Error initializing WritableIdentityCredential for update"));
}
*outWritableCredential = wc;
return ndk::ScopedAStatus::ok();
}
} // namespace aidl::android::hardware::identity