mirror of
https://github.com/Evolution-X/hardware_interfaces
synced 2026-02-01 16:50:18 +00:00
Key derivation for session encryption and MACing now involves mixing in SessionTranscriptBytes. Update docs, default implementation, and VTS tests to reflect this. Also, the standard changed such that instead of DeviceAuthentication being MACed or signed, it's instead DeviceAuthenticationBytes which is defined as #6.24(bstr .cbor DeviceAuthentication). The same also for ReaderAuthentication, now ReaderAuthenticationBytes is the CBOR which is signed by the reader. Also update the URL for CDDL since it's now a published RFC. Bug: 159482543 Test: atest VtsHalIdentityTargetTest Test: atest android.security.identity.cts Merged-In: I73fc7eb48ffb71e00a8b54849266ed814295fa39 Change-Id: I4bb5040895cf24a7089b7e36e05eeaf26496381e
892 lines
38 KiB
C++
892 lines
38 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 "Util.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>
|
|
|
|
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();
|
|
|
|
vector<uint8_t> hardwareBoundKey;
|
|
if (testCredential_) {
|
|
hardwareBoundKey = support::getTestHardwareBoundKey();
|
|
} else {
|
|
hardwareBoundKey = getHardwareBoundKey();
|
|
}
|
|
|
|
const vector<uint8_t>& encryptedCredentialKeys = encryptedCredentialKeysItem->value();
|
|
const vector<uint8_t> docTypeVec(docType_.begin(), docType_.end());
|
|
optional<vector<uint8_t>> decryptedCredentialKeys =
|
|
support::decryptAes128Gcm(hardwareBoundKey, encryptedCredentialKeys, docTypeVec);
|
|
if (!decryptedCredentialKeys) {
|
|
LOG(ERROR) << "Error decrypting CredentialKeys";
|
|
return IIdentityCredentialStore::STATUS_INVALID_DATA;
|
|
}
|
|
|
|
auto [dckItem, dckPos, dckMessage] = cppbor::parse(decryptedCredentialKeys.value());
|
|
if (dckItem == nullptr) {
|
|
LOG(ERROR) << "Decrypted CredentialKeys is not valid CBOR: " << dckMessage;
|
|
return IIdentityCredentialStore::STATUS_INVALID_DATA;
|
|
}
|
|
const cppbor::Array* dckArrayItem = dckItem->asArray();
|
|
if (dckArrayItem == nullptr || dckArrayItem->size() != 2) {
|
|
LOG(ERROR) << "Decrypted CredentialKeys is not an array with two elements";
|
|
return IIdentityCredentialStore::STATUS_INVALID_DATA;
|
|
}
|
|
const cppbor::Bstr* storageKeyItem = (*dckArrayItem)[0]->asBstr();
|
|
const cppbor::Bstr* credentialPrivKeyItem = (*dckArrayItem)[1]->asBstr();
|
|
if (storageKeyItem == nullptr || credentialPrivKeyItem == nullptr) {
|
|
LOG(ERROR) << "CredentialKeys unexpected item types";
|
|
return IIdentityCredentialStore::STATUS_INVALID_DATA;
|
|
}
|
|
storageKey_ = storageKeyItem->value();
|
|
credentialPrivKey_ = credentialPrivKeyItem->value();
|
|
|
|
return IIdentityCredentialStore::STATUS_OK;
|
|
}
|
|
|
|
ndk::ScopedAStatus IdentityCredential::deleteCredential(
|
|
vector<int8_t>* outProofOfDeletionSignature) {
|
|
cppbor::Array array = {"ProofOfDeletion", docType_, testCredential_};
|
|
vector<uint8_t> proofOfDeletion = array.encode();
|
|
|
|
optional<vector<uint8_t>> signature = support::coseSignEcDsa(credentialPrivKey_,
|
|
proofOfDeletion, // payload
|
|
{}, // additionalData
|
|
{}); // certificateChain
|
|
if (!signature) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_FAILED, "Error signing data"));
|
|
}
|
|
|
|
*outProofOfDeletionSignature = byteStringToSigned(signature.value());
|
|
return ndk::ScopedAStatus::ok();
|
|
}
|
|
|
|
ndk::ScopedAStatus IdentityCredential::createEphemeralKeyPair(vector<int8_t>* outKeyPair) {
|
|
optional<vector<uint8_t>> kp = support::createEcKeyPair();
|
|
if (!kp) {
|
|
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(kp.value());
|
|
if (!publicKey) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_FAILED,
|
|
"Error getting public part of ephemeral key pair"));
|
|
}
|
|
ephemeralPublicKey_ = publicKey.value();
|
|
|
|
*outKeyPair = byteStringToSigned(kp.value());
|
|
return ndk::ScopedAStatus::ok();
|
|
}
|
|
|
|
ndk::ScopedAStatus IdentityCredential::setReaderEphemeralPublicKey(
|
|
const vector<int8_t>& publicKey) {
|
|
readerPublicKey_ = byteStringToUnsigned(publicKey);
|
|
return ndk::ScopedAStatus::ok();
|
|
}
|
|
|
|
ndk::ScopedAStatus IdentityCredential::createAuthChallenge(int64_t* outChallenge) {
|
|
uint64_t challenge = 0;
|
|
while (challenge == 0) {
|
|
optional<vector<uint8_t>> bytes = support::getRandom(8);
|
|
if (!bytes) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_FAILED,
|
|
"Error getting random data for challenge"));
|
|
}
|
|
|
|
challenge = 0;
|
|
for (size_t n = 0; n < bytes.value().size(); n++) {
|
|
challenge |= ((bytes.value())[n] << (n * 8));
|
|
}
|
|
}
|
|
|
|
*outChallenge = challenge;
|
|
authChallenge_ = challenge;
|
|
return ndk::ScopedAStatus::ok();
|
|
}
|
|
|
|
// TODO: this could be a lot faster if we did all the splitting and pubkey extraction
|
|
// ahead of time.
|
|
bool checkReaderAuthentication(const SecureAccessControlProfile& profile,
|
|
const vector<uint8_t>& readerCertificateChain) {
|
|
optional<vector<uint8_t>> acpPubKey = support::certificateChainGetTopMostKey(
|
|
byteStringToUnsigned(profile.readerCertificate.encodedCertificate));
|
|
if (!acpPubKey) {
|
|
LOG(ERROR) << "Error extracting public key from readerCertificate in profile";
|
|
return false;
|
|
}
|
|
|
|
optional<vector<vector<uint8_t>>> certificatesInChain =
|
|
support::certificateChainSplit(readerCertificateChain);
|
|
if (!certificatesInChain) {
|
|
LOG(ERROR) << "Error splitting readerCertificateChain";
|
|
return false;
|
|
}
|
|
for (const vector<uint8_t>& certInChain : certificatesInChain.value()) {
|
|
optional<vector<uint8_t>> certPubKey = support::certificateChainGetTopMostKey(certInChain);
|
|
if (!certPubKey) {
|
|
LOG(ERROR)
|
|
<< "Error extracting public key from certificate in chain presented by reader";
|
|
return false;
|
|
}
|
|
if (acpPubKey.value() == certPubKey.value()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool checkUserAuthentication(const SecureAccessControlProfile& profile,
|
|
const VerificationToken& verificationToken,
|
|
const HardwareAuthToken& authToken, uint64_t authChallenge) {
|
|
if (profile.secureUserId != authToken.userId) {
|
|
LOG(ERROR) << "secureUserId in profile (" << profile.secureUserId
|
|
<< ") differs from userId in authToken (" << authToken.userId << ")";
|
|
return false;
|
|
}
|
|
|
|
if (verificationToken.timestamp.milliSeconds == 0) {
|
|
LOG(ERROR) << "VerificationToken is not set";
|
|
return false;
|
|
}
|
|
if (authToken.timestamp.milliSeconds == 0) {
|
|
LOG(ERROR) << "AuthToken is not set";
|
|
return false;
|
|
}
|
|
|
|
if (profile.timeoutMillis == 0) {
|
|
if (authToken.challenge == 0) {
|
|
LOG(ERROR) << "No challenge in authToken";
|
|
return false;
|
|
}
|
|
|
|
if (authToken.challenge != int64_t(authChallenge)) {
|
|
LOG(ERROR) << "Challenge in authToken (" << uint64_t(authToken.challenge) << ") "
|
|
<< "doesn't match the challenge we created (" << authChallenge << ")";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Timeout-based user auth follows. The verification token conveys what the
|
|
// time is right now in the environment which generated the auth token. This
|
|
// is what makes it possible to do timeout-based checks.
|
|
//
|
|
const Timestamp now = verificationToken.timestamp;
|
|
if (authToken.timestamp.milliSeconds > now.milliSeconds) {
|
|
LOG(ERROR) << "Timestamp in authToken (" << authToken.timestamp.milliSeconds
|
|
<< ") is in the future (now: " << now.milliSeconds << ")";
|
|
return false;
|
|
}
|
|
if (now.milliSeconds > authToken.timestamp.milliSeconds + profile.timeoutMillis) {
|
|
LOG(ERROR) << "Deadline for authToken (" << authToken.timestamp.milliSeconds << " + "
|
|
<< profile.timeoutMillis << " = "
|
|
<< (authToken.timestamp.milliSeconds + profile.timeoutMillis)
|
|
<< ") is in the past (now: " << now.milliSeconds << ")";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
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<int8_t>& itemsRequestS,
|
|
const vector<int8_t>& signingKeyBlobS, const vector<int8_t>& sessionTranscriptS,
|
|
const vector<int8_t>& readerSignatureS, const vector<int32_t>& requestCounts) {
|
|
auto sessionTranscript = byteStringToUnsigned(sessionTranscriptS);
|
|
auto itemsRequest = byteStringToUnsigned(itemsRequestS);
|
|
auto readerSignature = byteStringToUnsigned(readerSignatureS);
|
|
|
|
if (sessionTranscript.size() > 0) {
|
|
auto [item, _, message] = cppbor::parse(sessionTranscript);
|
|
if (item == nullptr) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_INVALID_DATA,
|
|
"SessionTranscript contains invalid CBOR"));
|
|
}
|
|
sessionTranscriptItem_ = std::move(item);
|
|
}
|
|
if (numStartRetrievalCalls_ > 0) {
|
|
if (sessionTranscript_ != sessionTranscript) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_SESSION_TRANSCRIPT_MISMATCH,
|
|
"Passed-in SessionTranscript doesn't match previously used SessionTranscript"));
|
|
}
|
|
}
|
|
sessionTranscript_ = sessionTranscript;
|
|
|
|
// 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"));
|
|
}
|
|
|
|
if (!support::certificateChainValidate(readerCertificateChain.value())) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_READER_SIGNATURE_CHECK_FAILED,
|
|
"Error validating reader certificate chain"));
|
|
}
|
|
|
|
optional<vector<uint8_t>> readerPublicKey =
|
|
support::certificateChainGetTopMostKey(readerCertificateChain.value());
|
|
if (!readerPublicKey) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_READER_SIGNATURE_CHECK_FAILED,
|
|
"Unable to get public key from reader certificate chain"));
|
|
}
|
|
|
|
const vector<uint8_t>& itemsRequestBytes = itemsRequest;
|
|
vector<uint8_t> encodedReaderAuthentication =
|
|
cppbor::Array()
|
|
.add("ReaderAuthentication")
|
|
.add(sessionTranscriptItem_->clone())
|
|
.add(cppbor::Semantic(24, itemsRequestBytes))
|
|
.encode();
|
|
vector<uint8_t> encodedReaderAuthenticationBytes =
|
|
cppbor::Semantic(24, encodedReaderAuthentication).encode();
|
|
if (!support::coseCheckEcDsaSignature(readerSignature,
|
|
encodedReaderAuthenticationBytes, // detached content
|
|
readerPublicKey.value())) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_READER_SIGNATURE_CHECK_FAILED,
|
|
"readerSignature check failed"));
|
|
}
|
|
}
|
|
|
|
// Here's where we would validate the passed-in |authToken| to assure ourselves
|
|
// that it comes from the e.g. biometric hardware and wasn't made up by an attacker.
|
|
//
|
|
// However this involves calculating the MAC. However this requires access
|
|
// to the key needed to a pre-shared key which we don't have...
|
|
//
|
|
|
|
// 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.
|
|
//
|
|
// We do this by just searching for the X and Y coordinates.
|
|
if (sessionTranscript.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;
|
|
}
|
|
}
|
|
|
|
// Validate all the access control profiles in the requestData.
|
|
bool haveAuthToken = (authToken.timestamp.milliSeconds != int64_t(0));
|
|
for (const auto& profile : accessControlProfiles) {
|
|
if (!secureAccessControlProfileCheckMac(profile, storageKey_)) {
|
|
LOG(ERROR) << "Error checking MAC for profile";
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_INVALID_DATA,
|
|
"Error checking MAC for profile"));
|
|
}
|
|
int accessControlCheck = IIdentityCredentialStore::STATUS_OK;
|
|
if (profile.userAuthenticationRequired) {
|
|
if (!haveAuthToken ||
|
|
!checkUserAuthentication(profile, verificationToken_, authToken, authChallenge_)) {
|
|
accessControlCheck = IIdentityCredentialStore::STATUS_USER_AUTHENTICATION_FAILED;
|
|
}
|
|
} else if (profile.readerCertificate.encodedCertificate.size() > 0) {
|
|
if (!readerCertificateChain ||
|
|
!checkReaderAuthentication(profile, readerCertificateChain.value())) {
|
|
accessControlCheck = IIdentityCredentialStore::STATUS_READER_AUTHENTICATION_FAILED;
|
|
}
|
|
}
|
|
profileIdToAccessCheckResult_[profile.id] = accessControlCheck;
|
|
}
|
|
|
|
deviceNameSpacesMap_ = cppbor::Map();
|
|
currentNameSpaceDeviceNameSpacesMap_ = cppbor::Map();
|
|
|
|
requestCountsRemaining_ = requestCounts;
|
|
currentNameSpace_ = "";
|
|
|
|
itemsRequest_ = itemsRequest;
|
|
signingKeyBlob_ = byteStringToUnsigned(signingKeyBlobS);
|
|
|
|
// Finally, calculate the size of DeviceNameSpaces. We need to know it ahead of time.
|
|
expectedDeviceNameSpacesSize_ = calcDeviceNameSpacesSize();
|
|
|
|
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();
|
|
}
|
|
|
|
size_t IdentityCredential::calcDeviceNameSpacesSize() {
|
|
/*
|
|
* 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;
|
|
size_t numNamespacesWithValues = 0;
|
|
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) {
|
|
auto it = profileIdToAccessCheckResult_.find(id);
|
|
if (it != profileIdToAccessCheckResult_.end()) {
|
|
int accessControlForProfile = it->second;
|
|
if (accessControlForProfile == IIdentityCredentialStore::STATUS_OK) {
|
|
authorized = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!authorized) {
|
|
continue;
|
|
}
|
|
|
|
itemsToInclude.push_back(rdi);
|
|
}
|
|
|
|
// If no entries are to be in the namespace, we don't include it...
|
|
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;
|
|
}
|
|
|
|
numNamespacesWithValues++;
|
|
}
|
|
|
|
// Now that we now the nunber of namespaces with values, we know how many
|
|
// bytes the DeviceNamespaces map in the beginning is going to take up.
|
|
ret += 1 + cborNumBytesForLength(numNamespacesWithValues);
|
|
|
|
return ret;
|
|
}
|
|
|
|
ndk::ScopedAStatus IdentityCredential::startRetrieveEntryValue(
|
|
const string& nameSpace, const string& name, int32_t entrySize,
|
|
const vector<int32_t>& accessControlProfileIds) {
|
|
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"));
|
|
}
|
|
|
|
if (currentNameSpace_ == "") {
|
|
// First call.
|
|
currentNameSpace_ = nameSpace;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// 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"));
|
|
}
|
|
}
|
|
|
|
// Enforce access control.
|
|
//
|
|
// Access is granted if at least one of the profiles grants access.
|
|
//
|
|
// If an item is configured without any profiles, access is denied.
|
|
//
|
|
int accessControl = IIdentityCredentialStore::STATUS_NO_ACCESS_CONTROL_PROFILES;
|
|
for (auto id : accessControlProfileIds) {
|
|
auto search = profileIdToAccessCheckResult_.find(id);
|
|
if (search == profileIdToAccessCheckResult_.end()) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_INVALID_DATA,
|
|
"Requested entry with unvalidated profile id"));
|
|
}
|
|
int accessControlForProfile = search->second;
|
|
if (accessControlForProfile == IIdentityCredentialStore::STATUS_OK) {
|
|
accessControl = IIdentityCredentialStore::STATUS_OK;
|
|
break;
|
|
}
|
|
accessControl = accessControlForProfile;
|
|
}
|
|
if (accessControl != IIdentityCredentialStore::STATUS_OK) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
int(accessControl), "Access control check failed"));
|
|
}
|
|
|
|
entryAdditionalData_ = entryCreateAdditionalData(nameSpace, name, accessControlProfileIds);
|
|
|
|
currentName_ = name;
|
|
entryRemainingBytes_ = entrySize;
|
|
entryValue_.resize(0);
|
|
|
|
return ndk::ScopedAStatus::ok();
|
|
}
|
|
|
|
ndk::ScopedAStatus IdentityCredential::retrieveEntryValue(const vector<int8_t>& encryptedContentS,
|
|
vector<int8_t>* outContent) {
|
|
auto encryptedContent = byteStringToUnsigned(encryptedContentS);
|
|
optional<vector<uint8_t>> content =
|
|
support::decryptAes128Gcm(storageKey_, encryptedContent, entryAdditionalData_);
|
|
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 = byteStringToSigned(content.value());
|
|
return ndk::ScopedAStatus::ok();
|
|
}
|
|
|
|
ndk::ScopedAStatus IdentityCredential::finishRetrieval(vector<int8_t>* outMac,
|
|
vector<int8_t>* outDeviceNameSpaces) {
|
|
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()));
|
|
}
|
|
|
|
// If there's no signing key or no sessionTranscript or no reader ephemeral
|
|
// public key, we return the empty MAC.
|
|
optional<vector<uint8_t>> mac;
|
|
if (signingKeyBlob_.size() > 0 && sessionTranscript_.size() > 0 &&
|
|
readerPublicKey_.size() > 0) {
|
|
cppbor::Array array;
|
|
array.add("DeviceAuthentication");
|
|
array.add(sessionTranscriptItem_->clone());
|
|
array.add(docType_);
|
|
array.add(cppbor::Semantic(24, encodedDeviceNameSpaces));
|
|
vector<uint8_t> deviceAuthenticationBytes = cppbor::Semantic(24, array.encode()).encode();
|
|
|
|
vector<uint8_t> docTypeAsBlob(docType_.begin(), docType_.end());
|
|
optional<vector<uint8_t>> signingKey =
|
|
support::decryptAes128Gcm(storageKey_, signingKeyBlob_, docTypeAsBlob);
|
|
if (!signingKey) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_INVALID_DATA,
|
|
"Error decrypting signingKeyBlob"));
|
|
}
|
|
|
|
optional<vector<uint8_t>> sharedSecret =
|
|
support::ecdh(readerPublicKey_, signingKey.value());
|
|
if (!sharedSecret) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_FAILED, "Error doing ECDH"));
|
|
}
|
|
|
|
// Mix-in SessionTranscriptBytes
|
|
vector<uint8_t> sessionTranscriptBytes = cppbor::Semantic(24, sessionTranscript_).encode();
|
|
vector<uint8_t> sharedSecretWithSessionTranscriptBytes = sharedSecret.value();
|
|
std::copy(sessionTranscriptBytes.begin(), sessionTranscriptBytes.end(),
|
|
std::back_inserter(sharedSecretWithSessionTranscriptBytes));
|
|
|
|
vector<uint8_t> salt = {0x00};
|
|
vector<uint8_t> info = {};
|
|
optional<vector<uint8_t>> derivedKey =
|
|
support::hkdf(sharedSecretWithSessionTranscriptBytes, salt, info, 32);
|
|
if (!derivedKey) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_FAILED,
|
|
"Error deriving key from shared secret"));
|
|
}
|
|
|
|
mac = support::coseMac0(derivedKey.value(), {}, // payload
|
|
deviceAuthenticationBytes); // detached content
|
|
if (!mac) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_FAILED, "Error MACing data"));
|
|
}
|
|
}
|
|
|
|
*outMac = byteStringToSigned(mac.value_or(vector<uint8_t>({})));
|
|
*outDeviceNameSpaces = byteStringToSigned(encodedDeviceNameSpaces);
|
|
return ndk::ScopedAStatus::ok();
|
|
}
|
|
|
|
ndk::ScopedAStatus IdentityCredential::generateSigningKeyPair(
|
|
vector<int8_t>* outSigningKeyBlob, Certificate* outSigningKeyCertificate) {
|
|
string serialDecimal = "0"; // TODO: set serial to something unique
|
|
string issuer = "Android Open Source Project";
|
|
string subject = "Android IdentityCredential Reference Implementation";
|
|
time_t validityNotBefore = time(nullptr);
|
|
time_t validityNotAfter = validityNotBefore + 365 * 24 * 3600;
|
|
|
|
optional<vector<uint8_t>> signingKeyPKCS8 = support::createEcKeyPair();
|
|
if (!signingKeyPKCS8) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_FAILED, "Error creating signingKey"));
|
|
}
|
|
|
|
optional<vector<uint8_t>> signingPublicKey =
|
|
support::ecKeyPairGetPublicKey(signingKeyPKCS8.value());
|
|
if (!signingPublicKey) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_FAILED,
|
|
"Error getting public part of signingKey"));
|
|
}
|
|
|
|
optional<vector<uint8_t>> signingKey = support::ecKeyPairGetPrivateKey(signingKeyPKCS8.value());
|
|
if (!signingKey) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_FAILED,
|
|
"Error getting private part of signingKey"));
|
|
}
|
|
|
|
optional<vector<uint8_t>> certificate = support::ecPublicKeyGenerateCertificate(
|
|
signingPublicKey.value(), credentialPrivKey_, serialDecimal, issuer, subject,
|
|
validityNotBefore, validityNotAfter);
|
|
if (!certificate) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_FAILED, "Error creating signingKey"));
|
|
}
|
|
|
|
optional<vector<uint8_t>> nonce = support::getRandom(12);
|
|
if (!nonce) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_FAILED, "Error getting random"));
|
|
}
|
|
vector<uint8_t> docTypeAsBlob(docType_.begin(), docType_.end());
|
|
optional<vector<uint8_t>> encryptedSigningKey = support::encryptAes128Gcm(
|
|
storageKey_, nonce.value(), signingKey.value(), docTypeAsBlob);
|
|
if (!encryptedSigningKey) {
|
|
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
|
IIdentityCredentialStore::STATUS_FAILED, "Error encrypting signingKey"));
|
|
}
|
|
*outSigningKeyBlob = byteStringToSigned(encryptedSigningKey.value());
|
|
*outSigningKeyCertificate = Certificate();
|
|
outSigningKeyCertificate->encodedCertificate = byteStringToSigned(certificate.value());
|
|
return ndk::ScopedAStatus::ok();
|
|
}
|
|
|
|
} // namespace aidl::android::hardware::identity
|