mirror of
https://github.com/Evolution-X/hardware_interfaces
synced 2026-02-01 16:50:18 +00:00
Merge "Update Identity Credential VTS tests." am: 17ec80b638
Original change: https://android-review.googlesource.com/c/platform/hardware/interfaces/+/1322407 Change-Id: I6ff5d40629c6754897f8eb3f11010966dfb93190
This commit is contained in:
@@ -160,17 +160,10 @@ interface IIdentityCredential {
|
||||
* ItemsRequestBytes
|
||||
* ]
|
||||
*
|
||||
* SessionTranscript = [
|
||||
* DeviceEngagementBytes,
|
||||
* EReaderKeyBytes
|
||||
* ]
|
||||
* SessionTranscript = any
|
||||
*
|
||||
* DeviceEngagementBytes = #6.24(bstr .cbor DeviceEngagement)
|
||||
* EReaderKeyBytes = #6.24(bstr .cbor EReaderKey.Pub)
|
||||
* ItemsRequestBytes = #6.24(bstr .cbor ItemsRequest)
|
||||
*
|
||||
* EReaderKey.Pub = COSE_Key ; Ephemeral public key provided by reader
|
||||
*
|
||||
* The public key corresponding to the key used to made signature, can be found in the
|
||||
* 'x5chain' unprotected header element of the COSE_Sign1 structure (as as described
|
||||
* in 'draft-ietf-cose-x509-04'). There will be at least one certificate in said element
|
||||
@@ -184,8 +177,12 @@ interface IIdentityCredential {
|
||||
*
|
||||
* If the SessionTranscript CBOR is not empty, the X and Y coordinates of the public
|
||||
* part of the key-pair previously generated by createEphemeralKeyPair() must appear
|
||||
* somewhere in the bytes of DeviceEngagement structure. Both X and Y should be in
|
||||
* uncompressed form. If this is not satisfied, the call fails with
|
||||
* somewhere in the bytes of the CBOR. Each of these coordinates must appear encoded
|
||||
* with the most significant bits first and use the exact amount of bits indicated by
|
||||
* the key size of the ephemeral keys. For example, if the ephemeral key is using the
|
||||
* P-256 curve then the 32 bytes for the X coordinate encoded with the most significant
|
||||
* bits first must appear somewhere in the CBOR and ditto for the 32 bytes for the Y
|
||||
* coordinate. If this is not satisfied, the call fails with
|
||||
* STATUS_EPHEMERAL_PUBLIC_KEY_NOT_FOUND.
|
||||
*
|
||||
* @param accessControlProfiles
|
||||
@@ -298,13 +295,8 @@ interface IIdentityCredential {
|
||||
*
|
||||
* DocType = tstr
|
||||
*
|
||||
* SessionTranscript = [
|
||||
* DeviceEngagementBytes,
|
||||
* EReaderKeyBytes
|
||||
* ]
|
||||
* SessionTranscript = any
|
||||
*
|
||||
* DeviceEngagementBytes = #6.24(bstr .cbor DeviceEngagement)
|
||||
* EReaderKeyBytes = #6.24(bstr .cbor EReaderKey.Pub)
|
||||
* DeviceNameSpacesBytes = #6.24(bstr .cbor DeviceNameSpaces)
|
||||
*
|
||||
* where
|
||||
@@ -356,8 +348,9 @@ interface IIdentityCredential {
|
||||
*
|
||||
* - subjectPublicKeyInfo: must contain attested public key.
|
||||
*
|
||||
* @param out signingKeyBlob contains an encrypted copy of the newly-generated private
|
||||
* signing key.
|
||||
* @param out signingKeyBlob contains an AES-GCM-ENC(storageKey, R, signingKey, docType)
|
||||
* where signingKey is an EC private key in uncompressed form. That is, the returned
|
||||
* blob is an encrypted copy of the newly-generated private signing key.
|
||||
*
|
||||
* @return an X.509 certificate for the new signing key, signed by the credential key.
|
||||
*/
|
||||
|
||||
@@ -29,9 +29,27 @@ interface IWritableIdentityCredential {
|
||||
* Gets the certificate chain for credentialKey which can be used to prove the hardware
|
||||
* characteristics to an issuing authority. Must not be called more than once.
|
||||
*
|
||||
* The following non-optional fields for the X.509 certificate shall be set as follows:
|
||||
*
|
||||
* - version: INTEGER 2 (means v3 certificate).
|
||||
*
|
||||
* - serialNumber: INTEGER 1 (fixed value: same on all certs).
|
||||
*
|
||||
* - signature: must be set to ECDSA.
|
||||
*
|
||||
* - subject: CN shall be set to "Android Identity Credential Key".
|
||||
*
|
||||
* - issuer: shall be set to "credentialStoreName (credentialStoreAuthorName)" using the
|
||||
* values returned in HardwareInformation.
|
||||
*
|
||||
* - validity: should be from current time and expire at the same time as the
|
||||
* attestation batch certificate used.
|
||||
*
|
||||
* - subjectPublicKeyInfo: must contain attested public key.
|
||||
*
|
||||
* The certificate chain must be generated using Keymaster Attestation
|
||||
* (see https://source.android.com/security/keystore/attestation) with the
|
||||
* following additional requirements:
|
||||
* following additional requirements on the data in the attestation extension:
|
||||
*
|
||||
* - The attestationVersion field in the attestation extension must be at least 3.
|
||||
*
|
||||
@@ -109,7 +127,8 @@ interface IWritableIdentityCredential {
|
||||
* in Tag::ATTESTATION_APPLICATION_ID. This schema is described in
|
||||
* https://developer.android.com/training/articles/security-key-attestation#certificate_schema_attestationid
|
||||
*
|
||||
* @param attestationChallenge a challenge set by the issuer to ensure freshness.
|
||||
* @param attestationChallenge a challenge set by the issuer to ensure freshness. If
|
||||
* this is empty, the call fails with STATUS_INVALID_DATA.
|
||||
*
|
||||
* @return the X.509 certificate chain for the credentialKey
|
||||
*/
|
||||
@@ -250,6 +269,7 @@ interface IWritableIdentityCredential {
|
||||
* CredentialKeys = [
|
||||
* bstr, ; storageKey, a 128-bit AES key
|
||||
* bstr ; credentialPrivKey, the private key for credentialKey
|
||||
* ; in uncompressed form
|
||||
* ]
|
||||
*
|
||||
* @param out proofOfProvisioningSignature proves to the IA that the credential was imported
|
||||
|
||||
@@ -164,6 +164,7 @@ ndk::ScopedAStatus IdentityCredential::createAuthChallenge(int64_t* outChallenge
|
||||
}
|
||||
|
||||
*outChallenge = challenge;
|
||||
authChallenge_ = challenge;
|
||||
return ndk::ScopedAStatus::ok();
|
||||
}
|
||||
|
||||
@@ -223,7 +224,8 @@ bool checkUserAuthentication(const SecureAccessControlProfile& profile,
|
||||
}
|
||||
|
||||
if (authToken.challenge != int64_t(authChallenge)) {
|
||||
LOG(ERROR) << "Challenge in authToken doesn't match the challenge we created";
|
||||
LOG(ERROR) << "Challenge in authToken (" << uint64_t(authToken.challenge) << ") "
|
||||
<< "doesn't match the challenge we created (" << authChallenge << ")";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -337,28 +339,6 @@ ndk::ScopedAStatus IdentityCredential::startRetrieval(
|
||||
//
|
||||
// We do this by just searching for the X and Y coordinates.
|
||||
if (sessionTranscript.size() > 0) {
|
||||
const cppbor::Array* array = sessionTranscriptItem_->asArray();
|
||||
if (array == nullptr || array->size() != 2) {
|
||||
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
||||
IIdentityCredentialStore::STATUS_EPHEMERAL_PUBLIC_KEY_NOT_FOUND,
|
||||
"SessionTranscript is not an array with two items"));
|
||||
}
|
||||
const cppbor::Semantic* taggedEncodedDE = (*array)[0]->asSemantic();
|
||||
if (taggedEncodedDE == nullptr || taggedEncodedDE->value() != 24) {
|
||||
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
||||
IIdentityCredentialStore::STATUS_EPHEMERAL_PUBLIC_KEY_NOT_FOUND,
|
||||
"First item in SessionTranscript array is not a "
|
||||
"semantic with value 24"));
|
||||
}
|
||||
const cppbor::Bstr* encodedDE = (taggedEncodedDE->child())->asBstr();
|
||||
if (encodedDE == nullptr) {
|
||||
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
||||
IIdentityCredentialStore::STATUS_EPHEMERAL_PUBLIC_KEY_NOT_FOUND,
|
||||
"Child of semantic in first item in SessionTranscript "
|
||||
"array is not a bstr"));
|
||||
}
|
||||
const vector<uint8_t>& bytesDE = encodedDE->value();
|
||||
|
||||
auto [getXYSuccess, ePubX, ePubY] = support::ecPublicKeyGetXandY(ephemeralPublicKey_);
|
||||
if (!getXYSuccess) {
|
||||
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
||||
@@ -366,8 +346,10 @@ ndk::ScopedAStatus IdentityCredential::startRetrieval(
|
||||
"Error extracting X and Y from ePub"));
|
||||
}
|
||||
if (sessionTranscript.size() > 0 &&
|
||||
!(memmem(bytesDE.data(), bytesDE.size(), ePubX.data(), ePubX.size()) != nullptr &&
|
||||
memmem(bytesDE.data(), bytesDE.size(), ePubY.data(), ePubY.size()) != nullptr)) {
|
||||
!(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 "
|
||||
@@ -474,9 +456,10 @@ ndk::ScopedAStatus IdentityCredential::startRetrieval(
|
||||
}
|
||||
|
||||
// Validate all the access control profiles in the requestData.
|
||||
bool haveAuthToken = (authToken.mac.size() > 0);
|
||||
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"));
|
||||
|
||||
@@ -65,6 +65,10 @@ ndk::ScopedAStatus WritableIdentityCredential::getAttestationCertificate(
|
||||
IIdentityCredentialStore::STATUS_FAILED,
|
||||
"Error attestation certificate previously generated"));
|
||||
}
|
||||
if (attestationChallenge.empty()) {
|
||||
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
||||
IIdentityCredentialStore::STATUS_INVALID_DATA, "Challenge can not be empty"));
|
||||
}
|
||||
|
||||
vector<uint8_t> challenge(attestationChallenge.begin(), attestationChallenge.end());
|
||||
vector<uint8_t> appId(attestationApplicationId.begin(), attestationApplicationId.end());
|
||||
@@ -165,6 +169,13 @@ ndk::ScopedAStatus WritableIdentityCredential::addAccessControlProfile(
|
||||
"userAuthenticationRequired is false but timeout is non-zero"));
|
||||
}
|
||||
|
||||
// If |userAuthenticationRequired| is true, then |secureUserId| must be non-zero.
|
||||
if (userAuthenticationRequired && secureUserId == 0) {
|
||||
return ndk::ScopedAStatus(AStatus_fromServiceSpecificErrorWithMessage(
|
||||
IIdentityCredentialStore::STATUS_INVALID_DATA,
|
||||
"userAuthenticationRequired is true but secureUserId is zero"));
|
||||
}
|
||||
|
||||
profile.id = id;
|
||||
profile.readerCertificate = readerCertificate;
|
||||
profile.userAuthenticationRequired = userAuthenticationRequired;
|
||||
|
||||
@@ -10,6 +10,8 @@ cc_test {
|
||||
"VtsIdentityTestUtils.cpp",
|
||||
"VtsAttestationTests.cpp",
|
||||
"VtsAttestationParserSupport.cpp",
|
||||
"UserAuthTests.cpp",
|
||||
"ReaderAuthTests.cpp",
|
||||
],
|
||||
shared_libs: [
|
||||
"android.hardware.keymaster@4.0",
|
||||
@@ -18,6 +20,7 @@ cc_test {
|
||||
"libkeymaster_portable",
|
||||
"libsoft_attestation_cert",
|
||||
"libpuresoftkeymasterdevice",
|
||||
"android.hardware.keymaster-ndk_platform",
|
||||
],
|
||||
static_libs: [
|
||||
"libcppbor",
|
||||
|
||||
596
identity/aidl/vts/ReaderAuthTests.cpp
Normal file
596
identity/aidl/vts/ReaderAuthTests.cpp
Normal file
@@ -0,0 +1,596 @@
|
||||
/*
|
||||
* 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 "ReaderAuthTests"
|
||||
|
||||
#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 "VtsIdentityTestUtils.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 ReaderAuthTests : public testing::TestWithParam<string> {
|
||||
public:
|
||||
virtual void SetUp() override {
|
||||
credentialStore_ = android::waitForDeclaredService<IIdentityCredentialStore>(
|
||||
String16(GetParam().c_str()));
|
||||
ASSERT_NE(credentialStore_, nullptr);
|
||||
}
|
||||
|
||||
void provisionData();
|
||||
void retrieveData(const vector<uint8_t>& readerPrivateKey,
|
||||
const vector<vector<uint8_t>>& readerCertChain, bool expectSuccess,
|
||||
bool leaveOutAccessibleToAllFromRequestMessage);
|
||||
|
||||
// Set by provisionData
|
||||
vector<uint8_t> readerPublicKey_;
|
||||
vector<uint8_t> readerPrivateKey_;
|
||||
vector<uint8_t> intermediateAPublicKey_;
|
||||
vector<uint8_t> intermediateAPrivateKey_;
|
||||
vector<uint8_t> intermediateBPublicKey_;
|
||||
vector<uint8_t> intermediateBPrivateKey_;
|
||||
vector<uint8_t> intermediateCPublicKey_;
|
||||
vector<uint8_t> intermediateCPrivateKey_;
|
||||
|
||||
vector<uint8_t> cert_A_SelfSigned_;
|
||||
|
||||
vector<uint8_t> cert_B_SelfSigned_;
|
||||
|
||||
vector<uint8_t> cert_B_SignedBy_C_;
|
||||
|
||||
vector<uint8_t> cert_C_SelfSigned_;
|
||||
|
||||
vector<uint8_t> cert_reader_SelfSigned_;
|
||||
vector<uint8_t> cert_reader_SignedBy_A_;
|
||||
vector<uint8_t> cert_reader_SignedBy_B_;
|
||||
|
||||
SecureAccessControlProfile sacp0_;
|
||||
SecureAccessControlProfile sacp1_;
|
||||
SecureAccessControlProfile sacp2_;
|
||||
SecureAccessControlProfile sacp3_;
|
||||
|
||||
vector<uint8_t> encContentAccessibleByA_;
|
||||
vector<uint8_t> encContentAccessibleByAorB_;
|
||||
vector<uint8_t> encContentAccessibleByB_;
|
||||
vector<uint8_t> encContentAccessibleByC_;
|
||||
vector<uint8_t> encContentAccessibleByAll_;
|
||||
vector<uint8_t> encContentAccessibleByNone_;
|
||||
|
||||
vector<uint8_t> credentialData_;
|
||||
|
||||
// Set by retrieveData()
|
||||
bool canGetAccessibleByA_;
|
||||
bool canGetAccessibleByAorB_;
|
||||
bool canGetAccessibleByB_;
|
||||
bool canGetAccessibleByC_;
|
||||
bool canGetAccessibleByAll_;
|
||||
bool canGetAccessibleByNone_;
|
||||
|
||||
sp<IIdentityCredentialStore> credentialStore_;
|
||||
};
|
||||
|
||||
pair<vector<uint8_t>, vector<uint8_t>> generateReaderKey() {
|
||||
optional<vector<uint8_t>> keyPKCS8 = support::createEcKeyPair();
|
||||
optional<vector<uint8_t>> publicKey = support::ecKeyPairGetPublicKey(keyPKCS8.value());
|
||||
optional<vector<uint8_t>> privateKey = support::ecKeyPairGetPrivateKey(keyPKCS8.value());
|
||||
return make_pair(publicKey.value(), privateKey.value());
|
||||
}
|
||||
|
||||
vector<uint8_t> generateReaderCert(const vector<uint8_t>& publicKey,
|
||||
const vector<uint8_t>& signingKey) {
|
||||
time_t validityNotBefore = 0;
|
||||
time_t validityNotAfter = 0xffffffff;
|
||||
optional<vector<uint8_t>> cert =
|
||||
support::ecPublicKeyGenerateCertificate(publicKey, signingKey, "24601", "Issuer",
|
||||
"Subject", validityNotBefore, validityNotAfter);
|
||||
return cert.value();
|
||||
}
|
||||
|
||||
void ReaderAuthTests::provisionData() {
|
||||
// Keys and certificates for intermediates.
|
||||
tie(intermediateAPublicKey_, intermediateAPrivateKey_) = generateReaderKey();
|
||||
tie(intermediateBPublicKey_, intermediateBPrivateKey_) = generateReaderKey();
|
||||
tie(intermediateCPublicKey_, intermediateCPrivateKey_) = generateReaderKey();
|
||||
|
||||
cert_A_SelfSigned_ = generateReaderCert(intermediateAPublicKey_, intermediateAPrivateKey_);
|
||||
|
||||
cert_B_SelfSigned_ = generateReaderCert(intermediateBPublicKey_, intermediateBPrivateKey_);
|
||||
|
||||
cert_B_SignedBy_C_ = generateReaderCert(intermediateBPublicKey_, intermediateCPrivateKey_);
|
||||
|
||||
cert_C_SelfSigned_ = generateReaderCert(intermediateCPublicKey_, intermediateCPrivateKey_);
|
||||
|
||||
// Key and self-signed certificate reader
|
||||
tie(readerPublicKey_, readerPrivateKey_) = generateReaderKey();
|
||||
cert_reader_SelfSigned_ = generateReaderCert(readerPublicKey_, readerPrivateKey_);
|
||||
|
||||
// Certificate for reader signed by intermediates
|
||||
cert_reader_SignedBy_A_ = generateReaderCert(readerPublicKey_, intermediateAPrivateKey_);
|
||||
cert_reader_SignedBy_B_ = generateReaderCert(readerPublicKey_, intermediateBPrivateKey_);
|
||||
|
||||
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 =
|
||||
465 + cert_A_SelfSigned_.size() + cert_B_SelfSigned_.size() + cert_C_SelfSigned_.size();
|
||||
ASSERT_TRUE(wc->setExpectedProofOfProvisioningSize(proofOfProvisioningSize).isOk());
|
||||
|
||||
// Not in v1 HAL, may fail
|
||||
wc->startPersonalization(4 /* numAccessControlProfiles */,
|
||||
{6} /* numDataElementsPerNamespace */);
|
||||
|
||||
// AIDL expects certificates wrapped in the Certificate type...
|
||||
Certificate cert_A;
|
||||
Certificate cert_B;
|
||||
Certificate cert_C;
|
||||
cert_A.encodedCertificate = cert_A_SelfSigned_;
|
||||
cert_B.encodedCertificate = cert_B_SelfSigned_;
|
||||
cert_C.encodedCertificate = cert_C_SelfSigned_;
|
||||
|
||||
// Access control profile 0: accessible by A
|
||||
ASSERT_TRUE(wc->addAccessControlProfile(0, cert_A, false, 0, 0, &sacp0_).isOk());
|
||||
|
||||
// Access control profile 1: accessible by B
|
||||
ASSERT_TRUE(wc->addAccessControlProfile(1, cert_B, false, 0, 0, &sacp1_).isOk());
|
||||
|
||||
// Access control profile 2: accessible by C
|
||||
ASSERT_TRUE(wc->addAccessControlProfile(2, cert_C, false, 0, 0, &sacp2_).isOk());
|
||||
|
||||
// Access control profile 3: open access
|
||||
ASSERT_TRUE(wc->addAccessControlProfile(3, {}, false, 0, 0, &sacp3_).isOk());
|
||||
|
||||
// Data Element: "Accessible by A"
|
||||
ASSERT_TRUE(wc->beginAddEntry({0}, "ns", "Accessible by A", 1).isOk());
|
||||
ASSERT_TRUE(wc->addEntryValue({9}, &encContentAccessibleByA_).isOk());
|
||||
|
||||
// Data Element: "Accessible by A or B"
|
||||
ASSERT_TRUE(wc->beginAddEntry({0, 1}, "ns", "Accessible by A or B", 1).isOk());
|
||||
ASSERT_TRUE(wc->addEntryValue({9}, &encContentAccessibleByAorB_).isOk());
|
||||
|
||||
// Data Element: "Accessible by B"
|
||||
ASSERT_TRUE(wc->beginAddEntry({1}, "ns", "Accessible by B", 1).isOk());
|
||||
ASSERT_TRUE(wc->addEntryValue({9}, &encContentAccessibleByB_).isOk());
|
||||
|
||||
// Data Element: "Accessible by C"
|
||||
ASSERT_TRUE(wc->beginAddEntry({2}, "ns", "Accessible by C", 1).isOk());
|
||||
ASSERT_TRUE(wc->addEntryValue({9}, &encContentAccessibleByC_).isOk());
|
||||
|
||||
// Data Element: "Accessible by All"
|
||||
ASSERT_TRUE(wc->beginAddEntry({3}, "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;
|
||||
ASSERT_TRUE(wc->finishAddingEntries(&credentialData_, &proofOfProvisioningSignature).isOk());
|
||||
}
|
||||
|
||||
RequestDataItem buildRequestDataItem(const string& name, size_t size,
|
||||
vector<int32_t> accessControlProfileIds) {
|
||||
RequestDataItem item;
|
||||
item.name = name;
|
||||
item.size = size;
|
||||
item.accessControlProfileIds = accessControlProfileIds;
|
||||
return item;
|
||||
}
|
||||
|
||||
void ReaderAuthTests::retrieveData(const vector<uint8_t>& readerPrivateKey,
|
||||
const vector<vector<uint8_t>>& readerCertChain,
|
||||
bool expectSuccess,
|
||||
bool leaveOutAccessibleToAllFromRequestMessage) {
|
||||
canGetAccessibleByA_ = false;
|
||||
canGetAccessibleByAorB_ = false;
|
||||
canGetAccessibleByB_ = false;
|
||||
canGetAccessibleByC_ = false;
|
||||
canGetAccessibleByAll_ = false;
|
||||
canGetAccessibleByNone_ = false;
|
||||
|
||||
sp<IIdentityCredential> c;
|
||||
ASSERT_TRUE(credentialStore_
|
||||
->getCredential(
|
||||
CipherSuite::CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256,
|
||||
credentialData_, &c)
|
||||
.isOk());
|
||||
|
||||
optional<vector<uint8_t>> readerEKeyPair = support::createEcKeyPair();
|
||||
optional<vector<uint8_t>> readerEPublicKey =
|
||||
support::ecKeyPairGetPublicKey(readerEKeyPair.value());
|
||||
ASSERT_TRUE(c->setReaderEphemeralPublicKey(readerEPublicKey.value()).isOk());
|
||||
|
||||
vector<uint8_t> eKeyPair;
|
||||
ASSERT_TRUE(c->createEphemeralKeyPair(&eKeyPair).isOk());
|
||||
optional<vector<uint8_t>> ePublicKey = support::ecKeyPairGetPublicKey(eKeyPair);
|
||||
|
||||
// Calculate requestData field and sign it with the reader key.
|
||||
auto [getXYSuccess, ephX, ephY] = support::ecPublicKeyGetXandY(ePublicKey.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::Semantic(24, deviceEngagementBytes))
|
||||
.add(cppbor::Semantic(24, eReaderPubBytes));
|
||||
vector<uint8_t> sessionTranscriptBytes = sessionTranscript.encode();
|
||||
|
||||
vector<uint8_t> itemsRequestBytes;
|
||||
if (leaveOutAccessibleToAllFromRequestMessage) {
|
||||
itemsRequestBytes =
|
||||
cppbor::Map("nameSpaces",
|
||||
cppbor::Map().add("ns", cppbor::Map()
|
||||
.add("Accessible by A", false)
|
||||
.add("Accessible by A or B", false)
|
||||
.add("Accessible by B", false)
|
||||
.add("Accessible by C", false)
|
||||
.add("Accessible by None", false)))
|
||||
.encode();
|
||||
} else {
|
||||
itemsRequestBytes =
|
||||
cppbor::Map("nameSpaces",
|
||||
cppbor::Map().add("ns", cppbor::Map()
|
||||
.add("Accessible by A", false)
|
||||
.add("Accessible by A or B", false)
|
||||
.add("Accessible by B", false)
|
||||
.add("Accessible by C", 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::Semantic(24, itemsRequestBytes))
|
||||
.encode();
|
||||
|
||||
optional<vector<uint8_t>> readerSignature =
|
||||
support::coseSignEcDsa(readerPrivateKey, // private key for reader
|
||||
{}, // content
|
||||
dataToSign, // detached content
|
||||
support::certificateChainJoin(readerCertChain));
|
||||
ASSERT_TRUE(readerSignature);
|
||||
|
||||
// Generate the key that will be used to sign AuthenticatedData.
|
||||
vector<uint8_t> signingKeyBlob;
|
||||
Certificate signingKeyCertificate;
|
||||
ASSERT_TRUE(c->generateSigningKeyPair(&signingKeyBlob, &signingKeyCertificate).isOk());
|
||||
|
||||
RequestNamespace rns;
|
||||
rns.namespaceName = "ns";
|
||||
rns.items.push_back(buildRequestDataItem("Accessible by A", 1, {0}));
|
||||
rns.items.push_back(buildRequestDataItem("Accessible by A or B", 1, {0, 1}));
|
||||
rns.items.push_back(buildRequestDataItem("Accessible by B", 1, {1}));
|
||||
rns.items.push_back(buildRequestDataItem("Accessible by C", 1, {2}));
|
||||
rns.items.push_back(buildRequestDataItem("Accessible by All", 1, {3}));
|
||||
rns.items.push_back(buildRequestDataItem("Accessible by None", 1, {}));
|
||||
// OK to fail, not available in v1 HAL
|
||||
c->setRequestedNamespaces({rns}).isOk();
|
||||
|
||||
// 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();
|
||||
// OK to fail, not available in v1 HAL
|
||||
c->setVerificationToken(verificationToken);
|
||||
|
||||
Status status = c->startRetrieval(
|
||||
{sacp0_, sacp1_, sacp2_, sacp3_}, authToken, itemsRequestBytes, signingKeyBlob,
|
||||
sessionTranscriptBytes, readerSignature.value(), {6 /* numDataElementsPerNamespace */});
|
||||
if (expectSuccess) {
|
||||
ASSERT_TRUE(status.isOk());
|
||||
} else {
|
||||
ASSERT_FALSE(status.isOk());
|
||||
return;
|
||||
}
|
||||
|
||||
vector<uint8_t> decrypted;
|
||||
|
||||
status = c->startRetrieveEntryValue("ns", "Accessible by A", 1, {0});
|
||||
if (status.isOk()) {
|
||||
canGetAccessibleByA_ = true;
|
||||
ASSERT_TRUE(c->retrieveEntryValue(encContentAccessibleByA_, &decrypted).isOk());
|
||||
}
|
||||
|
||||
status = c->startRetrieveEntryValue("ns", "Accessible by A or B", 1, {0, 1});
|
||||
if (status.isOk()) {
|
||||
canGetAccessibleByAorB_ = true;
|
||||
ASSERT_TRUE(c->retrieveEntryValue(encContentAccessibleByAorB_, &decrypted).isOk());
|
||||
}
|
||||
|
||||
status = c->startRetrieveEntryValue("ns", "Accessible by B", 1, {1});
|
||||
if (status.isOk()) {
|
||||
canGetAccessibleByB_ = true;
|
||||
ASSERT_TRUE(c->retrieveEntryValue(encContentAccessibleByB_, &decrypted).isOk());
|
||||
}
|
||||
|
||||
status = c->startRetrieveEntryValue("ns", "Accessible by C", 1, {2});
|
||||
if (status.isOk()) {
|
||||
canGetAccessibleByC_ = true;
|
||||
ASSERT_TRUE(c->retrieveEntryValue(encContentAccessibleByC_, &decrypted).isOk());
|
||||
}
|
||||
|
||||
status = c->startRetrieveEntryValue("ns", "Accessible by All", 1, {3});
|
||||
if (status.isOk()) {
|
||||
canGetAccessibleByAll_ = true;
|
||||
ASSERT_TRUE(c->retrieveEntryValue(encContentAccessibleByAll_, &decrypted).isOk());
|
||||
}
|
||||
|
||||
status = c->startRetrieveEntryValue("ns", "Accessible by None", 1, {});
|
||||
if (status.isOk()) {
|
||||
canGetAccessibleByNone_ = true;
|
||||
ASSERT_TRUE(c->retrieveEntryValue(encContentAccessibleByNone_, &decrypted).isOk());
|
||||
}
|
||||
|
||||
vector<uint8_t> mac;
|
||||
vector<uint8_t> deviceNameSpaces;
|
||||
ASSERT_TRUE(c->finishRetrieval(&mac, &deviceNameSpaces).isOk());
|
||||
}
|
||||
|
||||
TEST_P(ReaderAuthTests, presentingChain_Reader) {
|
||||
provisionData();
|
||||
retrieveData(readerPrivateKey_, {cert_reader_SelfSigned_}, true /* expectSuccess */,
|
||||
false /* leaveOutAccessibleToAllFromRequestMessage */);
|
||||
EXPECT_FALSE(canGetAccessibleByA_);
|
||||
EXPECT_FALSE(canGetAccessibleByAorB_);
|
||||
EXPECT_FALSE(canGetAccessibleByB_);
|
||||
EXPECT_FALSE(canGetAccessibleByC_);
|
||||
EXPECT_TRUE(canGetAccessibleByAll_);
|
||||
EXPECT_FALSE(canGetAccessibleByNone_);
|
||||
}
|
||||
|
||||
TEST_P(ReaderAuthTests, presentingChain_Reader_A) {
|
||||
provisionData();
|
||||
retrieveData(readerPrivateKey_, {cert_reader_SignedBy_A_, cert_A_SelfSigned_},
|
||||
true /* expectSuccess */, false /* leaveOutAccessibleToAllFromRequestMessage */);
|
||||
EXPECT_TRUE(canGetAccessibleByA_);
|
||||
EXPECT_TRUE(canGetAccessibleByAorB_);
|
||||
EXPECT_FALSE(canGetAccessibleByB_);
|
||||
EXPECT_FALSE(canGetAccessibleByC_);
|
||||
EXPECT_TRUE(canGetAccessibleByAll_);
|
||||
EXPECT_FALSE(canGetAccessibleByNone_);
|
||||
}
|
||||
|
||||
TEST_P(ReaderAuthTests, presentingChain_Reader_B) {
|
||||
provisionData();
|
||||
retrieveData(readerPrivateKey_, {cert_reader_SignedBy_B_, cert_B_SelfSigned_},
|
||||
true /* expectSuccess */, false /* leaveOutAccessibleToAllFromRequestMessage */);
|
||||
EXPECT_FALSE(canGetAccessibleByA_);
|
||||
EXPECT_TRUE(canGetAccessibleByAorB_);
|
||||
EXPECT_TRUE(canGetAccessibleByB_);
|
||||
EXPECT_FALSE(canGetAccessibleByC_);
|
||||
EXPECT_TRUE(canGetAccessibleByAll_);
|
||||
EXPECT_FALSE(canGetAccessibleByNone_);
|
||||
}
|
||||
|
||||
// This test proves that for the purpose of determining inclusion of an ACP certificate
|
||||
// in a presented reader chain, certificate equality is done by comparing public keys,
|
||||
// not bitwise comparison of the certificates.
|
||||
//
|
||||
// Specifically for this test, the ACP is configured with cert_B_SelfSigned_ and the
|
||||
// reader is presenting cert_B_SignedBy_C_. Both certificates have the same public
|
||||
// key - intermediateBPublicKey_ - but they are signed by different keys.
|
||||
//
|
||||
TEST_P(ReaderAuthTests, presentingChain_Reader_B_C) {
|
||||
provisionData();
|
||||
retrieveData(readerPrivateKey_,
|
||||
{cert_reader_SignedBy_B_, cert_B_SignedBy_C_, cert_C_SelfSigned_},
|
||||
true /* expectSuccess */, false /* leaveOutAccessibleToAllFromRequestMessage */);
|
||||
EXPECT_FALSE(canGetAccessibleByA_);
|
||||
EXPECT_TRUE(canGetAccessibleByAorB_);
|
||||
EXPECT_TRUE(canGetAccessibleByB_);
|
||||
EXPECT_TRUE(canGetAccessibleByC_);
|
||||
EXPECT_TRUE(canGetAccessibleByAll_);
|
||||
EXPECT_FALSE(canGetAccessibleByNone_);
|
||||
}
|
||||
|
||||
// This test presents a reader chain where the chain is invalid because
|
||||
// the 2nd certificate in the chain isn't signed by the 3rd one.
|
||||
//
|
||||
TEST_P(ReaderAuthTests, presentingInvalidChain) {
|
||||
provisionData();
|
||||
retrieveData(readerPrivateKey_,
|
||||
{cert_reader_SignedBy_B_, cert_B_SelfSigned_, cert_C_SelfSigned_},
|
||||
false /* expectSuccess */, false /* leaveOutAccessibleToAllFromRequestMessage */);
|
||||
}
|
||||
|
||||
// This tests presents a valid reader chain but where requestMessage isn't
|
||||
// signed by the private key corresponding to the public key in the top-level
|
||||
// certificate.
|
||||
//
|
||||
TEST_P(ReaderAuthTests, presentingMessageSignedNotByTopLevel) {
|
||||
provisionData();
|
||||
retrieveData(intermediateBPrivateKey_,
|
||||
{cert_reader_SignedBy_B_, cert_B_SignedBy_C_, cert_C_SelfSigned_},
|
||||
false /* expectSuccess */, false /* leaveOutAccessibleToAllFromRequestMessage */);
|
||||
}
|
||||
|
||||
// This test leaves out "Accessible by All" data element from the signed request
|
||||
// message (the CBOR from the reader) while still including this data element at
|
||||
// the API level. The call on the API level for said element will fail with
|
||||
// STATUS_NOT_IN_REQUEST_MESSAGE but this doesn't prevent the other elements
|
||||
// from being returned (if authorized, of course).
|
||||
//
|
||||
// This test verifies that.
|
||||
//
|
||||
TEST_P(ReaderAuthTests, limitedMessage) {
|
||||
provisionData();
|
||||
retrieveData(readerPrivateKey_, {cert_reader_SelfSigned_}, true /* expectSuccess */,
|
||||
true /* leaveOutAccessibleToAllFromRequestMessage */);
|
||||
EXPECT_FALSE(canGetAccessibleByA_);
|
||||
EXPECT_FALSE(canGetAccessibleByAorB_);
|
||||
EXPECT_FALSE(canGetAccessibleByB_);
|
||||
EXPECT_FALSE(canGetAccessibleByC_);
|
||||
EXPECT_FALSE(canGetAccessibleByAll_);
|
||||
EXPECT_FALSE(canGetAccessibleByNone_);
|
||||
}
|
||||
|
||||
TEST_P(ReaderAuthTests, ephemeralKeyNotInSessionTranscript) {
|
||||
provisionData();
|
||||
|
||||
sp<IIdentityCredential> c;
|
||||
ASSERT_TRUE(credentialStore_
|
||||
->getCredential(
|
||||
CipherSuite::CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256,
|
||||
credentialData_, &c)
|
||||
.isOk());
|
||||
|
||||
optional<vector<uint8_t>> readerEKeyPair = support::createEcKeyPair();
|
||||
optional<vector<uint8_t>> readerEPublicKey =
|
||||
support::ecKeyPairGetPublicKey(readerEKeyPair.value());
|
||||
ASSERT_TRUE(c->setReaderEphemeralPublicKey(readerEPublicKey.value()).isOk());
|
||||
|
||||
vector<uint8_t> eKeyPair;
|
||||
ASSERT_TRUE(c->createEphemeralKeyPair(&eKeyPair).isOk());
|
||||
optional<vector<uint8_t>> ePublicKey = support::ecKeyPairGetPublicKey(eKeyPair);
|
||||
|
||||
// Calculate requestData field and sign it with the reader key.
|
||||
auto [getXYSuccess, ephX, ephY] = support::ecPublicKeyGetXandY(ePublicKey.value());
|
||||
ASSERT_TRUE(getXYSuccess);
|
||||
// Instead of include the X and Y coordinates (|ephX| and |ephY|), add NUL bytes instead.
|
||||
vector<uint8_t> nulls(32);
|
||||
cppbor::Map deviceEngagement = cppbor::Map().add("ephX", nulls).add("ephY", nulls);
|
||||
vector<uint8_t> deviceEngagementBytes = deviceEngagement.encode();
|
||||
vector<uint8_t> eReaderPubBytes = cppbor::Tstr("ignored").encode();
|
||||
cppbor::Array sessionTranscript = cppbor::Array()
|
||||
.add(cppbor::Semantic(24, deviceEngagementBytes))
|
||||
.add(cppbor::Semantic(24, eReaderPubBytes));
|
||||
vector<uint8_t> sessionTranscriptBytes = sessionTranscript.encode();
|
||||
|
||||
vector<uint8_t> itemsRequestBytes;
|
||||
itemsRequestBytes =
|
||||
cppbor::Map("nameSpaces",
|
||||
cppbor::Map().add("ns", cppbor::Map()
|
||||
.add("Accessible by A", false)
|
||||
.add("Accessible by A or B", false)
|
||||
.add("Accessible by B", false)
|
||||
.add("Accessible by C", false)
|
||||
.add("Accessible by None", false)))
|
||||
.encode();
|
||||
vector<uint8_t> dataToSign = cppbor::Array()
|
||||
.add("ReaderAuthentication")
|
||||
.add(sessionTranscript.clone())
|
||||
.add(cppbor::Semantic(24, itemsRequestBytes))
|
||||
.encode();
|
||||
|
||||
vector<vector<uint8_t>> readerCertChain = {cert_reader_SelfSigned_};
|
||||
optional<vector<uint8_t>> readerSignature =
|
||||
support::coseSignEcDsa(readerPrivateKey_, // private key for reader
|
||||
{}, // content
|
||||
dataToSign, // detached content
|
||||
support::certificateChainJoin(readerCertChain));
|
||||
ASSERT_TRUE(readerSignature);
|
||||
|
||||
// Generate the key that will be used to sign AuthenticatedData.
|
||||
vector<uint8_t> signingKeyBlob;
|
||||
Certificate signingKeyCertificate;
|
||||
ASSERT_TRUE(c->generateSigningKeyPair(&signingKeyBlob, &signingKeyCertificate).isOk());
|
||||
|
||||
RequestNamespace rns;
|
||||
rns.namespaceName = "ns";
|
||||
rns.items.push_back(buildRequestDataItem("Accessible by A", 1, {0}));
|
||||
rns.items.push_back(buildRequestDataItem("Accessible by A or B", 1, {0, 1}));
|
||||
rns.items.push_back(buildRequestDataItem("Accessible by B", 1, {1}));
|
||||
rns.items.push_back(buildRequestDataItem("Accessible by C", 1, {2}));
|
||||
rns.items.push_back(buildRequestDataItem("Accessible by All", 1, {3}));
|
||||
rns.items.push_back(buildRequestDataItem("Accessible by None", 1, {}));
|
||||
// OK to fail, not available in v1 HAL
|
||||
c->setRequestedNamespaces({rns}).isOk();
|
||||
|
||||
// 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::TRUSTED_ENVIRONMENT;
|
||||
verificationToken.mac.clear();
|
||||
// OK to fail, not available in v1 HAL
|
||||
c->setVerificationToken(verificationToken);
|
||||
|
||||
// Finally check that STATUS_EPHEMERAL_PUBLIC_KEY_NOT_FOUND is returned.
|
||||
// This proves that the TA checked for X and Y coordinatets and didn't find
|
||||
// them.
|
||||
Status status = c->startRetrieval(
|
||||
{sacp0_, sacp1_, sacp2_, sacp3_}, authToken, itemsRequestBytes, signingKeyBlob,
|
||||
sessionTranscriptBytes, readerSignature.value(), {6 /* numDataElementsPerNamespace */});
|
||||
ASSERT_FALSE(status.isOk());
|
||||
ASSERT_EQ(binder::Status::EX_SERVICE_SPECIFIC, status.exceptionCode());
|
||||
ASSERT_EQ(IIdentityCredentialStore::STATUS_EPHEMERAL_PUBLIC_KEY_NOT_FOUND,
|
||||
status.serviceSpecificErrorCode());
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
Identity, ReaderAuthTests,
|
||||
testing::ValuesIn(android::getAidlHalInstanceNames(IIdentityCredentialStore::descriptor)),
|
||||
android::PrintInstanceNameToString);
|
||||
|
||||
} // namespace android::hardware::identity
|
||||
473
identity/aidl/vts/UserAuthTests.cpp
Normal file
473
identity/aidl/vts/UserAuthTests.cpp
Normal file
@@ -0,0 +1,473 @@
|
||||
/*
|
||||
* 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 "VtsIdentityTestUtils.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 VtsIdentityTestUtils.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::Semantic(24, deviceEngagementBytes));
|
||||
sessionTranscript.add(43, cppbor::Semantic(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::Semantic(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 */);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
Identity, UserAuthTests,
|
||||
testing::ValuesIn(android::getAidlHalInstanceNames(IIdentityCredentialStore::descriptor)),
|
||||
android::PrintInstanceNameToString);
|
||||
|
||||
} // namespace android::hardware::identity
|
||||
@@ -61,51 +61,6 @@ class VtsAttestationTests : public testing::TestWithParam<std::string> {
|
||||
sp<IIdentityCredentialStore> credentialStore_;
|
||||
};
|
||||
|
||||
TEST_P(VtsAttestationTests, verifyAttestationWithEmptyChallengeEmptyId) {
|
||||
Status result;
|
||||
|
||||
HardwareInformation hwInfo;
|
||||
ASSERT_TRUE(credentialStore_->getHardwareInformation(&hwInfo).isOk());
|
||||
|
||||
sp<IWritableIdentityCredential> writableCredential;
|
||||
ASSERT_TRUE(test_utils::setupWritableCredential(writableCredential, credentialStore_));
|
||||
|
||||
vector<uint8_t> attestationChallenge;
|
||||
vector<Certificate> attestationCertificate;
|
||||
vector<uint8_t> attestationApplicationId = {};
|
||||
result = writableCredential->getAttestationCertificate(
|
||||
attestationApplicationId, attestationChallenge, &attestationCertificate);
|
||||
|
||||
ASSERT_TRUE(result.isOk()) << result.exceptionCode() << "; " << result.exceptionMessage()
|
||||
<< endl;
|
||||
|
||||
EXPECT_TRUE(validateAttestationCertificate(attestationCertificate, attestationChallenge,
|
||||
attestationApplicationId, hwInfo));
|
||||
}
|
||||
|
||||
TEST_P(VtsAttestationTests, verifyAttestationWithEmptyChallengeNonemptyId) {
|
||||
Status result;
|
||||
|
||||
HardwareInformation hwInfo;
|
||||
ASSERT_TRUE(credentialStore_->getHardwareInformation(&hwInfo).isOk());
|
||||
|
||||
sp<IWritableIdentityCredential> writableCredential;
|
||||
ASSERT_TRUE(setupWritableCredential(writableCredential, credentialStore_));
|
||||
|
||||
vector<uint8_t> attestationChallenge;
|
||||
vector<Certificate> attestationCertificate;
|
||||
string applicationId = "Attestation Verification";
|
||||
vector<uint8_t> attestationApplicationId = {applicationId.begin(), applicationId.end()};
|
||||
|
||||
result = writableCredential->getAttestationCertificate(
|
||||
attestationApplicationId, attestationChallenge, &attestationCertificate);
|
||||
|
||||
ASSERT_TRUE(result.isOk()) << result.exceptionCode() << "; " << result.exceptionMessage()
|
||||
<< endl;
|
||||
EXPECT_TRUE(validateAttestationCertificate(attestationCertificate, attestationChallenge,
|
||||
attestationApplicationId, hwInfo));
|
||||
}
|
||||
|
||||
TEST_P(VtsAttestationTests, verifyAttestationWithNonemptyChallengeEmptyId) {
|
||||
Status result;
|
||||
|
||||
|
||||
@@ -27,15 +27,18 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <future>
|
||||
#include <map>
|
||||
#include <tuple>
|
||||
|
||||
#include "VtsIdentityTestUtils.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;
|
||||
@@ -66,6 +69,61 @@ TEST_P(IdentityAidl, hardwareInformation) {
|
||||
ASSERT_GE(info.dataChunkSize, 256);
|
||||
}
|
||||
|
||||
tuple<bool, string, vector<uint8_t>, vector<uint8_t>> extractFromTestCredentialData(
|
||||
const vector<uint8_t>& credentialData) {
|
||||
string docType;
|
||||
vector<uint8_t> storageKey;
|
||||
vector<uint8_t> credentialPrivKey;
|
||||
|
||||
auto [item, _, message] = cppbor::parse(credentialData);
|
||||
if (item == nullptr) {
|
||||
return make_tuple(false, docType, storageKey, credentialPrivKey);
|
||||
}
|
||||
|
||||
const cppbor::Array* arrayItem = item->asArray();
|
||||
if (arrayItem == nullptr || arrayItem->size() != 3) {
|
||||
return make_tuple(false, docType, storageKey, credentialPrivKey);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
auto [dckItem, dckPos, dckMessage] = cppbor::parse(decryptedCredentialKeys.value());
|
||||
if (dckItem == nullptr) {
|
||||
return make_tuple(false, docType, storageKey, credentialPrivKey);
|
||||
}
|
||||
const cppbor::Array* dckArrayItem = dckItem->asArray();
|
||||
if (dckArrayItem == nullptr || dckArrayItem->size() != 2) {
|
||||
return make_tuple(false, docType, storageKey, credentialPrivKey);
|
||||
}
|
||||
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);
|
||||
}
|
||||
storageKey = storageKeyItem->value();
|
||||
credentialPrivKey = credentialPrivKeyItem->value();
|
||||
return make_tuple(true, docType, storageKey, credentialPrivKey);
|
||||
}
|
||||
|
||||
TEST_P(IdentityAidl, createAndRetrieveCredential) {
|
||||
// First, generate a key-pair for the reader since its public key will be
|
||||
// part of the request data.
|
||||
@@ -155,6 +213,7 @@ TEST_P(IdentityAidl, createAndRetrieveCredential) {
|
||||
writableCredential->finishAddingEntries(&credentialData, &proofOfProvisioningSignature)
|
||||
.isOk());
|
||||
|
||||
// Validate the proofOfProvisioning which was returned
|
||||
optional<vector<uint8_t>> proofOfProvisioning =
|
||||
support::coseSignGetPayload(proofOfProvisioningSignature);
|
||||
ASSERT_TRUE(proofOfProvisioning);
|
||||
@@ -215,6 +274,22 @@ TEST_P(IdentityAidl, createAndRetrieveCredential) {
|
||||
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] =
|
||||
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());
|
||||
|
||||
// Now that the credential has been provisioned, read it back and check the
|
||||
// correct data is returned.
|
||||
sp<IIdentityCredential> credential;
|
||||
@@ -287,6 +362,24 @@ TEST_P(IdentityAidl, createAndRetrieveCredential) {
|
||||
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);
|
||||
|
||||
// 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
|
||||
@@ -316,6 +409,9 @@ TEST_P(IdentityAidl, createAndRetrieveCredential) {
|
||||
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;
|
||||
@@ -346,15 +442,12 @@ TEST_P(IdentityAidl, createAndRetrieveCredential) {
|
||||
deviceAuthentication.add(docType);
|
||||
deviceAuthentication.add(cppbor::Semantic(24, deviceNameSpacesBytes));
|
||||
vector<uint8_t> encodedDeviceAuthentication = deviceAuthentication.encode();
|
||||
optional<vector<uint8_t>> signingPublicKey =
|
||||
support::certificateChainGetTopMostKey(signingKeyCertificate.encodedCertificate);
|
||||
EXPECT_TRUE(signingPublicKey);
|
||||
|
||||
// Derive the key used for MACing.
|
||||
optional<vector<uint8_t>> readerEphemeralPrivateKey =
|
||||
support::ecKeyPairGetPrivateKey(readerEphemeralKeyPair.value());
|
||||
optional<vector<uint8_t>> sharedSecret =
|
||||
support::ecdh(signingPublicKey.value(), readerEphemeralPrivateKey.value());
|
||||
support::ecdh(signingPubKey.value(), readerEphemeralPrivateKey.value());
|
||||
ASSERT_TRUE(sharedSecret);
|
||||
vector<uint8_t> salt = {0x00};
|
||||
vector<uint8_t> info = {};
|
||||
|
||||
@@ -69,11 +69,10 @@ TEST_P(IdentityCredentialTests, verifyAttestationWithEmptyChallenge) {
|
||||
result = writableCredential->getAttestationCertificate(
|
||||
attestationApplicationId, attestationChallenge, &attestationCertificate);
|
||||
|
||||
EXPECT_TRUE(result.isOk()) << result.exceptionCode() << "; " << result.exceptionMessage()
|
||||
<< endl;
|
||||
|
||||
EXPECT_TRUE(test_utils::validateAttestationCertificate(
|
||||
attestationCertificate, attestationChallenge, attestationApplicationId, hwInfo));
|
||||
EXPECT_FALSE(result.isOk()) << result.exceptionCode() << "; " << result.exceptionMessage()
|
||||
<< endl;
|
||||
EXPECT_EQ(binder::Status::EX_SERVICE_SPECIFIC, result.exceptionCode());
|
||||
EXPECT_EQ(IIdentityCredentialStore::STATUS_INVALID_DATA, result.serviceSpecificErrorCode());
|
||||
}
|
||||
|
||||
TEST_P(IdentityCredentialTests, verifyAttestationSuccessWithChallenge) {
|
||||
@@ -130,6 +129,7 @@ TEST_P(IdentityCredentialTests, verifyStartPersonalization) {
|
||||
|
||||
// First call should go through
|
||||
const vector<int32_t> entryCounts = {2, 4};
|
||||
writableCredential->setExpectedProofOfProvisioningSize(123456);
|
||||
result = writableCredential->startPersonalization(5, entryCounts);
|
||||
ASSERT_TRUE(result.isOk()) << result.exceptionCode() << "; " << result.exceptionMessage()
|
||||
<< endl;
|
||||
@@ -151,18 +151,8 @@ TEST_P(IdentityCredentialTests, verifyStartPersonalizationMin) {
|
||||
|
||||
// Verify minimal number of profile count and entry count
|
||||
const vector<int32_t> entryCounts = {1, 1};
|
||||
writableCredential->startPersonalization(1, entryCounts);
|
||||
EXPECT_TRUE(result.isOk()) << result.exceptionCode() << "; " << result.exceptionMessage()
|
||||
<< endl;
|
||||
}
|
||||
|
||||
TEST_P(IdentityCredentialTests, verifyStartPersonalizationZero) {
|
||||
Status result;
|
||||
sp<IWritableIdentityCredential> writableCredential;
|
||||
ASSERT_TRUE(test_utils::setupWritableCredential(writableCredential, credentialStore_));
|
||||
|
||||
const vector<int32_t> entryCounts = {0};
|
||||
writableCredential->startPersonalization(0, entryCounts);
|
||||
writableCredential->setExpectedProofOfProvisioningSize(123456);
|
||||
result = writableCredential->startPersonalization(1, entryCounts);
|
||||
EXPECT_TRUE(result.isOk()) << result.exceptionCode() << "; " << result.exceptionMessage()
|
||||
<< endl;
|
||||
}
|
||||
@@ -174,7 +164,8 @@ TEST_P(IdentityCredentialTests, verifyStartPersonalizationOne) {
|
||||
|
||||
// Verify minimal number of profile count and entry count
|
||||
const vector<int32_t> entryCounts = {1};
|
||||
writableCredential->startPersonalization(1, entryCounts);
|
||||
writableCredential->setExpectedProofOfProvisioningSize(123456);
|
||||
result = writableCredential->startPersonalization(1, entryCounts);
|
||||
EXPECT_TRUE(result.isOk()) << result.exceptionCode() << "; " << result.exceptionMessage()
|
||||
<< endl;
|
||||
}
|
||||
@@ -186,7 +177,8 @@ TEST_P(IdentityCredentialTests, verifyStartPersonalizationLarge) {
|
||||
|
||||
// Verify set a large number of profile count and entry count is ok
|
||||
const vector<int32_t> entryCounts = {3000};
|
||||
writableCredential->startPersonalization(3500, entryCounts);
|
||||
writableCredential->setExpectedProofOfProvisioningSize(123456);
|
||||
result = writableCredential->startPersonalization(25, entryCounts);
|
||||
EXPECT_TRUE(result.isOk()) << result.exceptionCode() << "; " << result.exceptionMessage()
|
||||
<< endl;
|
||||
}
|
||||
@@ -198,7 +190,8 @@ TEST_P(IdentityCredentialTests, verifyProfileNumberMismatchShouldFail) {
|
||||
|
||||
// Enter mismatched entry and profile numbers
|
||||
const vector<int32_t> entryCounts = {5, 6};
|
||||
writableCredential->startPersonalization(5, entryCounts);
|
||||
writableCredential->setExpectedProofOfProvisioningSize(123456);
|
||||
result = writableCredential->startPersonalization(5, entryCounts);
|
||||
ASSERT_TRUE(result.isOk()) << result.exceptionCode() << "; " << result.exceptionMessage()
|
||||
<< endl;
|
||||
|
||||
@@ -234,7 +227,8 @@ TEST_P(IdentityCredentialTests, verifyDuplicateProfileId) {
|
||||
ASSERT_TRUE(test_utils::setupWritableCredential(writableCredential, credentialStore_));
|
||||
|
||||
const vector<int32_t> entryCounts = {3, 6};
|
||||
writableCredential->startPersonalization(3, entryCounts);
|
||||
writableCredential->setExpectedProofOfProvisioningSize(123456);
|
||||
result = writableCredential->startPersonalization(3, entryCounts);
|
||||
ASSERT_TRUE(result.isOk()) << result.exceptionCode() << "; " << result.exceptionMessage()
|
||||
<< endl;
|
||||
|
||||
@@ -251,9 +245,10 @@ TEST_P(IdentityCredentialTests, verifyDuplicateProfileId) {
|
||||
SecureAccessControlProfile profile;
|
||||
Certificate cert;
|
||||
cert.encodedCertificate = testProfile.readerCertificate;
|
||||
int64_t secureUserId = testProfile.userAuthenticationRequired ? 66 : 0;
|
||||
result = writableCredential->addAccessControlProfile(
|
||||
testProfile.id, cert, testProfile.userAuthenticationRequired,
|
||||
testProfile.timeoutMillis, 0, &profile);
|
||||
testProfile.timeoutMillis, secureUserId, &profile);
|
||||
|
||||
if (expectOk) {
|
||||
expectOk = false;
|
||||
@@ -554,7 +549,7 @@ TEST_P(IdentityCredentialTests, verifyEmptyNameSpaceMixedWithNonEmptyWorks) {
|
||||
;
|
||||
// OK to fail, not available in v1 HAL
|
||||
writableCredential->setExpectedProofOfProvisioningSize(expectedPoPSize);
|
||||
writableCredential->startPersonalization(3, entryCounts);
|
||||
result = writableCredential->startPersonalization(3, entryCounts);
|
||||
ASSERT_TRUE(result.isOk()) << result.exceptionCode() << "; " << result.exceptionMessage()
|
||||
<< endl;
|
||||
|
||||
@@ -608,7 +603,8 @@ TEST_P(IdentityCredentialTests, verifyInterleavingEntryNameSpaceOrderingFails) {
|
||||
// before "Image" and 2 after image, which is not correct. All of same name
|
||||
// space should occur together. Let's see if this fails.
|
||||
const vector<int32_t> entryCounts = {2u, 1u, 2u};
|
||||
writableCredential->startPersonalization(3, entryCounts);
|
||||
writableCredential->setExpectedProofOfProvisioningSize(123456);
|
||||
result = writableCredential->startPersonalization(3, entryCounts);
|
||||
ASSERT_TRUE(result.isOk()) << result.exceptionCode() << "; " << result.exceptionMessage()
|
||||
<< endl;
|
||||
|
||||
@@ -674,6 +670,7 @@ TEST_P(IdentityCredentialTests, verifyAccessControlProfileIdOutOfRange) {
|
||||
ASSERT_TRUE(test_utils::setupWritableCredential(writableCredential, credentialStore_));
|
||||
|
||||
const vector<int32_t> entryCounts = {1};
|
||||
writableCredential->setExpectedProofOfProvisioningSize(123456);
|
||||
Status result = writableCredential->startPersonalization(1, entryCounts);
|
||||
ASSERT_TRUE(result.isOk()) << result.exceptionCode() << "; " << result.exceptionMessage()
|
||||
<< endl;
|
||||
|
||||
@@ -96,9 +96,10 @@ optional<vector<SecureAccessControlProfile>> addAccessControlProfiles(
|
||||
SecureAccessControlProfile profile;
|
||||
Certificate cert;
|
||||
cert.encodedCertificate = testProfile.readerCertificate;
|
||||
int64_t secureUserId = testProfile.userAuthenticationRequired ? 66 : 0;
|
||||
result = writableCredential->addAccessControlProfile(
|
||||
testProfile.id, cert, testProfile.userAuthenticationRequired,
|
||||
testProfile.timeoutMillis, 0, &profile);
|
||||
testProfile.timeoutMillis, secureUserId, &profile);
|
||||
|
||||
// Don't use assert so all errors can be outputed. Then return
|
||||
// instead of exit even on errors so caller can decide.
|
||||
|
||||
@@ -134,6 +134,11 @@ optional<vector<uint8_t>> ecKeyPairGetPublicKey(const vector<uint8_t>& keyPair);
|
||||
//
|
||||
optional<vector<uint8_t>> ecKeyPairGetPrivateKey(const vector<uint8_t>& keyPair);
|
||||
|
||||
// Creates a PKCS#8 encoded key-pair from a private key (which must be uncompressed,
|
||||
// e.g. 32 bytes). The public key is derived from the given private key..
|
||||
//
|
||||
optional<vector<uint8_t>> ecPrivateKeyToKeyPair(const vector<uint8_t>& privateKey);
|
||||
|
||||
// For an EC key |keyPair| encoded in PKCS#8 format, creates a PKCS#12 structure
|
||||
// with the key-pair (not using a password to encrypt the data). The public key
|
||||
// in the created structure is included as a certificate, using the given fields
|
||||
|
||||
@@ -1047,6 +1047,42 @@ optional<vector<uint8_t>> ecKeyPairGetPrivateKey(const vector<uint8_t>& keyPair)
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
optional<vector<uint8_t>> ecPrivateKeyToKeyPair(const vector<uint8_t>& privateKey) {
|
||||
auto bn = BIGNUM_Ptr(BN_bin2bn(privateKey.data(), privateKey.size(), nullptr));
|
||||
if (bn.get() == nullptr) {
|
||||
LOG(ERROR) << "Error creating BIGNUM";
|
||||
return {};
|
||||
}
|
||||
|
||||
auto ecKey = EC_KEY_Ptr(EC_KEY_new_by_curve_name(NID_X9_62_prime256v1));
|
||||
if (EC_KEY_set_private_key(ecKey.get(), bn.get()) != 1) {
|
||||
LOG(ERROR) << "Error setting private key from BIGNUM";
|
||||
return {};
|
||||
}
|
||||
|
||||
auto pkey = EVP_PKEY_Ptr(EVP_PKEY_new());
|
||||
if (pkey.get() == nullptr) {
|
||||
LOG(ERROR) << "Memory allocation failed";
|
||||
return {};
|
||||
}
|
||||
|
||||
if (EVP_PKEY_set1_EC_KEY(pkey.get(), ecKey.get()) != 1) {
|
||||
LOG(ERROR) << "Error getting private key";
|
||||
return {};
|
||||
}
|
||||
|
||||
int size = i2d_PrivateKey(pkey.get(), nullptr);
|
||||
if (size == 0) {
|
||||
LOG(ERROR) << "Error generating public key encoding";
|
||||
return {};
|
||||
}
|
||||
vector<uint8_t> keyPair;
|
||||
keyPair.resize(size);
|
||||
unsigned char* p = keyPair.data();
|
||||
i2d_PrivateKey(pkey.get(), &p);
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
optional<vector<uint8_t>> ecKeyPairGetPkcs12(const vector<uint8_t>& keyPair, const string& name,
|
||||
const string& serialDecimal, const string& issuer,
|
||||
const string& subject, time_t validityNotBefore,
|
||||
|
||||
Reference in New Issue
Block a user