f3e0600395
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
836 lines
38 KiB
C++
836 lines
38 KiB
C++
/*
|
|
* Copyright (C) 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 "VtsHalIdentityEndToEndTest"
|
|
|
|
#include <aidl/Gtest.h>
|
|
#include <aidl/Vintf.h>
|
|
#include <android-base/logging.h>
|
|
#include <android/hardware/identity/IIdentityCredentialStore.h>
|
|
#include <android/hardware/identity/support/IdentityCredentialSupport.h>
|
|
#include <binder/IServiceManager.h>
|
|
#include <binder/ProcessState.h>
|
|
#include <cppbor.h>
|
|
#include <cppbor_parse.h>
|
|
#include <gtest/gtest.h>
|
|
#include <future>
|
|
#include <map>
|
|
#include <tuple>
|
|
|
|
#include "Util.h"
|
|
|
|
namespace android::hardware::identity {
|
|
|
|
using std::endl;
|
|
using std::make_tuple;
|
|
using std::map;
|
|
using std::optional;
|
|
using std::string;
|
|
using std::tuple;
|
|
using std::vector;
|
|
|
|
using ::android::sp;
|
|
using ::android::String16;
|
|
using ::android::binder::Status;
|
|
|
|
using ::android::hardware::keymaster::HardwareAuthToken;
|
|
using ::android::hardware::keymaster::VerificationToken;
|
|
|
|
using test_utils::validateAttestationCertificate;
|
|
|
|
class EndToEndTests : public testing::TestWithParam<std::string> {
|
|
public:
|
|
virtual void SetUp() override {
|
|
credentialStore_ = android::waitForDeclaredService<IIdentityCredentialStore>(
|
|
String16(GetParam().c_str()));
|
|
ASSERT_NE(credentialStore_, nullptr);
|
|
halApiVersion_ = credentialStore_->getInterfaceVersion();
|
|
}
|
|
|
|
sp<IIdentityCredentialStore> credentialStore_;
|
|
int halApiVersion_;
|
|
};
|
|
|
|
TEST_P(EndToEndTests, hardwareInformation) {
|
|
HardwareInformation info;
|
|
ASSERT_TRUE(credentialStore_->getHardwareInformation(&info).isOk());
|
|
ASSERT_GT(info.credentialStoreName.size(), 0);
|
|
ASSERT_GT(info.credentialStoreAuthorName.size(), 0);
|
|
ASSERT_GE(info.dataChunkSize, 256);
|
|
}
|
|
|
|
tuple<bool, string, vector<uint8_t>, vector<uint8_t>, vector<uint8_t>>
|
|
extractFromTestCredentialData(const vector<uint8_t>& credentialData) {
|
|
string docType;
|
|
vector<uint8_t> storageKey;
|
|
vector<uint8_t> credentialPrivKey;
|
|
vector<uint8_t> sha256Pop;
|
|
|
|
auto [item, _, message] = cppbor::parse(credentialData);
|
|
if (item == nullptr) {
|
|
return make_tuple(false, docType, storageKey, credentialPrivKey, sha256Pop);
|
|
}
|
|
|
|
const cppbor::Array* arrayItem = item->asArray();
|
|
if (arrayItem == nullptr || arrayItem->size() != 3) {
|
|
return make_tuple(false, docType, storageKey, credentialPrivKey, sha256Pop);
|
|
}
|
|
|
|
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) {
|
|
return make_tuple(false, docType, storageKey, credentialPrivKey, sha256Pop);
|
|
}
|
|
|
|
docType = docTypeItem->value();
|
|
|
|
vector<uint8_t> hardwareBoundKey = support::getTestHardwareBoundKey();
|
|
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) {
|
|
return make_tuple(false, docType, storageKey, credentialPrivKey, sha256Pop);
|
|
}
|
|
|
|
auto [dckItem, dckPos, dckMessage] = cppbor::parse(decryptedCredentialKeys.value());
|
|
if (dckItem == nullptr) {
|
|
return make_tuple(false, docType, storageKey, credentialPrivKey, sha256Pop);
|
|
}
|
|
const cppbor::Array* dckArrayItem = dckItem->asArray();
|
|
if (dckArrayItem == nullptr) {
|
|
return make_tuple(false, docType, storageKey, credentialPrivKey, sha256Pop);
|
|
}
|
|
if (dckArrayItem->size() < 2) {
|
|
return make_tuple(false, docType, storageKey, credentialPrivKey, sha256Pop);
|
|
}
|
|
const cppbor::Bstr* storageKeyItem = (*dckArrayItem)[0]->asBstr();
|
|
const cppbor::Bstr* credentialPrivKeyItem = (*dckArrayItem)[1]->asBstr();
|
|
if (storageKeyItem == nullptr || credentialPrivKeyItem == nullptr) {
|
|
return make_tuple(false, docType, storageKey, credentialPrivKey, sha256Pop);
|
|
}
|
|
storageKey = storageKeyItem->value();
|
|
credentialPrivKey = credentialPrivKeyItem->value();
|
|
if (dckArrayItem->size() == 3) {
|
|
const cppbor::Bstr* sha256PopItem = (*dckArrayItem)[2]->asBstr();
|
|
if (sha256PopItem == nullptr) {
|
|
return make_tuple(false, docType, storageKey, credentialPrivKey, sha256Pop);
|
|
}
|
|
sha256Pop = sha256PopItem->value();
|
|
}
|
|
return make_tuple(true, docType, storageKey, credentialPrivKey, sha256Pop);
|
|
}
|
|
|
|
TEST_P(EndToEndTests, createAndRetrieveCredential) {
|
|
// First, generate a key-pair for the reader since its public key will be
|
|
// part of the request data.
|
|
vector<uint8_t> readerKey;
|
|
optional<vector<uint8_t>> readerCertificate =
|
|
test_utils::generateReaderCertificate("1234", &readerKey);
|
|
ASSERT_TRUE(readerCertificate);
|
|
|
|
// Make the portrait image really big (just shy of 256 KiB) to ensure that
|
|
// the chunking code gets exercised.
|
|
vector<uint8_t> portraitImage;
|
|
test_utils::setImageData(portraitImage);
|
|
|
|
// Access control profiles:
|
|
const vector<test_utils::TestProfile> testProfiles = {// Profile 0 (reader authentication)
|
|
{0, readerCertificate.value(), false, 0},
|
|
// Profile 1 (no authentication)
|
|
{1, {}, false, 0}};
|
|
|
|
// It doesn't matter since no user auth is needed in this particular test,
|
|
// but for good measure, clear out the tokens we pass to the HAL.
|
|
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, 1}},
|
|
{"PersonalData", "Birth date", string("19120623"), vector<int32_t>{0, 1}},
|
|
{"PersonalData", "First name", string("Alan"), vector<int32_t>{0, 1}},
|
|
{"PersonalData", "Home address", string("Maida Vale, London, England"),
|
|
vector<int32_t>{0}},
|
|
{"Image", "Portrait image", portraitImage, vector<int32_t>{0, 1}},
|
|
};
|
|
const vector<int32_t> testEntriesEntryCounts = {static_cast<int32_t>(testEntries.size() - 1),
|
|
1u};
|
|
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;
|
|
|
|
validateAttestationCertificate(attData.attestationCertificate, attData.attestationChallenge,
|
|
attData.attestationApplicationId, true);
|
|
|
|
// 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 = 262861 - 326 + readerCertificate.value().size();
|
|
// 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"
|
|
" 'readerCertificate' : <not printed>,\n"
|
|
" },\n"
|
|
" {\n"
|
|
" 'id' : 1,\n"
|
|
" },\n"
|
|
" ],\n"
|
|
" {\n"
|
|
" 'PersonalData' : [\n"
|
|
" {\n"
|
|
" 'name' : 'Last name',\n"
|
|
" 'value' : 'Turing',\n"
|
|
" 'accessControlProfiles' : [0, 1, ],\n"
|
|
" },\n"
|
|
" {\n"
|
|
" 'name' : 'Birth date',\n"
|
|
" 'value' : '19120623',\n"
|
|
" 'accessControlProfiles' : [0, 1, ],\n"
|
|
" },\n"
|
|
" {\n"
|
|
" 'name' : 'First name',\n"
|
|
" 'value' : 'Alan',\n"
|
|
" 'accessControlProfiles' : [0, 1, ],\n"
|
|
" },\n"
|
|
" {\n"
|
|
" 'name' : 'Home address',\n"
|
|
" 'value' : 'Maida Vale, London, England',\n"
|
|
" 'accessControlProfiles' : [0, ],\n"
|
|
" },\n"
|
|
" ],\n"
|
|
" 'Image' : [\n"
|
|
" {\n"
|
|
" 'name' : 'Portrait image',\n"
|
|
" 'value' : <bstr size=262134 sha1=941e372f654d86c32d88fae9e41b706afbfd02bb>,\n"
|
|
" 'accessControlProfiles' : [0, 1, ],\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());
|
|
|
|
// Starting with API version 3 (feature version 202101) we require SHA-256(ProofOfProvisioning)
|
|
// to be in CredentialKeys (which is stored encrypted in CredentialData). Check
|
|
// that it's there with the expected value.
|
|
if (halApiVersion_ >= 3) {
|
|
ASSERT_EQ(exSha256Pop, support::sha256(proofOfProvisioning.value()));
|
|
}
|
|
|
|
// Now that the credential has been provisioned, read it back and check the
|
|
// correct data is returned.
|
|
sp<IIdentityCredential> credential;
|
|
ASSERT_TRUE(credentialStore_
|
|
->getCredential(
|
|
CipherSuite::CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256,
|
|
credentialData, &credential)
|
|
.isOk());
|
|
ASSERT_NE(credential, nullptr);
|
|
|
|
optional<vector<uint8_t>> readerEphemeralKeyPair = support::createEcKeyPair();
|
|
ASSERT_TRUE(readerEphemeralKeyPair);
|
|
optional<vector<uint8_t>> readerEphemeralPublicKey =
|
|
support::ecKeyPairGetPublicKey(readerEphemeralKeyPair.value());
|
|
ASSERT_TRUE(credential->setReaderEphemeralPublicKey(readerEphemeralPublicKey.value()).isOk());
|
|
|
|
vector<uint8_t> ephemeralKeyPair;
|
|
ASSERT_TRUE(credential->createEphemeralKeyPair(&ephemeralKeyPair).isOk());
|
|
optional<vector<uint8_t>> ephemeralPublicKey = support::ecKeyPairGetPublicKey(ephemeralKeyPair);
|
|
|
|
// Calculate requestData field and sign it with the reader key.
|
|
auto [getXYSuccess, ephX, ephY] = support::ecPublicKeyGetXandY(ephemeralPublicKey.value());
|
|
ASSERT_TRUE(getXYSuccess);
|
|
cppbor::Map deviceEngagement = cppbor::Map().add("ephX", ephX).add("ephY", ephY);
|
|
vector<uint8_t> deviceEngagementBytes = deviceEngagement.encode();
|
|
vector<uint8_t> eReaderPubBytes = cppbor::Tstr("ignored").encode();
|
|
cppbor::Array sessionTranscript = cppbor::Array()
|
|
.add(cppbor::SemanticTag(24, deviceEngagementBytes))
|
|
.add(cppbor::SemanticTag(24, eReaderPubBytes));
|
|
vector<uint8_t> sessionTranscriptEncoded = sessionTranscript.encode();
|
|
|
|
vector<uint8_t> itemsRequestBytes =
|
|
cppbor::Map("nameSpaces",
|
|
cppbor::Map()
|
|
.add("PersonalData", cppbor::Map()
|
|
.add("Last name", false)
|
|
.add("Birth date", false)
|
|
.add("First name", false)
|
|
.add("Home address", true))
|
|
.add("Image", cppbor::Map().add("Portrait image", false)))
|
|
.encode();
|
|
cborPretty = cppbor::prettyPrint(itemsRequestBytes, 32, {"EphemeralPublicKey"});
|
|
EXPECT_EQ(
|
|
"{\n"
|
|
" 'nameSpaces' : {\n"
|
|
" 'PersonalData' : {\n"
|
|
" 'Last name' : false,\n"
|
|
" 'Birth date' : false,\n"
|
|
" 'First name' : false,\n"
|
|
" 'Home address' : true,\n"
|
|
" },\n"
|
|
" 'Image' : {\n"
|
|
" 'Portrait image' : false,\n"
|
|
" },\n"
|
|
" },\n"
|
|
"}",
|
|
cborPretty);
|
|
vector<uint8_t> encodedReaderAuthentication =
|
|
cppbor::Array()
|
|
.add("ReaderAuthentication")
|
|
.add(sessionTranscript.clone())
|
|
.add(cppbor::SemanticTag(24, itemsRequestBytes))
|
|
.encode();
|
|
vector<uint8_t> encodedReaderAuthenticationBytes =
|
|
cppbor::SemanticTag(24, encodedReaderAuthentication).encode();
|
|
optional<vector<uint8_t>> readerSignature =
|
|
support::coseSignEcDsa(readerKey, {}, // content
|
|
encodedReaderAuthenticationBytes, // detached content
|
|
readerCertificate.value());
|
|
ASSERT_TRUE(readerSignature);
|
|
|
|
// 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);
|
|
|
|
// Since we're using a test-credential we know storageKey meaning we can get the
|
|
// private key. Do this, derive the public key from it, and check this matches what
|
|
// is in the certificate...
|
|
const vector<uint8_t> exDocTypeVec(exDocType.begin(), exDocType.end());
|
|
optional<vector<uint8_t>> exSigningPrivKey =
|
|
support::decryptAes128Gcm(exStorageKey, signingKeyBlob, exDocTypeVec);
|
|
ASSERT_TRUE(exSigningPrivKey);
|
|
optional<vector<uint8_t>> exSigningKeyPair =
|
|
support::ecPrivateKeyToKeyPair(exSigningPrivKey.value());
|
|
ASSERT_TRUE(exSigningKeyPair);
|
|
optional<vector<uint8_t>> exSigningPubKey =
|
|
support::ecKeyPairGetPublicKey(exSigningKeyPair.value());
|
|
ASSERT_TRUE(exSigningPubKey);
|
|
ASSERT_EQ(exSigningPubKey.value(), signingPubKey.value());
|
|
|
|
vector<RequestNamespace> requestedNamespaces = test_utils::buildRequestNamespaces(testEntries);
|
|
// OK to fail, not available in v1 HAL
|
|
credential->setRequestedNamespaces(requestedNamespaces);
|
|
// OK to fail, not available in v1 HAL
|
|
credential->setVerificationToken(verificationToken);
|
|
ASSERT_TRUE(credential
|
|
->startRetrieval(secureProfiles.value(), authToken, itemsRequestBytes,
|
|
signingKeyBlob, sessionTranscriptEncoded,
|
|
readerSignature.value(), testEntriesEntryCounts)
|
|
.isOk());
|
|
|
|
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);
|
|
|
|
// TODO: also use |exStorageKey| to decrypt data and check it's the same as whatt
|
|
// the HAL returns...
|
|
}
|
|
|
|
vector<uint8_t> mac;
|
|
vector<uint8_t> ecdsaSignature;
|
|
vector<uint8_t> deviceNameSpacesEncoded;
|
|
// 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"
|
|
" 'PersonalData' : {\n"
|
|
" 'Last name' : 'Turing',\n"
|
|
" 'Birth date' : '19120623',\n"
|
|
" 'First name' : 'Alan',\n"
|
|
" 'Home address' : 'Maida Vale, London, England',\n"
|
|
" },\n"
|
|
" 'Image' : {\n"
|
|
" 'Portrait image' : <bstr size=262134 "
|
|
"sha1=941e372f654d86c32d88fae9e41b706afbfd02bb>,\n"
|
|
" },\n"
|
|
"}",
|
|
cborPretty);
|
|
|
|
string docType = "org.iso.18013-5.2019.mdl";
|
|
optional<vector<uint8_t>> readerEphemeralPrivateKey =
|
|
support::ecKeyPairGetPrivateKey(readerEphemeralKeyPair.value());
|
|
optional<vector<uint8_t>> eMacKey =
|
|
support::calcEMacKey(readerEphemeralPrivateKey.value(), // Private Key
|
|
signingPubKey.value(), // Public Key
|
|
cppbor::SemanticTag(24, sessionTranscript.encode())
|
|
.encode()); // SessionTranscriptBytes
|
|
optional<vector<uint8_t>> calculatedMac =
|
|
support::calcMac(sessionTranscript.encode(), // SessionTranscript
|
|
docType, // DocType
|
|
deviceNameSpacesEncoded, // DeviceNamespaces
|
|
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()));
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
credential->setRequestedNamespaces({}); // OK to fail, not available in v1 HAL
|
|
ASSERT_TRUE(credential
|
|
->startRetrieval(
|
|
secureProfiles.value(), authToken, {}, // itemsRequestBytes
|
|
signingKeyBlob, sessionTranscriptEncoded, {}, // readerSignature,
|
|
testEntriesEntryCounts)
|
|
.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)
|
|
calculatedMac = support::calcMac(sessionTranscript.encode(), // SessionTranscript
|
|
docType, // DocType
|
|
deviceNameSpacesEncoded, // DeviceNamespaces
|
|
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()));
|
|
}
|
|
|
|
// Some mDL apps might send a request but with a single empty
|
|
// namespace. Check that too.
|
|
RequestNamespace emptyRequestNS;
|
|
emptyRequestNS.namespaceName = "PersonalData";
|
|
credential->setRequestedNamespaces({emptyRequestNS}); // OK to fail, not available in v1 HAL
|
|
ASSERT_TRUE(credential
|
|
->startRetrieval(
|
|
secureProfiles.value(), authToken, {}, // itemsRequestBytes
|
|
signingKeyBlob, sessionTranscriptEncoded, {}, // readerSignature,
|
|
testEntriesEntryCounts)
|
|
.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)
|
|
calculatedMac = support::calcMac(sessionTranscript.encode(), // SessionTranscript
|
|
docType, // DocType
|
|
deviceNameSpacesEncoded, // DeviceNamespaces
|
|
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);
|
|
INSTANTIATE_TEST_SUITE_P(
|
|
Identity, EndToEndTests,
|
|
testing::ValuesIn(android::getAidlHalInstanceNames(IIdentityCredentialStore::descriptor)),
|
|
android::PrintInstanceNameToString);
|
|
|
|
} // namespace android::hardware::identity
|
|
|
|
int main(int argc, char** argv) {
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
::android::ProcessState::self()->setThreadPoolMaxThreadCount(1);
|
|
::android::ProcessState::self()->startThreadPool();
|
|
return RUN_ALL_TESTS();
|
|
}
|