| /* |
| * 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(); |
| } |