| /* |
| * 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 "UserAuthTests" |
| |
| #include <aidl/Gtest.h> |
| #include <aidl/Vintf.h> |
| #include <aidl/android/hardware/keymaster/HardwareAuthToken.h> |
| #include <aidl/android/hardware/keymaster/VerificationToken.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 <utility> |
| |
| #include "Util.h" |
| |
| namespace android::hardware::identity { |
| |
| using std::endl; |
| using std::make_pair; |
| using std::map; |
| using std::optional; |
| using std::pair; |
| using std::string; |
| using std::tie; |
| using std::vector; |
| |
| using ::android::sp; |
| using ::android::String16; |
| using ::android::binder::Status; |
| |
| using ::android::hardware::keymaster::HardwareAuthToken; |
| using ::android::hardware::keymaster::VerificationToken; |
| |
| class UserAuthTests : public testing::TestWithParam<string> { |
| public: |
| virtual void SetUp() override { |
| credentialStore_ = android::waitForDeclaredService<IIdentityCredentialStore>( |
| String16(GetParam().c_str())); |
| ASSERT_NE(credentialStore_, nullptr); |
| } |
| |
| void provisionData(); |
| void setupRetrieveData(); |
| pair<HardwareAuthToken, VerificationToken> mintTokens(uint64_t challengeForAuthToken, |
| int64_t ageOfAuthTokenMilliSeconds); |
| void retrieveData(HardwareAuthToken authToken, VerificationToken verificationToken, |
| bool expectSuccess, bool useSessionTranscript); |
| |
| // Set by provisionData |
| SecureAccessControlProfile sacp0_; |
| SecureAccessControlProfile sacp1_; |
| SecureAccessControlProfile sacp2_; |
| |
| vector<uint8_t> encContentUserAuthPerSession_; |
| vector<uint8_t> encContentUserAuthTimeout_; |
| vector<uint8_t> encContentAccessibleByAll_; |
| vector<uint8_t> encContentAccessibleByNone_; |
| |
| vector<uint8_t> credentialData_; |
| |
| // Set by setupRetrieveData(). |
| int64_t authChallenge_; |
| cppbor::Map sessionTranscript_; |
| sp<IIdentityCredential> credential_; |
| |
| // Set by retrieveData() |
| bool canGetUserAuthPerSession_; |
| bool canGetUserAuthTimeout_; |
| bool canGetAccessibleByAll_; |
| bool canGetAccessibleByNone_; |
| |
| sp<IIdentityCredentialStore> credentialStore_; |
| }; |
| |
| void UserAuthTests::provisionData() { |
| string docType = "org.iso.18013-5.2019.mdl"; |
| bool testCredential = true; |
| sp<IWritableIdentityCredential> wc; |
| ASSERT_TRUE(credentialStore_->createCredential(docType, testCredential, &wc).isOk()); |
| |
| vector<uint8_t> attestationApplicationId = {}; |
| vector<uint8_t> attestationChallenge = {1}; |
| vector<Certificate> certChain; |
| ASSERT_TRUE(wc->getAttestationCertificate(attestationApplicationId, attestationChallenge, |
| &certChain) |
| .isOk()); |
| |
| size_t proofOfProvisioningSize = 381; |
| // Not in v1 HAL, may fail |
| wc->setExpectedProofOfProvisioningSize(proofOfProvisioningSize); |
| |
| ASSERT_TRUE(wc->startPersonalization(3 /* numAccessControlProfiles */, |
| {4} /* numDataElementsPerNamespace */) |
| .isOk()); |
| |
| // Access control profile 0: user auth every session (timeout = 0) |
| ASSERT_TRUE(wc->addAccessControlProfile(0, {}, true, 0, 65 /* secureUserId */, &sacp0_).isOk()); |
| |
| // Access control profile 1: user auth, 60 seconds timeout |
| ASSERT_TRUE( |
| wc->addAccessControlProfile(1, {}, true, 60000, 65 /* secureUserId */, &sacp1_).isOk()); |
| |
| // Access control profile 2: open access |
| ASSERT_TRUE(wc->addAccessControlProfile(2, {}, false, 0, 0, &sacp2_).isOk()); |
| |
| // Data Element: "UserAuth Per Session" |
| ASSERT_TRUE(wc->beginAddEntry({0}, "ns", "UserAuth Per Session", 1).isOk()); |
| ASSERT_TRUE(wc->addEntryValue({9}, &encContentUserAuthPerSession_).isOk()); |
| |
| // Data Element: "UserAuth Timeout" |
| ASSERT_TRUE(wc->beginAddEntry({1}, "ns", "UserAuth Timeout", 1).isOk()); |
| ASSERT_TRUE(wc->addEntryValue({9}, &encContentUserAuthTimeout_).isOk()); |
| |
| // Data Element: "Accessible by All" |
| ASSERT_TRUE(wc->beginAddEntry({2}, "ns", "Accessible by All", 1).isOk()); |
| ASSERT_TRUE(wc->addEntryValue({9}, &encContentAccessibleByAll_).isOk()); |
| |
| // Data Element: "Accessible by None" |
| ASSERT_TRUE(wc->beginAddEntry({}, "ns", "Accessible by None", 1).isOk()); |
| ASSERT_TRUE(wc->addEntryValue({9}, &encContentAccessibleByNone_).isOk()); |
| |
| vector<uint8_t> proofOfProvisioningSignature; |
| Status status = wc->finishAddingEntries(&credentialData_, &proofOfProvisioningSignature); |
| EXPECT_TRUE(status.isOk()) << status.exceptionCode() << ": " << status.exceptionMessage(); |
| } |
| |
| // From ReaderAuthTest.cpp - TODO: consolidate with Util.h |
| pair<vector<uint8_t>, vector<uint8_t>> generateReaderKey(); |
| vector<uint8_t> generateReaderCert(const vector<uint8_t>& publicKey, |
| const vector<uint8_t>& signingKey); |
| RequestDataItem buildRequestDataItem(const string& name, size_t size, |
| vector<int32_t> accessControlProfileIds); |
| |
| cppbor::Map calcSessionTranscript(const vector<uint8_t>& ePublicKey) { |
| auto [getXYSuccess, ephX, ephY] = support::ecPublicKeyGetXandY(ePublicKey); |
| 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(); |
| // Let SessionTranscript be a map here (it's an array in EndToEndTest) just |
| // to check that the implementation can deal with either. |
| cppbor::Map sessionTranscript; |
| sessionTranscript.add(42, cppbor::SemanticTag(24, deviceEngagementBytes)); |
| sessionTranscript.add(43, cppbor::SemanticTag(24, eReaderPubBytes)); |
| return sessionTranscript; |
| } |
| |
| void UserAuthTests::setupRetrieveData() { |
| ASSERT_TRUE(credentialStore_ |
| ->getCredential( |
| CipherSuite::CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256, |
| credentialData_, &credential_) |
| .isOk()); |
| |
| optional<vector<uint8_t>> readerEKeyPair = support::createEcKeyPair(); |
| optional<vector<uint8_t>> readerEPublicKey = |
| support::ecKeyPairGetPublicKey(readerEKeyPair.value()); |
| ASSERT_TRUE(credential_->setReaderEphemeralPublicKey(readerEPublicKey.value()).isOk()); |
| |
| vector<uint8_t> eKeyPair; |
| ASSERT_TRUE(credential_->createEphemeralKeyPair(&eKeyPair).isOk()); |
| optional<vector<uint8_t>> ePublicKey = support::ecKeyPairGetPublicKey(eKeyPair); |
| sessionTranscript_ = calcSessionTranscript(ePublicKey.value()); |
| |
| Status status = credential_->createAuthChallenge(&authChallenge_); |
| EXPECT_TRUE(status.isOk()) << status.exceptionCode() << ": " << status.exceptionMessage(); |
| } |
| |
| void UserAuthTests::retrieveData(HardwareAuthToken authToken, VerificationToken verificationToken, |
| bool expectSuccess, bool useSessionTranscript) { |
| canGetUserAuthPerSession_ = false; |
| canGetUserAuthTimeout_ = false; |
| canGetAccessibleByAll_ = false; |
| canGetAccessibleByNone_ = false; |
| |
| vector<uint8_t> itemsRequestBytes; |
| vector<uint8_t> sessionTranscriptBytes; |
| if (useSessionTranscript) { |
| sessionTranscriptBytes = sessionTranscript_.encode(); |
| |
| itemsRequestBytes = |
| cppbor::Map("nameSpaces", |
| cppbor::Map().add("ns", cppbor::Map() |
| .add("UserAuth Per Session", false) |
| .add("UserAuth Timeout", false) |
| .add("Accessible by All", false) |
| .add("Accessible by None", false))) |
| .encode(); |
| vector<uint8_t> dataToSign = cppbor::Array() |
| .add("ReaderAuthentication") |
| .add(sessionTranscript_.clone()) |
| .add(cppbor::SemanticTag(24, itemsRequestBytes)) |
| .encode(); |
| } |
| |
| // Generate the key that will be used to sign AuthenticatedData. |
| vector<uint8_t> signingKeyBlob; |
| Certificate signingKeyCertificate; |
| ASSERT_TRUE( |
| credential_->generateSigningKeyPair(&signingKeyBlob, &signingKeyCertificate).isOk()); |
| |
| RequestNamespace rns; |
| rns.namespaceName = "ns"; |
| rns.items.push_back(buildRequestDataItem("UserAuth Per Session", 1, {0})); |
| rns.items.push_back(buildRequestDataItem("UserAuth Timeout", 1, {1})); |
| rns.items.push_back(buildRequestDataItem("Accessible by All", 1, {2})); |
| rns.items.push_back(buildRequestDataItem("Accessible by None", 1, {})); |
| // OK to fail, not available in v1 HAL |
| credential_->setRequestedNamespaces({rns}).isOk(); |
| |
| // OK to fail, not available in v1 HAL |
| credential_->setVerificationToken(verificationToken); |
| |
| Status status = credential_->startRetrieval({sacp0_, sacp1_, sacp2_}, authToken, |
| itemsRequestBytes, signingKeyBlob, |
| sessionTranscriptBytes, {} /* readerSignature */, |
| {4 /* numDataElementsPerNamespace */}); |
| if (expectSuccess) { |
| ASSERT_TRUE(status.isOk()); |
| } else { |
| ASSERT_FALSE(status.isOk()); |
| return; |
| } |
| |
| vector<uint8_t> decrypted; |
| |
| status = credential_->startRetrieveEntryValue("ns", "UserAuth Per Session", 1, {0}); |
| if (status.isOk()) { |
| canGetUserAuthPerSession_ = true; |
| ASSERT_TRUE( |
| credential_->retrieveEntryValue(encContentUserAuthPerSession_, &decrypted).isOk()); |
| } |
| |
| status = credential_->startRetrieveEntryValue("ns", "UserAuth Timeout", 1, {1}); |
| if (status.isOk()) { |
| canGetUserAuthTimeout_ = true; |
| ASSERT_TRUE(credential_->retrieveEntryValue(encContentUserAuthTimeout_, &decrypted).isOk()); |
| } |
| |
| status = credential_->startRetrieveEntryValue("ns", "Accessible by All", 1, {2}); |
| if (status.isOk()) { |
| canGetAccessibleByAll_ = true; |
| ASSERT_TRUE(credential_->retrieveEntryValue(encContentAccessibleByAll_, &decrypted).isOk()); |
| } |
| |
| status = credential_->startRetrieveEntryValue("ns", "Accessible by None", 1, {}); |
| if (status.isOk()) { |
| canGetAccessibleByNone_ = true; |
| ASSERT_TRUE( |
| credential_->retrieveEntryValue(encContentAccessibleByNone_, &decrypted).isOk()); |
| } |
| |
| vector<uint8_t> mac; |
| vector<uint8_t> deviceNameSpaces; |
| ASSERT_TRUE(credential_->finishRetrieval(&mac, &deviceNameSpaces).isOk()); |
| } |
| |
| pair<HardwareAuthToken, VerificationToken> UserAuthTests::mintTokens( |
| uint64_t challengeForAuthToken, int64_t ageOfAuthTokenMilliSeconds) { |
| HardwareAuthToken authToken; |
| VerificationToken verificationToken; |
| |
| uint64_t epochMilliseconds = 1000ULL * 1000ULL * 1000ULL * 1000ULL; |
| |
| authToken.challenge = challengeForAuthToken; |
| authToken.userId = 65; |
| authToken.authenticatorId = 0; |
| authToken.authenticatorType = ::android::hardware::keymaster::HardwareAuthenticatorType::NONE; |
| authToken.timestamp.milliSeconds = epochMilliseconds - ageOfAuthTokenMilliSeconds; |
| authToken.mac.clear(); |
| verificationToken.challenge = authChallenge_; |
| verificationToken.timestamp.milliSeconds = epochMilliseconds; |
| verificationToken.securityLevel = |
| ::android::hardware::keymaster::SecurityLevel::TRUSTED_ENVIRONMENT; |
| verificationToken.mac.clear(); |
| return make_pair(authToken, verificationToken); |
| } |
| |
| TEST_P(UserAuthTests, GoodChallenge) { |
| provisionData(); |
| setupRetrieveData(); |
| auto [authToken, verificationToken] = mintTokens(authChallenge_, // challengeForAuthToken |
| 0); // ageOfAuthTokenMilliSeconds |
| retrieveData(authToken, verificationToken, true /* expectSuccess */, |
| true /* useSessionTranscript */); |
| EXPECT_TRUE(canGetUserAuthPerSession_); |
| EXPECT_TRUE(canGetUserAuthTimeout_); |
| EXPECT_TRUE(canGetAccessibleByAll_); |
| EXPECT_FALSE(canGetAccessibleByNone_); |
| } |
| |
| TEST_P(UserAuthTests, OtherChallenge) { |
| provisionData(); |
| setupRetrieveData(); |
| uint64_t otherChallenge = authChallenge_ ^ 0x12345678; |
| auto [authToken, verificationToken] = mintTokens(otherChallenge, // challengeForAuthToken |
| 0); // ageOfAuthTokenMilliSeconds |
| retrieveData(authToken, verificationToken, true /* expectSuccess */, |
| true /* useSessionTranscript */); |
| EXPECT_FALSE(canGetUserAuthPerSession_); |
| EXPECT_TRUE(canGetUserAuthTimeout_); |
| EXPECT_TRUE(canGetAccessibleByAll_); |
| EXPECT_FALSE(canGetAccessibleByNone_); |
| } |
| |
| TEST_P(UserAuthTests, NoChallenge) { |
| provisionData(); |
| setupRetrieveData(); |
| auto [authToken, verificationToken] = mintTokens(0, // challengeForAuthToken |
| 0); // ageOfAuthTokenMilliSeconds |
| retrieveData(authToken, verificationToken, true /* expectSuccess */, |
| true /* useSessionTranscript */); |
| EXPECT_FALSE(canGetUserAuthPerSession_); |
| EXPECT_TRUE(canGetUserAuthTimeout_); |
| EXPECT_TRUE(canGetAccessibleByAll_); |
| EXPECT_FALSE(canGetAccessibleByNone_); |
| } |
| |
| TEST_P(UserAuthTests, AuthTokenAgeZero) { |
| provisionData(); |
| setupRetrieveData(); |
| auto [authToken, verificationToken] = mintTokens(0, // challengeForAuthToken |
| 0); // ageOfAuthTokenMilliSeconds |
| retrieveData(authToken, verificationToken, true /* expectSuccess */, |
| true /* useSessionTranscript */); |
| EXPECT_FALSE(canGetUserAuthPerSession_); |
| EXPECT_TRUE(canGetUserAuthTimeout_); |
| EXPECT_TRUE(canGetAccessibleByAll_); |
| EXPECT_FALSE(canGetAccessibleByNone_); |
| } |
| |
| TEST_P(UserAuthTests, AuthTokenFromTheFuture) { |
| provisionData(); |
| setupRetrieveData(); |
| auto [authToken, verificationToken] = mintTokens(0, // challengeForAuthToken |
| -1 * 1000); // ageOfAuthTokenMilliSeconds |
| retrieveData(authToken, verificationToken, true /* expectSuccess */, |
| true /* useSessionTranscript */); |
| EXPECT_FALSE(canGetUserAuthPerSession_); |
| EXPECT_FALSE(canGetUserAuthTimeout_); |
| EXPECT_TRUE(canGetAccessibleByAll_); |
| EXPECT_FALSE(canGetAccessibleByNone_); |
| } |
| |
| TEST_P(UserAuthTests, AuthTokenInsideTimeout) { |
| provisionData(); |
| setupRetrieveData(); |
| auto [authToken, verificationToken] = mintTokens(0, // challengeForAuthToken |
| 30 * 1000); // ageOfAuthTokenMilliSeconds |
| retrieveData(authToken, verificationToken, true /* expectSuccess */, |
| true /* useSessionTranscript */); |
| EXPECT_FALSE(canGetUserAuthPerSession_); |
| EXPECT_TRUE(canGetUserAuthTimeout_); |
| EXPECT_TRUE(canGetAccessibleByAll_); |
| EXPECT_FALSE(canGetAccessibleByNone_); |
| } |
| |
| TEST_P(UserAuthTests, AuthTokenOutsideTimeout) { |
| provisionData(); |
| setupRetrieveData(); |
| auto [authToken, verificationToken] = mintTokens(0, // challengeForAuthToken |
| 61 * 1000); // ageOfAuthTokenMilliSeconds |
| retrieveData(authToken, verificationToken, true /* expectSuccess */, |
| true /* useSessionTranscript */); |
| EXPECT_FALSE(canGetUserAuthPerSession_); |
| EXPECT_FALSE(canGetUserAuthTimeout_); |
| EXPECT_TRUE(canGetAccessibleByAll_); |
| EXPECT_FALSE(canGetAccessibleByNone_); |
| } |
| |
| // The API works even when there's no SessionTranscript / itemsRequest. |
| // Verify that. |
| TEST_P(UserAuthTests, NoSessionTranscript) { |
| provisionData(); |
| setupRetrieveData(); |
| auto [authToken, verificationToken] = mintTokens(0, // challengeForAuthToken |
| 1 * 1000); // ageOfAuthTokenMilliSeconds |
| retrieveData(authToken, verificationToken, true /* expectSuccess */, |
| false /* useSessionTranscript */); |
| EXPECT_FALSE(canGetUserAuthPerSession_); |
| EXPECT_TRUE(canGetUserAuthTimeout_); |
| EXPECT_TRUE(canGetAccessibleByAll_); |
| EXPECT_FALSE(canGetAccessibleByNone_); |
| } |
| |
| // This test verifies that it's possible to do multiple requests as long |
| // as the sessionTranscript doesn't change. |
| // |
| TEST_P(UserAuthTests, MultipleRequestsSameSessionTranscript) { |
| provisionData(); |
| setupRetrieveData(); |
| |
| // First we try with a stale authToken |
| // |
| auto [authToken, verificationToken] = mintTokens(0, // challengeForAuthToken |
| 61 * 1000); // ageOfAuthTokenMilliSeconds |
| retrieveData(authToken, verificationToken, true /* expectSuccess */, |
| true /* useSessionTranscript */); |
| EXPECT_FALSE(canGetUserAuthPerSession_); |
| EXPECT_FALSE(canGetUserAuthTimeout_); |
| EXPECT_TRUE(canGetAccessibleByAll_); |
| EXPECT_FALSE(canGetAccessibleByNone_); |
| |
| // Then we get a new authToken and try again. |
| tie(authToken, verificationToken) = mintTokens(0, // challengeForAuthToken |
| 5 * 1000); // ageOfAuthTokenMilliSeconds |
| retrieveData(authToken, verificationToken, true /* expectSuccess */, |
| true /* useSessionTranscript */); |
| EXPECT_FALSE(canGetUserAuthPerSession_); |
| EXPECT_TRUE(canGetUserAuthTimeout_); |
| EXPECT_TRUE(canGetAccessibleByAll_); |
| EXPECT_FALSE(canGetAccessibleByNone_); |
| } |
| |
| // Like MultipleRequestsSameSessionTranscript but we change the sessionTranscript |
| // between the two calls. This test verifies that change is detected and the |
| // second request fails. |
| // |
| TEST_P(UserAuthTests, MultipleRequestsSessionTranscriptChanges) { |
| provisionData(); |
| setupRetrieveData(); |
| |
| // First we try with a stale authToken |
| // |
| auto [authToken, verificationToken] = mintTokens(0, // challengeForAuthToken |
| 61 * 1000); // ageOfAuthTokenMilliSeconds |
| retrieveData(authToken, verificationToken, true /* expectSuccess */, |
| true /* useSessionTranscript */); |
| EXPECT_FALSE(canGetUserAuthPerSession_); |
| EXPECT_FALSE(canGetUserAuthTimeout_); |
| EXPECT_TRUE(canGetAccessibleByAll_); |
| EXPECT_FALSE(canGetAccessibleByNone_); |
| |
| // Then we get a new authToken and try again. |
| tie(authToken, verificationToken) = mintTokens(0, // challengeForAuthToken |
| 5 * 1000); // ageOfAuthTokenMilliSeconds |
| |
| // Change sessionTranscript... |
| optional<vector<uint8_t>> eKeyPairNew = support::createEcKeyPair(); |
| optional<vector<uint8_t>> ePublicKeyNew = support::ecKeyPairGetPublicKey(eKeyPairNew.value()); |
| sessionTranscript_ = calcSessionTranscript(ePublicKeyNew.value()); |
| |
| // ... and expect failure. |
| retrieveData(authToken, verificationToken, false /* expectSuccess */, |
| true /* useSessionTranscript */); |
| } |
| |
| GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(UserAuthTests); |
| INSTANTIATE_TEST_SUITE_P( |
| Identity, UserAuthTests, |
| testing::ValuesIn(android::getAidlHalInstanceNames(IIdentityCredentialStore::descriptor)), |
| android::PrintInstanceNameToString); |
| |
| } // namespace android::hardware::identity |