| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "device/fido/virtual_ctap2_device.h" |
| |
| #include <algorithm> |
| #include <array> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/base64url.h" |
| #include "base/bind.h" |
| #include "base/containers/span.h" |
| #include "base/json/string_escape.h" |
| #include "base/logging.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "components/apdu/apdu_response.h" |
| #include "components/cbor/reader.h" |
| #include "components/cbor/writer.h" |
| #include "crypto/ec_private_key.h" |
| #include "device/fido/authenticator_get_assertion_response.h" |
| #include "device/fido/authenticator_make_credential_response.h" |
| #include "device/fido/authenticator_supported_options.h" |
| #include "device/fido/bio/enrollment.h" |
| #include "device/fido/credential_management.h" |
| #include "device/fido/ctap_get_assertion_request.h" |
| #include "device/fido/ctap_make_credential_request.h" |
| #include "device/fido/device_response_converter.h" |
| #include "device/fido/fido_constants.h" |
| #include "device/fido/fido_parsing_utils.h" |
| #include "device/fido/large_blob.h" |
| #include "device/fido/opaque_attestation_statement.h" |
| #include "device/fido/p256_public_key.h" |
| #include "device/fido/pin.h" |
| #include "device/fido/pin_internal.h" |
| #include "device/fido/public_key.h" |
| #include "device/fido/virtual_u2f_device.h" |
| #include "third_party/boringssl/src/include/openssl/aes.h" |
| #include "third_party/boringssl/src/include/openssl/digest.h" |
| #include "third_party/boringssl/src/include/openssl/ec.h" |
| #include "third_party/boringssl/src/include/openssl/ec_key.h" |
| #include "third_party/boringssl/src/include/openssl/evp.h" |
| #include "third_party/boringssl/src/include/openssl/hmac.h" |
| #include "third_party/boringssl/src/include/openssl/mem.h" |
| #include "third_party/boringssl/src/include/openssl/obj.h" |
| #include "third_party/boringssl/src/include/openssl/rand.h" |
| #include "third_party/boringssl/src/include/openssl/sha.h" |
| |
| namespace device { |
| |
| namespace { |
| |
| constexpr std::array<uint8_t, kAaguidLength> kDeviceAaguid = { |
| {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, |
| 0x05, 0x06, 0x07, 0x08}}; |
| |
| struct PinUvAuthTokenPermissions { |
| uint8_t permissions; |
| base::Optional<std::string> rp_id; |
| }; |
| |
| uint8_t GetSupportedPermissionsMask(const VirtualCtap2Device::Config& config) { |
| uint8_t permissions = |
| static_cast<uint8_t>(pin::Permissions::kMakeCredential) | |
| static_cast<uint8_t>(pin::Permissions::kGetAssertion); |
| if (config.credential_management_support) { |
| permissions |= |
| static_cast<uint8_t>(pin::Permissions::kCredentialManagement); |
| } |
| if (config.bio_enrollment_support) { |
| permissions |= static_cast<uint8_t>(pin::Permissions::kBioEnrollment); |
| } |
| if (config.large_blob_support) { |
| permissions |= static_cast<uint8_t>(pin::Permissions::kLargeBlobWrite); |
| } |
| return permissions; |
| } |
| |
| std::vector<uint8_t> ConstructResponse(CtapDeviceResponseCode response_code, |
| base::span<const uint8_t> data) { |
| std::vector<uint8_t> response{base::strict_cast<uint8_t>(response_code)}; |
| fido_parsing_utils::Append(&response, data); |
| return response; |
| } |
| |
| // Returns true if the |permissions| parameter requires an explicit permissions |
| // RPID. |
| bool PermissionsRequireRPID(uint8_t permissions) { |
| return permissions & |
| static_cast<uint8_t>(pin::Permissions::kMakeCredential) || |
| permissions & static_cast<uint8_t>(pin::Permissions::kGetAssertion); |
| } |
| |
| CtapDeviceResponseCode ExtractPermissions( |
| const cbor::Value::MapValue& request_map, |
| const VirtualCtap2Device::Config& config, |
| PinUvAuthTokenPermissions& out_permissions) { |
| const auto permissions_it = request_map.find( |
| cbor::Value(static_cast<int>(pin::RequestKey::kPermissions))); |
| if (permissions_it == request_map.end() || |
| !permissions_it->second.is_unsigned()) { |
| return CtapDeviceResponseCode::kCtap2ErrMissingParameter; |
| } |
| out_permissions.permissions = permissions_it->second.GetUnsigned(); |
| if (out_permissions.permissions == 0) { |
| return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; |
| } |
| |
| DCHECK_EQ(out_permissions.permissions & ~GetSupportedPermissionsMask(config), |
| 0); |
| |
| const auto permissions_rpid_it = request_map.find( |
| cbor::Value(static_cast<int>(pin::RequestKey::kPermissionsRPID))); |
| if (permissions_rpid_it == request_map.end() && |
| PermissionsRequireRPID(out_permissions.permissions)) { |
| return CtapDeviceResponseCode::kCtap2ErrMissingParameter; |
| } |
| if (permissions_rpid_it != request_map.end()) { |
| if (!permissions_rpid_it->second.is_string()) { |
| return CtapDeviceResponseCode::kCtap2ErrMissingParameter; |
| } |
| out_permissions.rp_id = permissions_rpid_it->second.GetString(); |
| } |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| void ReturnCtap2Response( |
| FidoDevice::DeviceCallback cb, |
| CtapDeviceResponseCode response_code, |
| base::Optional<base::span<const uint8_t>> data = base::nullopt) { |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| base::BindOnce(std::move(cb), |
| ConstructResponse(response_code, |
| data.value_or(std::vector<uint8_t>{})))); |
| } |
| |
| std::vector<uint8_t> ConstructSignatureBuffer( |
| const AuthenticatorData& authenticator_data, |
| base::span<const uint8_t, kClientDataHashLength> client_data_hash) { |
| std::vector<uint8_t> signature_buffer; |
| fido_parsing_utils::Append(&signature_buffer, |
| authenticator_data.SerializeToByteArray()); |
| fido_parsing_utils::Append(&signature_buffer, client_data_hash); |
| return signature_buffer; |
| } |
| |
| std::string ConstructAndroidClientDataJSON( |
| const AndroidClientDataExtensionInput& input, |
| base::StringPiece type) { |
| std::string challenge_b64url; |
| base::Base64UrlEncode( |
| base::StringPiece(reinterpret_cast<const char*>(input.challenge.data()), |
| input.challenge.size()), |
| base::Base64UrlEncodePolicy::OMIT_PADDING, &challenge_b64url); |
| return "{\"challenge\":" + base::GetQuotedJSONString(challenge_b64url) + |
| ",\"origin\":" + base::GetQuotedJSONString(input.origin.Serialize()) + |
| ",\"type\":" + base::GetQuotedJSONString(type) + |
| ",\"androidPackageName\":\"org.chromium.device.fido.test\"}"; |
| } |
| |
| std::vector<uint8_t> ConstructMakeCredentialResponse( |
| const base::Optional<std::vector<uint8_t>> attestation_certificate, |
| base::span<const uint8_t> signature, |
| AuthenticatorData authenticator_data, |
| base::Optional<std::vector<uint8_t>> android_client_data_ext, |
| bool enterprise_attestation_requested, |
| base::Optional<std::array<uint8_t, kLargeBlobKeyLength>> large_blob_key) { |
| cbor::Value::MapValue attestation_map; |
| attestation_map.emplace("alg", -7); |
| attestation_map.emplace("sig", fido_parsing_utils::Materialize(signature)); |
| |
| if (attestation_certificate) { |
| cbor::Value::ArrayValue certificate_chain; |
| certificate_chain.emplace_back(std::move(*attestation_certificate)); |
| attestation_map.emplace("x5c", std::move(certificate_chain)); |
| } |
| |
| AuthenticatorMakeCredentialResponse make_credential_response( |
| FidoTransportProtocol::kUsbHumanInterfaceDevice, |
| AttestationObject( |
| std::move(authenticator_data), |
| std::make_unique<OpaqueAttestationStatement>( |
| "packed", cbor::Value(std::move(attestation_map))))); |
| if (android_client_data_ext) { |
| make_credential_response.set_android_client_data_ext( |
| *android_client_data_ext); |
| } |
| make_credential_response.enterprise_attestation_returned = |
| enterprise_attestation_requested; |
| if (large_blob_key) { |
| make_credential_response.set_large_blob_key(*large_blob_key); |
| } |
| return AsCTAPStyleCBORBytes(make_credential_response); |
| } |
| |
| base::Optional<std::vector<uint8_t>> GetPINBytestring( |
| const cbor::Value::MapValue& request, |
| pin::RequestKey key) { |
| const auto it = request.find(cbor::Value(static_cast<int>(key))); |
| if (it == request.end() || !it->second.is_bytestring()) { |
| return base::nullopt; |
| } |
| return it->second.GetBytestring(); |
| } |
| |
| base::Optional<bssl::UniquePtr<EC_POINT>> GetPINKey( |
| const cbor::Value::MapValue& request, |
| pin::RequestKey map_key) { |
| const auto it = request.find(cbor::Value(static_cast<int>(map_key))); |
| if (it == request.end() || !it->second.is_map()) { |
| return base::nullopt; |
| } |
| const auto& cose_key = it->second.GetMap(); |
| auto response = pin::KeyAgreementResponse::ParseFromCOSE(cose_key); |
| if (!response) { |
| return base::nullopt; |
| } |
| |
| bssl::UniquePtr<EC_GROUP> group( |
| EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)); |
| return pin::PointFromKeyAgreementResponse(group.get(), *response).value(); |
| } |
| |
| // ConfirmPresentedPIN checks whether |encrypted_pin_hash| is a valid proof-of- |
| // possession of the PIN, given that |shared_key| is the result of the ECDH key |
| // agreement. |
| CtapDeviceResponseCode ConfirmPresentedPIN( |
| PINUVAuthProtocol pin_protocol, |
| VirtualCtap2Device::State* state, |
| const std::vector<uint8_t>& shared_key, |
| const std::vector<uint8_t>& encrypted_pin_hash) { |
| constexpr size_t kPinHashSize = AES_BLOCK_SIZE; |
| if (encrypted_pin_hash.empty() || |
| encrypted_pin_hash.size() % kPinHashSize != 0u) { |
| return CtapDeviceResponseCode::kCtap2ErrPinInvalid; |
| } |
| |
| if (state->pin_retries == 0) { |
| return CtapDeviceResponseCode::kCtap2ErrPinBlocked; |
| } |
| if (state->soft_locked) { |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthBlocked; |
| } |
| |
| state->pin_retries--; |
| state->pin_retries_since_insertion++; |
| |
| std::vector<uint8_t> pin_hash = pin::ProtocolVersion(pin_protocol) |
| .Decrypt(shared_key, encrypted_pin_hash); |
| |
| uint8_t calculated_pin_hash[SHA256_DIGEST_LENGTH]; |
| SHA256(reinterpret_cast<const uint8_t*>(state->pin.data()), state->pin.size(), |
| calculated_pin_hash); |
| static_assert(sizeof(calculated_pin_hash) >= kPinHashSize, ""); |
| |
| if (state->pin.empty() || pin_hash.size() != kPinHashSize || |
| CRYPTO_memcmp(pin_hash.data(), calculated_pin_hash, kPinHashSize) != 0) { |
| if (state->pin_retries == 0) { |
| return CtapDeviceResponseCode::kCtap2ErrPinBlocked; |
| } |
| if (state->pin_retries_since_insertion == 3) { |
| state->soft_locked = true; |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthBlocked; |
| } |
| return CtapDeviceResponseCode::kCtap2ErrPinInvalid; |
| } |
| |
| state->pin_retries = kMaxPinRetries; |
| state->uv_retries = kMaxUvRetries; |
| state->pin_retries_since_insertion = 0; |
| |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| // SetPIN sets the current PIN based on the ciphertext in |encrypted_pin|, given |
| // that |shared_key| is the result of the ECDH key agreement. |
| CtapDeviceResponseCode SetPIN( |
| PINUVAuthProtocol protocol, |
| VirtualCtap2Device::State* state, |
| const std::vector<uint8_t>& shared_key, |
| const std::vector<uint8_t>& encrypted_pin, |
| const std::vector<uint8_t>& pin_auth, |
| base::Optional<base::span<const uint8_t>> current_encrypted_pin_hash) { |
| const pin::Protocol& pin_protocol = pin::ProtocolVersion(protocol); |
| std::vector<uint8_t> pin_auth_bytes; |
| pin_auth_bytes.insert(pin_auth_bytes.begin(), encrypted_pin.begin(), |
| encrypted_pin.end()); |
| if (current_encrypted_pin_hash) { |
| pin_auth_bytes.insert(pin_auth_bytes.end(), |
| current_encrypted_pin_hash->begin(), |
| current_encrypted_pin_hash->end()); |
| } |
| if (!pin_protocol.Verify(shared_key, pin_auth_bytes, pin_auth)) { |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; |
| } |
| |
| if (encrypted_pin.size() < 64) { |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; |
| } |
| |
| std::vector<uint8_t> plaintext_pin = |
| pin_protocol.Decrypt(shared_key, encrypted_pin); |
| |
| size_t padding_len = 0; |
| while (padding_len < plaintext_pin.size() && |
| plaintext_pin[plaintext_pin.size() - padding_len - 1] == 0) { |
| padding_len++; |
| } |
| |
| plaintext_pin.resize(plaintext_pin.size() - padding_len); |
| if (padding_len == 0 || plaintext_pin.size() < state->min_pin_length || |
| plaintext_pin.size() > 63) { |
| return CtapDeviceResponseCode::kCtap2ErrPinPolicyViolation; |
| } |
| |
| state->pin = std::string(reinterpret_cast<const char*>(plaintext_pin.data()), |
| plaintext_pin.size()); |
| state->pin_retries = kMaxPinRetries; |
| state->uv_retries = kMaxUvRetries; |
| state->force_pin_change = false; |
| |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| // VerifyPINUVAuthToken returns whether |request_map| contains a pinAuth |
| // parameter mapped to |pin_auth_map_key| that is a valid PIN/UV Auth Protocol |
| // authentication of |pinauth_bytes|. |pin_protocol_map_key| is the |
| // |request_map| index for the selected PIN/UV protocol version, which is |
| // checked against the supported versions from |authenticator_info|. |
| CtapDeviceResponseCode VerifyPINUVAuthToken( |
| const AuthenticatorGetInfoResponse& authenticator_info, |
| base::span<const uint8_t> pin_token, |
| const cbor::Value::MapValue& request_map, |
| const cbor::Value& pin_protocol_map_key, |
| const cbor::Value& pin_auth_map_key, |
| base::span<const uint8_t> pinauth_bytes) { |
| DCHECK( |
| authenticator_info.options.client_pin_availability != |
| AuthenticatorSupportedOptions::ClientPinAvailability::kNotSupported || |
| (authenticator_info.options.user_verification_availability != |
| AuthenticatorSupportedOptions::UserVerificationAvailability:: |
| kNotSupported)); |
| DCHECK(authenticator_info.pin_protocols && |
| !authenticator_info.pin_protocols->empty()); |
| |
| const auto pin_protocol_it = request_map.find(pin_protocol_map_key); |
| if (pin_protocol_it == request_map.end() || |
| !pin_protocol_it->second.is_unsigned()) { |
| return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; |
| } |
| base::Optional<PINUVAuthProtocol> protocol = |
| ToPINUVAuthProtocol(pin_protocol_it->second.GetUnsigned()); |
| if (!protocol || |
| !base::Contains(*authenticator_info.pin_protocols, *protocol)) { |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; |
| } |
| const auto pinauth_it = request_map.find(pin_auth_map_key); |
| if (pinauth_it == request_map.end() || !pinauth_it->second.is_bytestring()) { |
| return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; |
| } |
| if (!pin::ProtocolVersion(*protocol).Verify( |
| pin_token, pinauth_bytes, pinauth_it->second.GetBytestring())) { |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; |
| } |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| // Like AsCBOR(const PublicKeyCredentialRpEntity&), but optionally allows name |
| // to be INVALID_UTF8. |
| base::Optional<cbor::Value> RpEntityAsCBOR( |
| const PublicKeyCredentialRpEntity& rp, |
| bool allow_invalid_utf8) { |
| if (!allow_invalid_utf8) { |
| return AsCBOR(rp); |
| } |
| |
| cbor::Value::MapValue rp_map; |
| rp_map.emplace(kEntityIdMapKey, rp.id); |
| if (rp.name) { |
| rp_map.emplace(kEntityNameMapKey, |
| cbor::Value::InvalidUTF8StringValueForTesting(*rp.name)); |
| } |
| if (rp.icon_url) { |
| rp_map.emplace(kIconUrlMapKey, rp.icon_url->spec()); |
| } |
| return cbor::Value(std::move(rp_map)); |
| } |
| |
| // Like AsCBOR(const PublicKeyCredentialUserEntity&), but optionally allows name |
| // or displayName to be INVALID_UTF8. |
| base::Optional<cbor::Value> UserEntityAsCBOR( |
| const PublicKeyCredentialUserEntity& user, |
| bool allow_invalid_utf8) { |
| if (!allow_invalid_utf8) { |
| return AsCBOR(user); |
| } |
| |
| cbor::Value::MapValue user_map; |
| user_map.emplace(kEntityIdMapKey, user.id); |
| if (user.name) { |
| user_map.emplace(kEntityNameMapKey, |
| cbor::Value::InvalidUTF8StringValueForTesting(*user.name)); |
| } |
| // Empty icon URLs result in CTAP1_ERR_INVALID_LENGTH on some security keys. |
| if (user.icon_url && !user.icon_url->is_empty()) { |
| user_map.emplace(kIconUrlMapKey, user.icon_url->spec()); |
| } |
| if (user.display_name) { |
| user_map.emplace( |
| kDisplayNameMapKey, |
| cbor::Value::InvalidUTF8StringValueForTesting(*user.display_name)); |
| } |
| return cbor::Value(std::move(user_map)); |
| } |
| |
| std::vector<uint8_t> WriteCBOR(cbor::Value value, |
| bool allow_invalid_utf8 = false) { |
| cbor::Writer::Config config; |
| config.allow_invalid_utf8_for_testing = allow_invalid_utf8; |
| return *cbor::Writer::Write(std::move(value), std::move(config)); |
| } |
| |
| std::vector<uint8_t> EncodeGetAssertionResponse( |
| const AuthenticatorGetAssertionResponse& response, |
| bool allow_invalid_utf8) { |
| cbor::Value::MapValue response_map; |
| if (response.credential()) { |
| response_map.emplace(1, AsCBOR(*response.credential())); |
| } |
| |
| response_map.emplace(2, response.auth_data().SerializeToByteArray()); |
| response_map.emplace(3, response.signature()); |
| |
| if (response.user_entity()) { |
| response_map.emplace( |
| 4, *UserEntityAsCBOR(*response.user_entity(), allow_invalid_utf8)); |
| } |
| if (response.num_credentials()) { |
| response_map.emplace(5, response.num_credentials().value()); |
| } |
| if (response.android_client_data_ext()) { |
| response_map.emplace(0xf0, |
| cbor::Value(*response.android_client_data_ext())); |
| } |
| if (response.large_blob_key()) { |
| response_map.emplace(0x07, cbor::Value(*response.large_blob_key())); |
| } |
| |
| return WriteCBOR(cbor::Value(std::move(response_map)), allow_invalid_utf8); |
| } |
| |
| std::vector<uint8_t> GenerateAndEncryptToken( |
| PINUVAuthProtocol pin_protocol, |
| base::span<const uint8_t> shared_key, |
| base::span<uint8_t, 32> pin_token) { |
| RAND_bytes(pin_token.data(), pin_token.size()); |
| return pin::ProtocolVersion(pin_protocol).Encrypt(shared_key, pin_token); |
| } |
| |
| } // namespace |
| |
| VirtualCtap2Device::Config::Config() = default; |
| VirtualCtap2Device::Config::Config(const Config&) = default; |
| VirtualCtap2Device::Config& VirtualCtap2Device::Config::operator=( |
| const Config&) = default; |
| VirtualCtap2Device::Config::~Config() = default; |
| |
| VirtualCtap2Device::VirtualCtap2Device() { |
| RegenerateKeyAgreementKey(); |
| Init({ProtocolVersion::kCtap2}); |
| } |
| |
| VirtualCtap2Device::VirtualCtap2Device(scoped_refptr<State> state, |
| const Config& config) |
| : VirtualFidoDevice(std::move(state)), config_(config) { |
| RegenerateKeyAgreementKey(); |
| |
| Init({ProtocolVersion::kCtap2}); |
| std::vector<ProtocolVersion> versions = {ProtocolVersion::kCtap2}; |
| if (config.u2f_support) { |
| versions.emplace_back(ProtocolVersion::kU2f); |
| u2f_device_ = std::make_unique<VirtualU2fDevice>(NewReferenceToState()); |
| } |
| Init(std::move(versions)); |
| |
| AuthenticatorSupportedOptions options; |
| bool options_updated = false; |
| if (config.pin_support) { |
| options_updated = true; |
| |
| if (mutable_state()->pin.empty()) { |
| options.client_pin_availability = AuthenticatorSupportedOptions:: |
| ClientPinAvailability::kSupportedButPinNotSet; |
| } else { |
| options.client_pin_availability = AuthenticatorSupportedOptions:: |
| ClientPinAvailability::kSupportedAndPinSet; |
| } |
| } |
| |
| if (config.internal_uv_support) { |
| options_updated = true; |
| if (mutable_state()->fingerprints_enrolled) { |
| options.user_verification_availability = AuthenticatorSupportedOptions:: |
| UserVerificationAvailability::kSupportedAndConfigured; |
| } else { |
| options.user_verification_availability = AuthenticatorSupportedOptions:: |
| UserVerificationAvailability::kSupportedButNotConfigured; |
| } |
| } |
| |
| options.supports_pin_uv_auth_token = config.pin_uv_auth_token_support; |
| DCHECK(!options.supports_pin_uv_auth_token || |
| SupportsAtLeast(Ctap2Version::kCtap2_1)); |
| |
| if (config.resident_key_support) { |
| options_updated = true; |
| options.supports_resident_key = true; |
| } |
| |
| if (config.credential_management_support) { |
| options_updated = true; |
| options.supports_credential_management = true; |
| options.supports_credential_management_preview = true; |
| } |
| |
| if (config.bio_enrollment_support) { |
| options_updated = true; |
| if (mutable_state()->bio_enrollment_provisioned) { |
| options.bio_enrollment_availability = AuthenticatorSupportedOptions:: |
| BioEnrollmentAvailability::kSupportedAndProvisioned; |
| } else { |
| options.bio_enrollment_availability = AuthenticatorSupportedOptions:: |
| BioEnrollmentAvailability::kSupportedButUnprovisioned; |
| } |
| } |
| |
| if (config.bio_enrollment_preview_support) { |
| options_updated = true; |
| if (mutable_state()->bio_enrollment_provisioned) { |
| options.bio_enrollment_availability_preview = |
| AuthenticatorSupportedOptions::BioEnrollmentAvailability:: |
| kSupportedAndProvisioned; |
| } else { |
| options.bio_enrollment_availability_preview = |
| AuthenticatorSupportedOptions::BioEnrollmentAvailability:: |
| kSupportedButUnprovisioned; |
| } |
| } |
| |
| if (config.is_platform_authenticator) { |
| options_updated = true; |
| options.is_platform_device = true; |
| } |
| |
| if (config.cred_protect_support) { |
| options_updated = true; |
| options.default_cred_protect = config.default_cred_protect; |
| } |
| |
| if (config.support_android_client_data_extension) { |
| options_updated = true; |
| options.supports_android_client_data_ext = true; |
| } |
| |
| if (config.support_enterprise_attestation) { |
| options_updated = true; |
| options.enterprise_attestation = true; |
| } |
| |
| if (config.large_blob_support) { |
| DCHECK(config.resident_key_support); |
| DCHECK(SupportsAtLeast(Ctap2Version::kCtap2_1)); |
| DCHECK((!config.pin_support && !config.internal_uv_support) || |
| config.pin_uv_auth_token_support) |
| << "PinUvAuthToken support is required to write large blobs for " |
| "uv-enabled authenticators"; |
| options_updated = true; |
| options.supports_large_blobs = true; |
| device_info_->max_serialized_large_blob_array = |
| config.available_large_blob_storage; |
| } |
| |
| if (config.always_uv) { |
| DCHECK(config.pin_support || config.internal_uv_support); |
| options_updated = true; |
| options.always_uv = true; |
| } |
| |
| if (options_updated) { |
| device_info_->options = std::move(options); |
| } |
| |
| std::vector<std::string> extensions; |
| |
| if (config.cred_protect_support) { |
| extensions.emplace_back(device::kExtensionCredProtect); |
| } |
| |
| if (config.hmac_secret_support) { |
| extensions.emplace_back(device::kExtensionHmacSecret); |
| } |
| |
| if (config.support_android_client_data_extension) { |
| extensions.emplace_back(device::kExtensionAndroidClientData); |
| } |
| |
| if (config.large_blob_support) { |
| extensions.emplace_back(device::kExtensionLargeBlobKey); |
| } |
| |
| if (!extensions.empty()) { |
| device_info_->extensions.emplace(std::move(extensions)); |
| } |
| |
| if (config.max_credential_count_in_list > 0) { |
| device_info_->max_credential_count_in_list = |
| config.max_credential_count_in_list; |
| } |
| |
| if (config.max_credential_id_length > 0) { |
| device_info_->max_credential_id_length = config.max_credential_id_length; |
| } |
| |
| if (config.support_invalid_for_testing_algorithm) { |
| device_info_->algorithms.push_back( |
| static_cast<int32_t>(CoseAlgorithmIdentifier::kInvalidForTesting)); |
| } |
| |
| if (config.pin_support || config.pin_uv_auth_token_support) { |
| device_info_->pin_protocols = |
| base::flat_set<PINUVAuthProtocol>{config.pin_protocol}; |
| } |
| |
| if (config.resident_key_support && SupportsAtLeast(Ctap2Version::kCtap2_1)) { |
| device_info_->remaining_discoverable_credentials = |
| remaining_resident_credentials(); |
| } |
| |
| if (config.min_pin_length_support) { |
| DCHECK(config.pin_support); |
| DCHECK(config.pin_uv_auth_token_support); |
| device_info_->min_pin_length = mutable_state()->min_pin_length; |
| device_info_->force_pin_change = mutable_state()->force_pin_change; |
| } |
| } |
| |
| VirtualCtap2Device::~VirtualCtap2Device() = default; |
| |
| void VirtualCtap2Device::SetPin(std::string pin) { |
| DCHECK_NE( |
| device_info_->options.client_pin_availability, |
| AuthenticatorSupportedOptions::ClientPinAvailability::kNotSupported); |
| DCHECK_GE(pin.size(), mutable_state()->min_pin_length); |
| mutable_state()->pin = std::move(pin); |
| mutable_state()->pin_retries = device::kMaxPinRetries; |
| device_info_->options.client_pin_availability = |
| AuthenticatorSupportedOptions::ClientPinAvailability::kSupportedAndPinSet; |
| } |
| |
| void VirtualCtap2Device::SetForcePinChange(bool force_pin_change) { |
| DCHECK(config_.min_pin_length_support); |
| mutable_state()->force_pin_change = force_pin_change; |
| device_info_->force_pin_change = force_pin_change; |
| } |
| |
| void VirtualCtap2Device::SetMinPinLength(uint32_t min_pin_length) { |
| DCHECK(config_.min_pin_length_support); |
| mutable_state()->min_pin_length = min_pin_length; |
| device_info_->min_pin_length = min_pin_length; |
| } |
| |
| // As all operations for VirtualCtap2Device are synchronous and we do not wait |
| // for user touch, Cancel command is no-op. |
| void VirtualCtap2Device::Cancel(CancelToken) {} |
| |
| FidoDevice::CancelToken VirtualCtap2Device::DeviceTransact( |
| std::vector<uint8_t> command, |
| DeviceCallback cb) { |
| if (command.empty()) { |
| ReturnCtap2Response(std::move(cb), CtapDeviceResponseCode::kCtap2ErrOther); |
| return 0; |
| } |
| |
| auto cmd_type = command[0]; |
| // The CTAP2 commands start at one, so a "command" of zero indicates that this |
| // is a U2F message. |
| if (cmd_type == 0 && config_.u2f_support) { |
| if (config_.always_uv && !mutable_state()->fingerprints_enrolled) { |
| // The U2F_REGISTER and U2F_AUTHENTICATE commands MUST immediately fail |
| // and return SW_COMMAND_NOT_ALLOWED if the alwaysUv option is true and |
| // the device is not protected by a built-in user verification method. |
| // Have the authenticator will just fail all u2f requests for simplicity. |
| NOTREACHED(); |
| std::move(cb).Run( |
| apdu::ApduResponse({}, |
| apdu::ApduResponse::Status::SW_COMMAND_NOT_ALLOWED) |
| .GetEncodedResponse()); |
| return 0; |
| } |
| u2f_device_->DeviceTransact(std::move(command), std::move(cb)); |
| return 0; |
| } |
| |
| const CtapRequestCommand ctap_command = |
| static_cast<CtapRequestCommand>(cmd_type); |
| if (config_.override_response_map.contains(ctap_command)) { |
| ReturnCtap2Response(std::move(cb), |
| config_.override_response_map.at(ctap_command), {}); |
| return 0; |
| } |
| |
| const auto request_bytes = base::make_span(command).subspan(1); |
| CtapDeviceResponseCode response_code = |
| CtapDeviceResponseCode::kCtap1ErrInvalidCommand; |
| std::vector<uint8_t> response_data; |
| |
| switch (ctap_command) { |
| case CtapRequestCommand::kAuthenticatorGetInfo: |
| if (!request_bytes.empty()) { |
| ReturnCtap2Response(std::move(cb), |
| CtapDeviceResponseCode::kCtap2ErrOther); |
| return 0; |
| } |
| |
| response_code = OnAuthenticatorGetInfo(&response_data); |
| break; |
| case CtapRequestCommand::kAuthenticatorMakeCredential: { |
| auto opt_response_code = OnMakeCredential(request_bytes, &response_data); |
| if (!opt_response_code) { |
| // Simulate timeout due to unresponded User Presence check. |
| return 0; |
| } |
| response_code = *opt_response_code; |
| break; |
| } |
| case CtapRequestCommand::kAuthenticatorGetAssertion: { |
| auto opt_response_code = OnGetAssertion(request_bytes, &response_data); |
| if (!opt_response_code) { |
| // Simulate timeout due to unresponded User Presence check. |
| return 0; |
| } |
| response_code = *opt_response_code; |
| break; |
| } |
| case CtapRequestCommand::kAuthenticatorGetNextAssertion: |
| response_code = OnGetNextAssertion(request_bytes, &response_data); |
| break; |
| case CtapRequestCommand::kAuthenticatorClientPin: { |
| auto opt_response_code = OnPINCommand(request_bytes, &response_data); |
| if (!opt_response_code) { |
| // Simulate timeout due to unresponded User Presence check. |
| return 0; |
| } |
| response_code = *opt_response_code; |
| break; |
| } |
| case CtapRequestCommand::kAuthenticatorCredentialManagement: |
| case CtapRequestCommand::kAuthenticatorCredentialManagementPreview: |
| response_code = OnCredentialManagement(request_bytes, &response_data); |
| break; |
| case CtapRequestCommand::kAuthenticatorBioEnrollment: |
| case CtapRequestCommand::kAuthenticatorBioEnrollmentPreview: |
| response_code = OnBioEnrollment(request_bytes, &response_data); |
| break; |
| case CtapRequestCommand::kAuthenticatorSelection: |
| DCHECK(SupportsAtLeast(Ctap2Version::kCtap2_1)); |
| if (!SimulatePress()) { |
| // Simulate timeout due to unresponded User Presence check. |
| return 0; |
| } |
| response_code = CtapDeviceResponseCode::kSuccess; |
| break; |
| case CtapRequestCommand::kAuthenticatorLargeBlobs: |
| response_code = OnLargeBlobs(request_bytes, &response_data); |
| break; |
| default: |
| break; |
| } |
| |
| // Call |callback| via the |MessageLoop| because |AuthenticatorImpl| doesn't |
| // support callback hairpinning. |
| ReturnCtap2Response(std::move(cb), response_code, std::move(response_data)); |
| return 0; |
| } |
| |
| base::WeakPtr<FidoDevice> VirtualCtap2Device::GetWeakPtr() { |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| void VirtualCtap2Device::Init(std::vector<ProtocolVersion> versions) { |
| device_info_ = AuthenticatorGetInfoResponse( |
| std::move(versions), config_.ctap2_versions, kDeviceAaguid); |
| device_info_->algorithms = { |
| static_cast<int32_t>(CoseAlgorithmIdentifier::kEs256), |
| static_cast<int32_t>(CoseAlgorithmIdentifier::kEdDSA), |
| static_cast<int32_t>(CoseAlgorithmIdentifier::kRs256), |
| }; |
| } |
| |
| base::Optional<CtapDeviceResponseCode> |
| VirtualCtap2Device::CheckUserVerification( |
| bool is_make_credential, |
| const AuthenticatorGetInfoResponse& authenticator_info, |
| const std::string& rp_id, |
| const base::Optional<std::vector<uint8_t>>& pin_auth, |
| const base::Optional<PINUVAuthProtocol>& pin_protocol, |
| base::span<const uint8_t> pin_token, |
| base::span<const uint8_t> client_data_hash, |
| UserVerificationRequirement user_verification, |
| bool user_presence_required, |
| bool* out_user_verified) { |
| const AuthenticatorSupportedOptions& options = authenticator_info.options; |
| |
| // The following quotes are from the CTAP2 spec: |
| |
| // 1. "If authenticator supports clientPin and platform sends a zero length |
| // pinAuth, wait for user touch and then return either CTAP2_ERR_PIN_NOT_SET |
| // if pin is not set or CTAP2_ERR_PIN_INVALID if pin has been set." |
| const bool supports_pin = |
| options.client_pin_availability != |
| AuthenticatorSupportedOptions::ClientPinAvailability::kNotSupported; |
| if (supports_pin && pin_auth && pin_auth->empty()) { |
| if (!SimulatePress()) |
| return base::nullopt; |
| |
| switch (options.client_pin_availability) { |
| case AuthenticatorSupportedOptions::ClientPinAvailability:: |
| kSupportedAndPinSet: |
| return CtapDeviceResponseCode::kCtap2ErrPinInvalid; |
| case AuthenticatorSupportedOptions::ClientPinAvailability:: |
| kSupportedButPinNotSet: |
| return CtapDeviceResponseCode::kCtap2ErrPinNotSet; |
| case AuthenticatorSupportedOptions::ClientPinAvailability::kNotSupported: |
| NOTREACHED(); |
| } |
| } |
| const base::Optional<base::flat_set<PINUVAuthProtocol>>& |
| supported_pin_protocols = authenticator_info.pin_protocols; |
| DCHECK(!supports_pin || |
| (supported_pin_protocols && !supported_pin_protocols->empty())); |
| |
| // 2. "If authenticator supports clientPin and pinAuth parameter is present |
| // and the pinProtocol is not supported, return CTAP2_ERR_PIN_AUTH_INVALID |
| // error." |
| if (supports_pin && pin_auth && |
| (!pin_protocol || |
| !base::Contains(*supported_pin_protocols, *pin_protocol))) { |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; |
| } |
| |
| const bool can_do_uv = |
| options.user_verification_availability == |
| AuthenticatorSupportedOptions::UserVerificationAvailability:: |
| kSupportedAndConfigured || |
| options.client_pin_availability == |
| AuthenticatorSupportedOptions::ClientPinAvailability:: |
| kSupportedAndPinSet; |
| |
| // (CTAP2.1) 5. "If the alwaysUv option ID is present and true and the "up" |
| // option is present and true then:" |
| if (options.always_uv && user_presence_required) { |
| // 5.1 "If the authenticator is not protected by some form of user |
| // verification:" |
| if (!can_do_uv) { |
| // 5.1.1 "If the clientPin option ID is present: (clientPin is supported)" |
| if (options.client_pin_availability == |
| AuthenticatorSupportedOptions::ClientPinAvailability:: |
| kSupportedAndPinSet) { |
| return CtapDeviceResponseCode::kCtap2ErrPinRequired; |
| } else { |
| return CtapDeviceResponseCode::kCtap2ErrOperationDenied; |
| } |
| } |
| // 5.4 "If the "uv" option is false and the authenticator supports a |
| // built-in user verification method, and the user verification method is |
| // enabled then:" |
| if (user_verification == UserVerificationRequirement::kDiscouraged && |
| options.user_verification_availability == |
| AuthenticatorSupportedOptions::UserVerificationAvailability:: |
| kSupportedAndConfigured) { |
| user_verification = UserVerificationRequirement::kRequired; |
| } |
| // 5.5 "If the clientPin option ID is present and the pinUvAuthParam |
| // parameter is not present, then end the operation by returning |
| // CTAP2_ERR_PIN_REQUIRED." |
| if (options.client_pin_availability != |
| AuthenticatorSupportedOptions::ClientPinAvailability:: |
| kNotSupported && |
| !pin_auth) { |
| return CtapDeviceResponseCode::kCtap2ErrPinRequired; |
| } |
| } |
| |
| // 3. "If authenticator is not protected by some form of user verification and |
| // platform has set "uv" or pinAuth to get the user verification, return |
| // CTAP2_ERR_INVALID_OPTION." |
| if (!can_do_uv && |
| (user_verification == UserVerificationRequirement::kRequired || |
| pin_auth)) { |
| return CtapDeviceResponseCode::kCtap2ErrInvalidOption; |
| } |
| |
| // "If authenticator is protected by some form of user verification:" |
| bool uv = false; |
| if (can_do_uv) { |
| // "If the request is passed with "uv" option, use built-in user |
| // verification method and verify the user." |
| if (user_verification == UserVerificationRequirement::kRequired) { |
| if (options.user_verification_availability == |
| AuthenticatorSupportedOptions::UserVerificationAvailability:: |
| kSupportedAndConfigured) { |
| if (!SimulatePress()) |
| return base::nullopt; |
| |
| if (!config_.user_verification_succeeds) { |
| if (is_make_credential) |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; |
| return CtapDeviceResponseCode::kCtap2ErrOperationDenied; |
| } |
| uv = true; |
| } else { |
| // UV was requested, but either not supported or not configured. |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; |
| } |
| } |
| |
| // "If pinUvAuthParam parameter is present and pinUvAuthProtocol is 1". |
| if (pin_auth && (options.client_pin_availability == |
| AuthenticatorSupportedOptions::ClientPinAvailability:: |
| kSupportedAndPinSet || |
| options.supports_pin_uv_auth_token)) { |
| DCHECK(pin_protocol); |
| |
| // "Verify that the pinUvAuthToken has the {mc,ga} permission, if not, |
| // return CTAP2_ERR_PIN_AUTH_INVALID." |
| auto permission = is_make_credential ? pin::Permissions::kMakeCredential |
| : pin::Permissions::kGetAssertion; |
| if (!(mutable_state()->pin_uv_token_permissions & |
| static_cast<uint8_t>(permission))) { |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; |
| } |
| |
| // "If the pinUvAuthToken has a permissions RPID associated and it |
| // does not match the RPID in this request, return |
| // CTAP2_ERR_PIN_AUTH_INVALID." |
| if (mutable_state()->pin_uv_token_rpid && |
| mutable_state()->pin_uv_token_rpid != rp_id) { |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; |
| } |
| |
| // "If the pinUvAuthToken does not have a permissions RPID associated, |
| // associate the request RPID as permissions RPID." |
| if (!mutable_state()->pin_uv_token_rpid) { |
| mutable_state()->pin_uv_token_rpid = rp_id; |
| } |
| |
| // Verify pinUvAuthParam. |
| if (!pin::ProtocolVersion(*pin_protocol) |
| .Verify(pin_token, client_data_hash, *pin_auth)) { |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; |
| } |
| |
| uv = true; |
| } |
| |
| if (is_make_credential && !uv) { |
| return CtapDeviceResponseCode::kCtap2ErrPinRequired; |
| } |
| } |
| |
| *out_user_verified = uv; |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnMakeCredential( |
| base::span<const uint8_t> request_bytes, |
| std::vector<uint8_t>* response) { |
| const auto& cbor_request = cbor::Reader::Read(request_bytes); |
| if (!cbor_request || !cbor_request->is_map()) { |
| DLOG(ERROR) << "Incorrectly formatted MakeCredential request."; |
| return CtapDeviceResponseCode::kCtap2ErrOther; |
| } |
| |
| CtapMakeCredentialRequest::ParseOpts parse_opts; |
| parse_opts.reject_all_extensions = config_.reject_all_extensions; |
| auto opt_request = |
| CtapMakeCredentialRequest::Parse(cbor_request->GetMap(), parse_opts); |
| if (!opt_request) { |
| DLOG(ERROR) << "Incorrectly formatted MakeCredential request."; |
| return CtapDeviceResponseCode::kCtap2ErrOther; |
| } |
| CtapMakeCredentialRequest request = std::move(*opt_request); |
| |
| bool user_verified; |
| const base::Optional<CtapDeviceResponseCode> uv_error = CheckUserVerification( |
| /*is_make_credential=*/true, *device_info_, request.rp.id, |
| request.pin_auth, request.pin_protocol, mutable_state()->pin_token, |
| request.client_data_hash, request.user_verification, |
| /*user_presence_required=*/true, &user_verified); |
| if (uv_error != CtapDeviceResponseCode::kSuccess) { |
| return uv_error; |
| } |
| |
| // 6. Check for already registered credentials. |
| const auto rp_id_hash = fido_parsing_utils::CreateSHA256Hash(request.rp.id); |
| if ((config_.reject_large_allow_and_exclude_lists && |
| request.exclude_list.size() > 1) || |
| (config_.max_credential_count_in_list && |
| request.exclude_list.size() > config_.max_credential_count_in_list)) { |
| return CtapDeviceResponseCode::kCtap2ErrLimitExceeded; |
| } |
| |
| for (const auto& excluded_credential : request.exclude_list) { |
| if (0 < config_.max_credential_id_length && |
| config_.max_credential_id_length < excluded_credential.id().size()) { |
| return CtapDeviceResponseCode::kCtap2ErrLimitExceeded; |
| } |
| const RegistrationData* found = |
| FindRegistrationData(excluded_credential.id(), rp_id_hash); |
| if (found) { |
| if (found->protection == device::CredProtect::kUVRequired && |
| !user_verified) { |
| // Cannot disclose the existence of this credential without UV. If |
| // a credentials ends up being created it'll overwrite this one. |
| continue; |
| } |
| if (!SimulatePress()) { |
| return base::nullopt; |
| } |
| return CtapDeviceResponseCode::kCtap2ErrCredentialExcluded; |
| } |
| } |
| |
| // Step 7. |
| std::unique_ptr<PrivateKey> private_key; |
| for (const auto& param : |
| request.public_key_credential_params.public_key_credential_params()) { |
| switch (param.algorithm) { |
| default: |
| continue; |
| case static_cast<int32_t>(CoseAlgorithmIdentifier::kEs256): |
| private_key = PrivateKey::FreshP256Key(); |
| break; |
| case static_cast<int32_t>(CoseAlgorithmIdentifier::kRs256): |
| private_key = PrivateKey::FreshRSAKey(); |
| break; |
| case static_cast<int32_t>(CoseAlgorithmIdentifier::kEdDSA): |
| private_key = PrivateKey::FreshEd25519Key(); |
| break; |
| case static_cast<int32_t>(CoseAlgorithmIdentifier::kInvalidForTesting): |
| if (!config_.support_invalid_for_testing_algorithm) { |
| continue; |
| } |
| private_key = PrivateKey::FreshInvalidForTestingKey(); |
| break; |
| } |
| break; |
| } |
| |
| if (!private_key) { |
| DLOG(ERROR) << "Virtual CTAP2 device does not support any public-key " |
| "algorithm listed in the request"; |
| return CtapDeviceResponseCode::kCtap2ErrUnsupportedAlgorithm; |
| } |
| std::unique_ptr<PublicKey> public_key(private_key->GetPublicKey()); |
| |
| // Step 8. |
| if ((request.resident_key_required && |
| !device_info_->options.supports_resident_key) || |
| !device_info_->options.supports_user_presence) { |
| return CtapDeviceResponseCode::kCtap2ErrUnsupportedOption; |
| } |
| |
| // Step 10. Simulate a press unless the user has been verified by internal |
| // user verification. |
| if ((!user_verified || request.user_verification == |
| UserVerificationRequirement::kDiscouraged) && |
| !SimulatePress()) { |
| return base::nullopt; |
| } |
| |
| // Our key handles are simple hashes of the public key. |
| const auto key_handle = crypto::SHA256Hash(public_key->cose_key_bytes); |
| |
| base::Optional<cbor::Value> extensions; |
| cbor::Value::MapValue extensions_map; |
| if (request.hmac_secret) { |
| if (!config_.hmac_secret_support) { |
| // Should not have been sent. Authenticators will normally ignore unknown |
| // extensions but Chromium should not make this mistake. |
| DLOG(ERROR) |
| << "Rejecting makeCredential due to unexpected hmac_secret extension"; |
| return CtapDeviceResponseCode::kCtap2ErrUnsupportedExtension; |
| } |
| extensions_map.emplace(cbor::Value(kExtensionHmacSecret), |
| cbor::Value(true)); |
| } |
| |
| CredProtect cred_protect = config_.default_cred_protect; |
| if (request.cred_protect) { |
| cred_protect = *request.cred_protect; |
| } |
| if (config_.force_cred_protect) { |
| cred_protect = *config_.force_cred_protect; |
| } |
| |
| if (request.cred_protect || |
| cred_protect != device::CredProtect::kUVOptional) { |
| extensions_map.emplace(cbor::Value(kExtensionCredProtect), |
| cbor::Value(static_cast<int64_t>(cred_protect))); |
| } |
| |
| if (request.large_blob_key) { |
| if (!config_.large_blob_support) { |
| DLOG(ERROR) << "Rejecting makeCredential due to unexpected largeBlobKey " |
| "extension"; |
| return CtapDeviceResponseCode::kCtap2ErrUnsupportedExtension; |
| } |
| if (!request.resident_key_required) { |
| DLOG(ERROR) |
| << "largeBlobKey is not supported for non resident credentials"; |
| return CtapDeviceResponseCode::kCtap2ErrInvalidOption; |
| } |
| } |
| |
| if (config_.add_extra_extension) { |
| extensions_map.emplace(cbor::Value("unsolicited"), cbor::Value(42)); |
| } |
| |
| if (!extensions_map.empty()) { |
| extensions = cbor::Value(std::move(extensions_map)); |
| } |
| |
| AuthenticatorData authenticator_data( |
| rp_id_hash, /*user_present=*/true, user_verified, 01ul, |
| ConstructAttestedCredentialData(key_handle, std::move(public_key)), |
| std::move(extensions)); |
| |
| base::Optional<std::string> opt_android_client_data_json; |
| if (request.android_client_data_ext && |
| config_.support_android_client_data_extension) { |
| opt_android_client_data_json.emplace(ConstructAndroidClientDataJSON( |
| *request.android_client_data_ext, "webauthn.create")); |
| } |
| |
| auto sign_buffer = ConstructSignatureBuffer( |
| authenticator_data, |
| opt_android_client_data_json |
| ? fido_parsing_utils::CreateSHA256Hash(*opt_android_client_data_json) |
| : request.client_data_hash); |
| |
| // Sign with attestation key. |
| // Note: Non-deterministic, you need to mock this out if you rely on |
| // deterministic behavior. |
| std::vector<uint8_t> sig; |
| std::unique_ptr<crypto::ECPrivateKey> attestation_private_key = |
| crypto::ECPrivateKey::CreateFromPrivateKeyInfo(GetAttestationKey()); |
| bool status = |
| Sign(attestation_private_key.get(), std::move(sign_buffer), &sig); |
| DCHECK(status); |
| |
| base::Optional<std::vector<uint8_t>> attestation_cert; |
| bool enterprise_attestation_requested = false; |
| if (!mutable_state()->self_attestation) { |
| if (config_.support_enterprise_attestation) { |
| switch (request.attestation_preference) { |
| case AttestationConveyancePreference:: |
| kEnterpriseIfRPListedOnAuthenticator: |
| if (base::Contains(config_.enterprise_attestation_rps, |
| request.rp.id)) { |
| enterprise_attestation_requested = true; |
| } |
| break; |
| case AttestationConveyancePreference::kEnterpriseApprovedByBrowser: |
| enterprise_attestation_requested = true; |
| break; |
| default: |
| enterprise_attestation_requested = false; |
| } |
| } |
| if (config_.always_return_enterprise_attestation) { |
| enterprise_attestation_requested = true; |
| } |
| attestation_cert = |
| GenerateAttestationCertificate(enterprise_attestation_requested); |
| if (!attestation_cert) { |
| DLOG(ERROR) << "Failed to generate attestation certificate."; |
| return CtapDeviceResponseCode::kCtap2ErrOther; |
| } |
| } |
| |
| base::Optional<std::vector<uint8_t>> opt_android_client_data_ext; |
| if (opt_android_client_data_json) { |
| opt_android_client_data_ext.emplace(); |
| fido_parsing_utils::Append( |
| &*opt_android_client_data_ext, |
| base::make_span(reinterpret_cast<const uint8_t*>( |
| opt_android_client_data_json->data()), |
| opt_android_client_data_json->size())); |
| } else if (config_.send_unsolicited_android_client_data_extension) { |
| const std::string client_data_json = |
| "{\"challenge\":" |
| "\"ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnBZWFFpT2pFMU" |
| "9EYzNOamMxTnpJc0ltVjRjQ0k2TVRVNE56ZzROelUzTWl3aWMzVmlJam9pWkdaa1ptY2" |
| "lmUS5FdFFyUXNSWE9qNlpkMGFseXVkUzF3X3FORjJSbElZdTNfb0NvTDRzbWI4\"," |
| "\"origin\":" + |
| base::GetQuotedJSONString("https://" + request.rp.id) + |
| ",\"type\":\"webauthn.create\",\"androidPackageName\":\"org.chromium." |
| "device.fido.test\"}"; |
| opt_android_client_data_ext.emplace(); |
| fido_parsing_utils::Append(&*opt_android_client_data_ext, |
| base::make_span(reinterpret_cast<const uint8_t*>( |
| client_data_json.data()), |
| client_data_json.size())); |
| } |
| |
| base::Optional<std::array<uint8_t, kLargeBlobKeyLength>> large_blob_key; |
| if (request.large_blob_key) { |
| large_blob_key.emplace(); |
| RAND_bytes(large_blob_key->data(), large_blob_key->size()); |
| } |
| |
| *response = ConstructMakeCredentialResponse( |
| std::move(attestation_cert), sig, std::move(authenticator_data), |
| std::move(opt_android_client_data_ext), enterprise_attestation_requested, |
| large_blob_key); |
| RegistrationData registration(std::move(private_key), rp_id_hash, |
| 1 /* signature counter */); |
| |
| if (request.resident_key_required) { |
| // If there's already a registration for this RP and user ID, delete it. |
| for (const auto& registration : mutable_state()->registrations) { |
| if (registration.second.is_resident && |
| rp_id_hash == registration.second.application_parameter && |
| registration.second.user->id == request.user.id) { |
| mutable_state()->registrations.erase(registration.first); |
| break; |
| } |
| } |
| |
| if (remaining_resident_credentials() == 0) { |
| return CtapDeviceResponseCode::kCtap2ErrKeyStoreFull; |
| } |
| |
| registration.is_resident = true; |
| registration.user = request.user; |
| registration.rp = request.rp; |
| } |
| |
| registration.protection = cred_protect; |
| |
| if (request.hmac_secret) { |
| registration.hmac_key.emplace(); |
| RAND_bytes(registration.hmac_key->first.data(), |
| registration.hmac_key->first.size()); |
| RAND_bytes(registration.hmac_key->second.data(), |
| registration.hmac_key->second.size()); |
| } |
| |
| registration.large_blob_key = std::move(large_blob_key); |
| |
| StoreNewKey(key_handle, std::move(registration)); |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnGetAssertion( |
| base::span<const uint8_t> request_bytes, |
| std::vector<uint8_t>* response) { |
| // Step numbers in this function refer to |
| // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorGetAssertion |
| const auto& cbor_request = cbor::Reader::Read(request_bytes); |
| if (!cbor_request || !cbor_request->is_map()) { |
| DLOG(ERROR) << "Incorrectly formatted MakeCredential request."; |
| return CtapDeviceResponseCode::kCtap2ErrOther; |
| } |
| |
| const auto& request_map = cbor_request->GetMap(); |
| CtapGetAssertionRequest::ParseOpts parse_opts; |
| parse_opts.reject_all_extensions = config_.reject_all_extensions; |
| auto opt_request = CtapGetAssertionRequest::Parse(request_map, parse_opts); |
| if (!opt_request) { |
| DLOG(ERROR) << "Incorrectly formatted GetAssertion request."; |
| return CtapDeviceResponseCode::kCtap2ErrOther; |
| } |
| CtapGetAssertionRequest request = std::move(*opt_request); |
| |
| mutable_state()->allow_list_sizes.push_back(request.allow_list.size()); |
| |
| bool user_verified; |
| const base::Optional<CtapDeviceResponseCode> uv_error = CheckUserVerification( |
| /*is_make_credential=*/false, *device_info_, request.rp_id, |
| request.pin_auth, request.pin_protocol, mutable_state()->pin_token, |
| request.client_data_hash, request.user_verification, |
| request.user_presence_required, &user_verified); |
| if (uv_error != CtapDeviceResponseCode::kSuccess) { |
| return uv_error; |
| } |
| |
| if (!config_.resident_key_support && request.allow_list.empty()) { |
| return CtapDeviceResponseCode::kCtap2ErrNoCredentials; |
| } |
| |
| const auto rp_id_hash = fido_parsing_utils::CreateSHA256Hash(request.rp_id); |
| |
| std::vector<std::pair<base::span<const uint8_t>, RegistrationData*>> |
| found_registrations; |
| |
| if (!request.user_presence_required && |
| config_.reject_silent_authentication_requests) { |
| return CtapDeviceResponseCode::kCtap2ErrUnsupportedOption; |
| } |
| |
| if ((config_.reject_large_allow_and_exclude_lists && |
| request.allow_list.size() > 1) || |
| (config_.max_credential_count_in_list && |
| request.allow_list.size() > config_.max_credential_count_in_list)) { |
| return CtapDeviceResponseCode::kCtap2ErrLimitExceeded; |
| } |
| |
| for (const auto& allowed_credential : request.allow_list) { |
| if (0 < config_.max_credential_id_length && |
| config_.max_credential_id_length < allowed_credential.id().size()) { |
| return CtapDeviceResponseCode::kCtap2ErrLimitExceeded; |
| } |
| RegistrationData* registration = |
| FindRegistrationData(allowed_credential.id(), rp_id_hash); |
| if (registration && |
| !(registration->is_u2f && config_.ignore_u2f_credentials)) { |
| found_registrations.emplace_back(allowed_credential.id(), registration); |
| break; |
| } |
| } |
| |
| // CTAP 2.1 prohibits an empty (but present) allow_list. In CTAP 2.0, it is |
| // technically permissible to send an empty allow_list when asking for |
| // discoverable credentials, but some authenticators in practice don't take it |
| // that way. Thus this code mirrors that to better reflect reality. |
| if (request_map.find(cbor::Value(3)) == request_map.end()) { |
| DCHECK(config_.resident_key_support); |
| for (auto& registration : mutable_state()->registrations) { |
| if (registration.second.is_resident && |
| registration.second.application_parameter == rp_id_hash) { |
| DCHECK(!registration.second.is_u2f); |
| found_registrations.emplace_back(registration.first, |
| ®istration.second); |
| } |
| } |
| } |
| |
| // Enforce credProtect semantics. |
| found_registrations.erase( |
| std::remove_if( |
| found_registrations.begin(), found_registrations.end(), |
| [user_verified, &request]( |
| const std::pair<base::span<const uint8_t>, RegistrationData*>& |
| candidate) -> bool { |
| switch (candidate.second->protection) { |
| case CredProtect::kUVOptional: |
| return false; |
| case CredProtect::kUVOrCredIDRequired: |
| return request.allow_list.empty() && !user_verified; |
| case CredProtect::kUVRequired: |
| return !user_verified; |
| } |
| }), |
| found_registrations.end()); |
| |
| if (config_.return_immediate_invalid_credential_error && |
| found_registrations.empty()) { |
| return CtapDeviceResponseCode::kCtap2ErrInvalidCredential; |
| } |
| |
| // Step 5. |
| if (!device_info_->options.supports_user_presence && |
| request.user_presence_required) { |
| return CtapDeviceResponseCode::kCtap2ErrUnsupportedOption; |
| } |
| |
| // Step 7. |
| if (request.user_presence_required && |
| (!user_verified || request.user_verification == |
| UserVerificationRequirement::kDiscouraged) && |
| !SimulatePress()) { |
| return base::nullopt; |
| } |
| |
| // Step 8. |
| if (found_registrations.empty()) { |
| return CtapDeviceResponseCode::kCtap2ErrNoCredentials; |
| } |
| |
| base::Optional<std::array<uint8_t, SHA256_DIGEST_LENGTH>> hmac_shared_key; |
| base::Optional<std::array<uint8_t, 32>> hmac_salt1; |
| base::Optional<std::array<uint8_t, 32>> hmac_salt2; |
| |
| if (request.hmac_secret) { |
| if (!mutable_state()->ecdh_key) { |
| // Platform did not fetch the authenticator ECDH key first. |
| NOTREACHED(); |
| return CtapDeviceResponseCode::kCtap2ErrMissingParameter; |
| } |
| if (!request.pin_protocol) { |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; |
| } |
| const pin::Protocol& pin_protocol = |
| pin::ProtocolVersion(*request.pin_protocol); |
| |
| const auto& x962 = request.hmac_secret->public_key_x962; |
| bssl::UniquePtr<EC_GROUP> p256( |
| EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)); |
| bssl::UniquePtr<EC_POINT> platform_point(EC_POINT_new(p256.get())); |
| if (!EC_POINT_oct2point(p256.get(), platform_point.get(), x962.data(), |
| x962.size(), /*ctx=*/nullptr)) { |
| NOTREACHED(); |
| return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; |
| } |
| |
| std::vector<uint8_t> shared_key = pin_protocol.CalculateSharedKey( |
| mutable_state()->ecdh_key.get(), platform_point.get()); |
| |
| const auto& encrypted_salts = request.hmac_secret->encrypted_salts; |
| if (encrypted_salts.size() != 32 && encrypted_salts.size() != 64) { |
| NOTREACHED(); |
| return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; |
| } |
| |
| std::vector<uint8_t> salts = |
| pin_protocol.Decrypt(shared_key, encrypted_salts); |
| CHECK_EQ(salts.size(), encrypted_salts.size()); |
| |
| if (pin_protocol.Authenticate(shared_key, encrypted_salts) != |
| request.hmac_secret->salts_auth) { |
| NOTREACHED(); |
| return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; |
| } |
| |
| hmac_salt1.emplace(); |
| memcpy(hmac_salt1->data(), salts.data(), hmac_salt1->size()); |
| if (encrypted_salts.size() == 64) { |
| hmac_salt2.emplace(); |
| memcpy(hmac_salt2->data(), salts.data() + hmac_salt1->size(), |
| hmac_salt2->size()); |
| } |
| |
| hmac_shared_key.emplace(); |
| CHECK_EQ(hmac_shared_key->size(), shared_key.size()); |
| memcpy(hmac_shared_key->data(), shared_key.data(), shared_key.size()); |
| } |
| |
| // This implementation does not sort credentials by creation time as the spec |
| // requires. |
| |
| mutable_state()->pending_assertions.clear(); |
| bool done_first = false; |
| for (const auto& registration : found_registrations) { |
| registration.second->counter++; |
| |
| base::Optional<AttestedCredentialData> opt_attested_cred_data; |
| if (config_.return_attested_cred_data_in_get_assertion_response) { |
| opt_attested_cred_data.emplace(ConstructAttestedCredentialData( |
| registration.first, |
| registration.second->private_key->GetPublicKey())); |
| } |
| |
| cbor::Value::MapValue extensions_map; |
| if (config_.add_extra_extension) { |
| extensions_map.emplace(cbor::Value("unsolicited"), cbor::Value(42)); |
| } |
| |
| if (hmac_salt1 && registration.second->hmac_key) { |
| const std::pair<std::array<uint8_t, 32>, std::array<uint8_t, 32>>& |
| hmac_keys = *registration.second->hmac_key; |
| const std::array<uint8_t, 32>& hmac_key = |
| user_verified ? hmac_keys.second : hmac_keys.first; |
| |
| unsigned hmac_out_length; |
| uint8_t hmac_result[SHA256_DIGEST_LENGTH]; |
| std::vector<uint8_t> outputs; |
| |
| HMAC(EVP_sha256(), hmac_key.data(), hmac_key.size(), hmac_salt1->data(), |
| hmac_salt1->size(), hmac_result, &hmac_out_length); |
| DCHECK_EQ(hmac_out_length, sizeof(hmac_result)); |
| outputs.insert(outputs.end(), &hmac_result[0], |
| &hmac_result[sizeof(hmac_result)]); |
| |
| if (hmac_salt2) { |
| HMAC(EVP_sha256(), hmac_key.data(), hmac_key.size(), hmac_salt2->data(), |
| hmac_salt2->size(), hmac_result, &hmac_out_length); |
| DCHECK_EQ(hmac_out_length, sizeof(hmac_result)); |
| outputs.insert(outputs.end(), &hmac_result[0], |
| &hmac_result[sizeof(hmac_result)]); |
| } |
| |
| std::vector<uint8_t> encrypted_outputs = |
| pin::ProtocolVersion(*request.pin_protocol) |
| .Encrypt(*hmac_shared_key, outputs); |
| CHECK_EQ(encrypted_outputs.size(), outputs.size()); |
| |
| extensions_map.emplace(kExtensionHmacSecret, |
| std::move(encrypted_outputs)); |
| } |
| |
| base::Optional<cbor::Value> extensions; |
| if (!extensions_map.empty()) { |
| extensions.emplace(std::move(extensions_map)); |
| } |
| |
| AuthenticatorData authenticator_data( |
| rp_id_hash, request.user_presence_required, user_verified, |
| registration.second->counter, std::move(opt_attested_cred_data), |
| std::move(extensions)); |
| |
| base::Optional<std::string> opt_android_client_data_json; |
| if (request.android_client_data_ext && |
| config_.support_android_client_data_extension) { |
| opt_android_client_data_json.emplace(ConstructAndroidClientDataJSON( |
| *request.android_client_data_ext, "webauthn.get")); |
| } |
| |
| auto signature_buffer = ConstructSignatureBuffer( |
| authenticator_data, opt_android_client_data_json |
| ? fido_parsing_utils::CreateSHA256Hash( |
| *opt_android_client_data_json) |
| : request.client_data_hash); |
| |
| std::vector<uint8_t> signature; |
| if (config_.always_uv && !user_verified) { |
| // Requests without user presence and with up=0 produce bogus signatures. |
| DCHECK(!request.user_presence_required); |
| signature = |
| registration.second->private_key->Sign(std::vector<uint8_t>{0}); |
| } else { |
| signature = registration.second->private_key->Sign(signature_buffer); |
| } |
| |
| AuthenticatorGetAssertionResponse assertion( |
| std::move(authenticator_data), |
| fido_parsing_utils::Materialize(signature)); |
| |
| bool include_credential; |
| switch (config_.include_credential_in_assertion_response) { |
| case VirtualCtap2Device::Config::IncludeCredential::ONLY_IF_NEEDED: |
| include_credential = request.allow_list.size() != 1; |
| break; |
| case VirtualCtap2Device::Config::IncludeCredential::ALWAYS: |
| include_credential = true; |
| break; |
| case VirtualCtap2Device::Config::IncludeCredential::NEVER: |
| include_credential = false; |
| break; |
| } |
| |
| if (include_credential) { |
| assertion.SetCredential( |
| {CredentialType::kPublicKey, |
| fido_parsing_utils::Materialize(registration.first)}); |
| } |
| |
| if (registration.second->is_resident) { |
| assertion.SetUserEntity(registration.second->user.value()); |
| } |
| |
| if (request.large_blob_key) { |
| if (!config_.large_blob_support) { |
| return CtapDeviceResponseCode::kCtap2ErrUnsupportedExtension; |
| } |
| if (registration.second->large_blob_key) { |
| assertion.set_large_blob_key(*registration.second->large_blob_key); |
| } |
| } |
| |
| if (opt_android_client_data_json) { |
| std::vector<uint8_t> android_client_data_ext; |
| fido_parsing_utils::Append( |
| &android_client_data_ext, |
| base::make_span(reinterpret_cast<const uint8_t*>( |
| opt_android_client_data_json->data()), |
| opt_android_client_data_json->size())); |
| assertion.set_android_client_data_ext(std::move(android_client_data_ext)); |
| } else if (config_.send_unsolicited_android_client_data_extension) { |
| const std::string client_data_json = |
| "{challenge:" |
| "\"ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnBZWFFpT2pFMU" |
| "9EYzNOamMxTnpJc0ltVjRjQ0k2TVRVNE56ZzROelUzTWl3aWMzVmlJam9pWkdaa1ptY2" |
| "lmUS5FdFFyUXNSWE9qNlpkMGFseXVkUzF3X3FORjJSbElZdTNfb0NvTDRzbWI4\"," |
| "origin:\"https://" + |
| request.rp_id + "\",type:\"webauthn.get\"}"; |
| std::vector<uint8_t> android_client_data_ext; |
| fido_parsing_utils::Append( |
| &android_client_data_ext, |
| base::make_span( |
| reinterpret_cast<const uint8_t*>(client_data_json.data()), |
| client_data_json.size())); |
| assertion.set_android_client_data_ext(std::move(android_client_data_ext)); |
| } |
| |
| if (!done_first) { |
| if (found_registrations.size() > 1) { |
| DCHECK_LT(found_registrations.size(), 256u); |
| assertion.SetNumCredentials(found_registrations.size()); |
| } |
| *response = EncodeGetAssertionResponse( |
| assertion, config_.allow_invalid_utf8_in_credential_entities); |
| done_first = true; |
| } else { |
| // These replies will be returned in response to a GetNextAssertion |
| // request. |
| mutable_state()->pending_assertions.emplace_back( |
| EncodeGetAssertionResponse( |
| assertion, config_.allow_invalid_utf8_in_credential_entities)); |
| } |
| } |
| |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| CtapDeviceResponseCode VirtualCtap2Device::OnGetNextAssertion( |
| base::span<const uint8_t> request_bytes, |
| std::vector<uint8_t>* response) { |
| if (!request_bytes.empty() && !cbor::Reader::Read(request_bytes)) { |
| return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; |
| } |
| |
| auto& pending_assertions = mutable_state()->pending_assertions; |
| if (pending_assertions.empty()) { |
| return CtapDeviceResponseCode::kCtap2ErrNotAllowed; |
| } |
| |
| *response = std::move(pending_assertions.back()); |
| pending_assertions.pop_back(); |
| |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnPINCommand( |
| base::span<const uint8_t> request_bytes, |
| std::vector<uint8_t>* response) { |
| const auto& cbor_request = cbor::Reader::Read(request_bytes); |
| if (!cbor_request || !cbor_request->is_map()) { |
| return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; |
| } |
| const auto& request_map = cbor_request->GetMap(); |
| |
| const auto protocol_it = request_map.find( |
| cbor::Value(static_cast<int>(pin::RequestKey::kProtocol))); |
| if (protocol_it == request_map.end() || !protocol_it->second.is_unsigned()) { |
| return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; |
| } |
| base::Optional<PINUVAuthProtocol> pin_protocol = |
| ToPINUVAuthProtocol(protocol_it->second.GetUnsigned()); |
| if (!pin_protocol) { |
| return CtapDeviceResponseCode::kCtap1ErrInvalidCommand; |
| } |
| if (*pin_protocol != config_.pin_protocol) { |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; |
| } |
| |
| const auto subcommand_it = request_map.find( |
| cbor::Value(static_cast<int>(pin::RequestKey::kSubcommand))); |
| if (subcommand_it == request_map.end() || |
| !subcommand_it->second.is_unsigned()) { |
| return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; |
| } |
| const int64_t subcommand = subcommand_it->second.GetUnsigned(); |
| |
| if (device_info_->options.client_pin_availability == |
| AuthenticatorSupportedOptions::ClientPinAvailability::kNotSupported && |
| !config_.pin_uv_auth_token_support && |
| // hmac_secret requires the platform to fetch the key-agreement key and |
| // so, presumably, devices that support it must support at least that |
| // subcommand of PIN support too. |
| (!config_.hmac_secret_support || |
| subcommand != |
| static_cast<int>(device::pin::Subcommand::kGetKeyAgreement))) { |
| return CtapDeviceResponseCode::kCtap1ErrInvalidCommand; |
| } |
| |
| cbor::Value::MapValue response_map; |
| switch (subcommand) { |
| case static_cast<int>(device::pin::Subcommand::kGetRetries): |
| response_map.emplace(static_cast<int>(pin::ResponseKey::kRetries), |
| mutable_state()->pin_retries); |
| break; |
| |
| case static_cast<int>(device::pin::Subcommand::kGetUvRetries): |
| response_map.emplace(static_cast<int>(pin::ResponseKey::kUvRetries), |
| mutable_state()->uv_retries); |
| break; |
| |
| case static_cast<int>(device::pin::Subcommand::kGetKeyAgreement): { |
| std::array<uint8_t, kP256X962Length> x962; |
| CHECK_EQ(x962.size(), |
| EC_POINT_point2oct( |
| EC_KEY_get0_group(mutable_state()->ecdh_key.get()), |
| EC_KEY_get0_public_key(mutable_state()->ecdh_key.get()), |
| POINT_CONVERSION_UNCOMPRESSED, x962.data(), x962.size(), |
| nullptr /* BN_CTX */)); |
| |
| response_map.emplace(static_cast<int>(pin::ResponseKey::kKeyAgreement), |
| pin::EncodeCOSEPublicKey(x962)); |
| break; |
| } |
| |
| case static_cast<int>(device::pin::Subcommand::kSetPIN): { |
| const auto encrypted_pin = |
| GetPINBytestring(request_map, pin::RequestKey::kNewPINEnc); |
| const auto pin_auth = |
| GetPINBytestring(request_map, pin::RequestKey::kPINAuth); |
| const auto peer_key = |
| GetPINKey(request_map, pin::RequestKey::kKeyAgreement); |
| |
| if (!encrypted_pin || !pin_auth || !peer_key) { |
| return CtapDeviceResponseCode::kCtap2ErrMissingParameter; |
| } |
| |
| if (!mutable_state()->pin.empty()) { |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; |
| } |
| |
| if (!mutable_state()->ecdh_key) { |
| // kGetKeyAgreement should have been called first. |
| NOTREACHED(); |
| return CtapDeviceResponseCode::kCtap2ErrPinTokenExpired; |
| } |
| std::vector<uint8_t> shared_key = |
| pin::ProtocolVersion(*pin_protocol) |
| .CalculateSharedKey(mutable_state()->ecdh_key.get(), |
| peer_key->get()); |
| |
| CtapDeviceResponseCode err = |
| SetPIN(*pin_protocol, mutable_state(), shared_key, *encrypted_pin, |
| *pin_auth, /*current_encrypted_pin_hash=*/base::nullopt); |
| if (err != CtapDeviceResponseCode::kSuccess) { |
| return err; |
| } |
| |
| AuthenticatorSupportedOptions options = device_info_->options; |
| options.client_pin_availability = AuthenticatorSupportedOptions:: |
| ClientPinAvailability::kSupportedAndPinSet; |
| device_info_->options = std::move(options); |
| |
| break; |
| } |
| |
| case static_cast<int>(device::pin::Subcommand::kChangePIN): { |
| const auto encrypted_new_pin = |
| GetPINBytestring(request_map, pin::RequestKey::kNewPINEnc); |
| const auto encrypted_pin_hash = |
| GetPINBytestring(request_map, pin::RequestKey::kPINHashEnc); |
| const auto pin_auth = |
| GetPINBytestring(request_map, pin::RequestKey::kPINAuth); |
| const auto peer_key = |
| GetPINKey(request_map, pin::RequestKey::kKeyAgreement); |
| |
| if (!encrypted_pin_hash || !encrypted_new_pin || !pin_auth || !peer_key) { |
| return CtapDeviceResponseCode::kCtap2ErrMissingParameter; |
| } |
| |
| if (!mutable_state()->ecdh_key) { |
| // kGetKeyAgreement should have been called first. |
| NOTREACHED(); |
| return CtapDeviceResponseCode::kCtap2ErrPinTokenExpired; |
| } |
| std::vector<uint8_t> shared_key = |
| pin::ProtocolVersion(*pin_protocol) |
| .CalculateSharedKey(mutable_state()->ecdh_key.get(), |
| peer_key->get()); |
| |
| CtapDeviceResponseCode err = ConfirmPresentedPIN( |
| *pin_protocol, mutable_state(), shared_key, *encrypted_pin_hash); |
| if (err != CtapDeviceResponseCode::kSuccess) { |
| RegenerateKeyAgreementKey(); |
| return err; |
| } |
| |
| err = SetPIN(*pin_protocol, mutable_state(), shared_key, |
| *encrypted_new_pin, *pin_auth, encrypted_pin_hash); |
| if (err != CtapDeviceResponseCode::kSuccess) { |
| return err; |
| } |
| |
| break; |
| } |
| |
| case static_cast<int>(device::pin::Subcommand::kGetPINToken): |
| case static_cast<int>( |
| device::pin::Subcommand::kGetPinUvAuthTokenUsingPinWithPermissions): { |
| if (subcommand == |
| static_cast<int>(device::pin::Subcommand:: |
| kGetPinUvAuthTokenUsingPinWithPermissions) && |
| !config_.pin_uv_auth_token_support) { |
| return CtapDeviceResponseCode::kCtap1ErrInvalidCommand; |
| } |
| const auto encrypted_pin_hash = |
| GetPINBytestring(request_map, pin::RequestKey::kPINHashEnc); |
| const auto peer_key = |
| GetPINKey(request_map, pin::RequestKey::kKeyAgreement); |
| |
| if (!encrypted_pin_hash || !peer_key) { |
| return CtapDeviceResponseCode::kCtap2ErrMissingParameter; |
| } |
| |
| PinUvAuthTokenPermissions permissions; |
| if (subcommand == |
| static_cast<int>(device::pin::Subcommand::kGetPINToken)) { |
| if (request_map.find(cbor::Value(static_cast<int>( |
| pin::RequestKey::kPermissions))) != request_map.end() || |
| request_map.find(cbor::Value(static_cast<int>( |
| pin::RequestKey::kPermissionsRPID))) != request_map.end()) { |
| return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; |
| } |
| // Set default PinUvAuthToken permissions. |
| permissions.permissions = |
| static_cast<uint8_t>(pin::Permissions::kMakeCredential) | |
| static_cast<uint8_t>(pin::Permissions::kGetAssertion); |
| } else { |
| DCHECK_EQ( |
| subcommand, |
| static_cast<int>(device::pin::Subcommand:: |
| kGetPinUvAuthTokenUsingPinWithPermissions)); |
| CtapDeviceResponseCode response = |
| ExtractPermissions(request_map, config_, permissions); |
| if (response != CtapDeviceResponseCode::kSuccess) { |
| return response; |
| } |
| } |
| |
| if (!mutable_state()->ecdh_key) { |
| // kGetKeyAgreement should have been called first. |
| NOTREACHED(); |
| return CtapDeviceResponseCode::kCtap2ErrPinTokenExpired; |
| } |
| std::vector<uint8_t> shared_key = |
| pin::ProtocolVersion(*pin_protocol) |
| .CalculateSharedKey(mutable_state()->ecdh_key.get(), |
| peer_key->get()); |
| |
| CtapDeviceResponseCode err = ConfirmPresentedPIN( |
| *pin_protocol, mutable_state(), shared_key, *encrypted_pin_hash); |
| if (err != CtapDeviceResponseCode::kSuccess) { |
| RegenerateKeyAgreementKey(); |
| return err; |
| } |
| |
| mutable_state()->pin_retries = kMaxPinRetries; |
| |
| if (mutable_state()->force_pin_change) { |
| return subcommand == |
| static_cast<int>(device::pin::Subcommand::kGetPINToken) |
| ? CtapDeviceResponseCode::kCtap2ErrPinInvalid |
| : CtapDeviceResponseCode::kCtap2ErrPinPolicyViolation; |
| } |
| |
| mutable_state()->pin_uv_token_permissions = permissions.permissions; |
| mutable_state()->pin_uv_token_rpid = permissions.rp_id; |
| |
| response_map.emplace(static_cast<int>(pin::ResponseKey::kPINToken), |
| GenerateAndEncryptToken(*pin_protocol, shared_key, |
| mutable_state()->pin_token)); |
| break; |
| } |
| |
| case static_cast<int>(device::pin::Subcommand::kGetUvToken): { |
| const auto peer_key = |
| GetPINKey(request_map, pin::RequestKey::kKeyAgreement); |
| if (!peer_key) { |
| return CtapDeviceResponseCode::kCtap2ErrMissingParameter; |
| } |
| |
| PinUvAuthTokenPermissions permissions; |
| CtapDeviceResponseCode response = |
| ExtractPermissions(request_map, config_, permissions); |
| if (response != CtapDeviceResponseCode::kSuccess) { |
| return response; |
| } |
| |
| if (device_info_->options.user_verification_availability == |
| AuthenticatorSupportedOptions::UserVerificationAvailability:: |
| kSupportedButNotConfigured) { |
| return CtapDeviceResponseCode::kCtap2ErrNotAllowed; |
| } |
| |
| if (mutable_state()->uv_retries <= 0) { |
| return CtapDeviceResponseCode::kCtap2ErrUvBlocked; |
| } |
| |
| if (!mutable_state()->ecdh_key) { |
| // kGetKeyAgreement should have been called first. |
| NOTREACHED(); |
| return CtapDeviceResponseCode::kCtap2ErrPinTokenExpired; |
| } |
| std::vector<uint8_t> shared_key = |
| pin::ProtocolVersion(*pin_protocol) |
| .CalculateSharedKey(mutable_state()->ecdh_key.get(), |
| peer_key->get()); |
| |
| --mutable_state()->uv_retries; |
| |
| // Simulate internal UV. |
| if (!SimulatePress()) { |
| return base::nullopt; |
| } |
| if (!config_.user_verification_succeeds) { |
| return mutable_state()->uv_retries > 0 |
| ? CtapDeviceResponseCode::kCtap2ErrUvInvalid |
| : CtapDeviceResponseCode::kCtap2ErrUvBlocked; |
| } |
| |
| mutable_state()->pin_retries = kMaxPinRetries; |
| mutable_state()->uv_retries = kMaxUvRetries; |
| mutable_state()->pin_uv_token_permissions = permissions.permissions; |
| mutable_state()->pin_uv_token_rpid = permissions.rp_id; |
| |
| response_map.emplace(static_cast<int>(pin::ResponseKey::kPINToken), |
| GenerateAndEncryptToken(*pin_protocol, shared_key, |
| mutable_state()->pin_token)); |
| break; |
| } |
| |
| default: |
| return CtapDeviceResponseCode::kCtap1ErrInvalidCommand; |
| } |
| |
| *response = cbor::Writer::Write(cbor::Value(std::move(response_map))).value(); |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| CtapDeviceResponseCode VirtualCtap2Device::OnCredentialManagement( |
| base::span<const uint8_t> request_bytes, |
| std::vector<uint8_t>* response) { |
| if (!device_info_->options.supports_credential_management) { |
| return CtapDeviceResponseCode::kCtap2ErrUnsupportedOption; |
| } |
| |
| const auto& cbor_request = cbor::Reader::Read(request_bytes); |
| if (!cbor_request || !cbor_request->is_map()) { |
| return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; |
| } |
| const auto& request_map = cbor_request->GetMap(); |
| const auto subcommand_it = request_map.find(cbor::Value( |
| static_cast<int>(CredentialManagementRequestKey::kSubCommand))); |
| if (subcommand_it == request_map.end() || |
| !subcommand_it->second.is_unsigned()) { |
| return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; |
| } |
| const int64_t subcommand = subcommand_it->second.GetUnsigned(); |
| |
| cbor::Value::MapValue response_map; |
| switch (static_cast<CredentialManagementSubCommand>(subcommand)) { |
| case CredentialManagementSubCommand::kGetCredsMetadata: { |
| CtapDeviceResponseCode pin_status = VerifyPINUVAuthToken( |
| *device_info_, mutable_state()->pin_token, request_map, |
| cbor::Value( |
| static_cast<int>(CredentialManagementRequestKey::kPinProtocol)), |
| cbor::Value( |
| static_cast<int>(CredentialManagementRequestKey::kPinAuth)), |
| {{static_cast<uint8_t>(subcommand)}}); |
| if (pin_status != CtapDeviceResponseCode::kSuccess) { |
| return pin_status; |
| } |
| |
| const size_t num_resident = |
| std::count_if(mutable_state()->registrations.begin(), |
| mutable_state()->registrations.end(), |
| [](const auto& it) { return it.second.is_resident; }); |
| response_map.emplace( |
| static_cast<int>(CredentialManagementResponseKey:: |
| kExistingResidentCredentialsCount), |
| static_cast<int64_t>(num_resident)); |
| |
| const size_t num_remaining = |
| config_.resident_credential_storage - num_resident; |
| DCHECK_LE(0ul, num_remaining); |
| response_map.emplace( |
| static_cast<int>(CredentialManagementResponseKey:: |
| kMaxPossibleRemainingResidentCredentialsCount), |
| static_cast<int64_t>(num_remaining)); |
| |
| *response = |
| cbor::Writer::Write(cbor::Value(std::move(response_map))).value(); |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| case CredentialManagementSubCommand::kEnumerateRPsBegin: { |
| CtapDeviceResponseCode pin_status = VerifyPINUVAuthToken( |
| *device_info_, mutable_state()->pin_token, request_map, |
| cbor::Value( |
| static_cast<int>(CredentialManagementRequestKey::kPinProtocol)), |
| cbor::Value( |
| static_cast<int>(CredentialManagementRequestKey::kPinAuth)), |
| {{static_cast<uint8_t>(subcommand)}}); |
| if (pin_status != CtapDeviceResponseCode::kSuccess) { |
| return pin_status; |
| } |
| |
| InitPendingRPs(); |
| response_map.emplace( |
| static_cast<int>(CredentialManagementResponseKey::kTotalRPs), |
| static_cast<int>(mutable_state()->pending_rps.size())); |
| if (!mutable_state()->pending_rps.empty()) { |
| GetNextRP(&response_map); |
| } |
| |
| *response = WriteCBOR(cbor::Value(std::move(response_map)), |
| config_.allow_invalid_utf8_in_credential_entities); |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| case CredentialManagementSubCommand::kEnumerateRPsGetNextRP: { |
| if (mutable_state()->pending_rps.empty()) { |
| return CtapDeviceResponseCode::kCtap2ErrNotAllowed; |
| } |
| GetNextRP(&response_map); |
| |
| *response = WriteCBOR(cbor::Value(std::move(response_map)), |
| config_.allow_invalid_utf8_in_credential_entities); |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| case CredentialManagementSubCommand::kEnumerateCredentialsBegin: { |
| const auto params_it = request_map.find(cbor::Value( |
| static_cast<int>(CredentialManagementRequestKey::kSubCommandParams))); |
| if (params_it == request_map.end() && !params_it->second.is_map()) { |
| return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; |
| } |
| const cbor::Value::MapValue& params = params_it->second.GetMap(); |
| |
| // pinAuth = LEFT(HMAC-SHA-256(pinToken, enumerateCredentialsBegin (0x04) |
| // || subCommandParams), 16) |
| std::vector<uint8_t> pinauth_bytes = |
| cbor::Writer::Write(cbor::Value(params)).value(); |
| pinauth_bytes.insert(pinauth_bytes.begin(), |
| static_cast<uint8_t>(subcommand)); |
| CtapDeviceResponseCode pin_status = VerifyPINUVAuthToken( |
| *device_info_, mutable_state()->pin_token, request_map, |
| cbor::Value( |
| static_cast<int>(CredentialManagementRequestKey::kPinProtocol)), |
| cbor::Value( |
| static_cast<int>(CredentialManagementRequestKey::kPinAuth)), |
| pinauth_bytes); |
| if (pin_status != CtapDeviceResponseCode::kSuccess) { |
| return pin_status; |
| } |
| |
| const auto rp_id_hash_it = params.find(cbor::Value( |
| static_cast<int>(CredentialManagementRequestParamKey::kRPIDHash))); |
| if (rp_id_hash_it == params.end() || |
| !rp_id_hash_it->second.is_bytestring() || |
| rp_id_hash_it->second.GetBytestring().size() != kRpIdHashLength) { |
| return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; |
| } |
| |
| InitPendingRegistrations(rp_id_hash_it->second.GetBytestring()); |
| if (mutable_state()->pending_registrations.empty()) { |
| return CtapDeviceResponseCode::kCtap2ErrNoCredentials; |
| } |
| response_map.swap(mutable_state()->pending_registrations.front()); |
| response_map.emplace( |
| static_cast<int>(CredentialManagementResponseKey::kTotalCredentials), |
| static_cast<int>(mutable_state()->pending_registrations.size())); |
| mutable_state()->pending_registrations.pop_front(); |
| |
| *response = WriteCBOR(cbor::Value(std::move(response_map)), |
| config_.allow_invalid_utf8_in_credential_entities); |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| case CredentialManagementSubCommand:: |
| kEnumerateCredentialsGetNextCredential: { |
| if (mutable_state()->pending_registrations.empty()) { |
| return CtapDeviceResponseCode::kCtap2ErrNotAllowed; |
| } |
| response_map.swap(mutable_state()->pending_registrations.front()); |
| mutable_state()->pending_registrations.pop_front(); |
| |
| *response = WriteCBOR(cbor::Value(std::move(response_map)), |
| config_.allow_invalid_utf8_in_credential_entities); |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| case CredentialManagementSubCommand::kDeleteCredential: { |
| const auto params_it = request_map.find(cbor::Value( |
| static_cast<int>(CredentialManagementRequestKey::kSubCommandParams))); |
| if (params_it == request_map.end() && !params_it->second.is_map()) { |
| return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; |
| } |
| const cbor::Value::MapValue& params = params_it->second.GetMap(); |
| // pinAuth = LEFT(HMAC-SHA-256(pinToken, enumerateCredentialsBegin (0x04) |
| // || subCommandParams), 16) |
| std::vector<uint8_t> pinauth_bytes = |
| cbor::Writer::Write(cbor::Value(params)).value(); |
| pinauth_bytes.insert(pinauth_bytes.begin(), |
| static_cast<uint8_t>(subcommand)); |
| CtapDeviceResponseCode pin_status = VerifyPINUVAuthToken( |
| *device_info_, mutable_state()->pin_token, request_map, |
| cbor::Value( |
| static_cast<int>(CredentialManagementRequestKey::kPinProtocol)), |
| cbor::Value( |
| static_cast<int>(CredentialManagementRequestKey::kPinAuth)), |
| pinauth_bytes); |
| if (pin_status != CtapDeviceResponseCode::kSuccess) { |
| return pin_status; |
| } |
| |
| // The spec doesn't say, but we clear the enumerateRPs and |
| // enumerateCredentials states after deleteCredential to avoid having to |
| // update them. |
| mutable_state()->pending_rps.clear(); |
| mutable_state()->pending_registrations.clear(); |
| |
| const auto credential_id_it = params.find(cbor::Value(static_cast<int>( |
| CredentialManagementRequestParamKey::kCredentialID))); |
| if (credential_id_it == params.end() || |
| !credential_id_it->second.is_map()) { |
| return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; |
| } |
| auto credential_id = PublicKeyCredentialDescriptor::CreateFromCBORValue( |
| cbor::Value(credential_id_it->second.GetMap())); |
| if (!credential_id) { |
| return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; |
| } |
| if (!base::Contains(mutable_state()->registrations, |
| credential_id->id())) { |
| return CtapDeviceResponseCode::kCtap2ErrNoCredentials; |
| } |
| mutable_state()->registrations.erase(credential_id->id()); |
| *response = {}; |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| } |
| NOTREACHED(); |
| return CtapDeviceResponseCode::kCtap2ErrInvalidOption; |
| } |
| |
| CtapDeviceResponseCode VirtualCtap2Device::OnBioEnrollment( |
| base::span<const uint8_t> request_bytes, |
| std::vector<uint8_t>* response) { |
| // TODO(martinkr): Verify PIN/UV Auth. |
| // Check to ensure that device supports bio enrollment. |
| if (device_info_->options.bio_enrollment_availability == |
| AuthenticatorSupportedOptions::BioEnrollmentAvailability:: |
| kNotSupported && |
| device_info_->options.bio_enrollment_availability_preview == |
| AuthenticatorSupportedOptions::BioEnrollmentAvailability:: |
| kNotSupported) { |
| return CtapDeviceResponseCode::kCtap2ErrUnsupportedOption; |
| } |
| |
| // Read request bytes into |cbor::Value::MapValue|. |
| const auto& cbor_request = cbor::Reader::Read(request_bytes); |
| if (!cbor_request || !cbor_request->is_map()) { |
| return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; |
| } |
| const auto& request_map = cbor_request->GetMap(); |
| |
| cbor::Value::MapValue response_map; |
| |
| // Check for the get-modality command. |
| auto it = request_map.find( |
| cbor::Value(static_cast<int>(BioEnrollmentRequestKey::kGetModality))); |
| if (it != request_map.end()) { |
| if (!it->second.is_bool()) { |
| return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; |
| } |
| if (!it->second.GetBool()) { |
| // This value is optional so sending |false| is prohibited by the spec. |
| return CtapDeviceResponseCode::kCtap2ErrInvalidOption; |
| } |
| response_map.emplace(static_cast<int>(BioEnrollmentResponseKey::kModality), |
| static_cast<int>(BioEnrollmentModality::kFingerprint)); |
| *response = |
| cbor::Writer::Write(cbor::Value(std::move(response_map))).value(); |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| // Check for subcommands. |
| it = request_map.find( |
| cbor::Value(static_cast<int>(BioEnrollmentRequestKey::kSubCommand))); |
| if (it == request_map.end()) { |
| // Could not find a valid command, so return an error. |
| NOTREACHED(); |
| return CtapDeviceResponseCode::kCtap2ErrInvalidOption; |
| } |
| |
| if (!it->second.is_unsigned()) { |
| return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; |
| } |
| |
| // Template id from subcommand parameters, if it exists. |
| base::Optional<uint8_t> template_id; |
| base::Optional<std::string> name; |
| auto params_it = request_map.find(cbor::Value( |
| static_cast<int>(BioEnrollmentRequestKey::kSubCommandParams))); |
| if (params_it != request_map.end()) { |
| const auto& params = params_it->second.GetMap(); |
| auto template_it = params.find(cbor::Value( |
| static_cast<int>(BioEnrollmentSubCommandParam::kTemplateId))); |
| if (template_it != params.end()) { |
| if (!template_it->second.is_bytestring()) { |
| NOTREACHED() << "Template ID parameter must be a CBOR bytestring."; |
| return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; |
| } |
| // Simplification: for unit tests, enforce one byte template IDs |
| DCHECK_EQ(template_it->second.GetBytestring().size(), 1u); |
| template_id = template_it->second.GetBytestring()[0]; |
| } |
| auto name_it = params.find(cbor::Value( |
| static_cast<int>(BioEnrollmentSubCommandParam::kTemplateFriendlyName))); |
| if (name_it != params.end()) { |
| if (!name_it->second.is_string()) { |
| NOTREACHED() << "Name parameter must be a CBOR string."; |
| return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; |
| } |
| name = name_it->second.GetString(); |
| } |
| } |
| |
| auto cmd = |
| ToBioEnrollmentEnum<BioEnrollmentSubCommand>(it->second.GetUnsigned()); |
| if (!cmd) { |
| // Invalid command is unsupported. |
| return CtapDeviceResponseCode::kCtap2ErrUnsupportedOption; |
| } |
| |
| using SubCmd = BioEnrollmentSubCommand; |
| switch (*cmd) { |
| // TODO(crbug.com/1090415): some of these commands should be checking |
| // PinUvAuthToken. |
| case SubCmd::kGetFingerprintSensorInfo: |
| response_map.emplace( |
| static_cast<int>(BioEnrollmentResponseKey::kModality), |
| static_cast<int>(BioEnrollmentModality::kFingerprint)); |
| response_map.emplace( |
| static_cast<int>(BioEnrollmentResponseKey::kFingerprintKind), |
| static_cast<int>(BioEnrollmentFingerprintKind::kTouch)); |
| response_map.emplace( |
| static_cast<int>( |
| BioEnrollmentResponseKey::kMaxCaptureSamplesRequiredForEnroll), |
| config_.bio_enrollment_samples_required); |
| break; |
| case SubCmd::kEnrollBegin: |
| if (mutable_state()->bio_templates.size() == |
| config_.bio_enrollment_capacity) { |
| return CtapDeviceResponseCode::kCtap2ErrKeyStoreFull; |
| } |
| mutable_state()->bio_current_template_id = 0; |
| while (mutable_state()->bio_templates.find( |
| ++(*mutable_state()->bio_current_template_id)) != |
| mutable_state()->bio_templates.end()) { |
| // Check for integer overflow (indicates full) |
| DCHECK_LT(*mutable_state()->bio_current_template_id, 255); |
| } |
| mutable_state()->bio_remaining_samples = |
| config_.bio_enrollment_samples_required; |
| response_map.emplace( |
| static_cast<int>(BioEnrollmentResponseKey::kTemplateId), |
| std::vector<uint8_t>{*mutable_state()->bio_current_template_id}); |
| response_map.emplace( |
| static_cast<int>(BioEnrollmentResponseKey::kLastEnrollSampleStatus), |
| static_cast<int>(BioEnrollmentSampleStatus::kGood)); |
| response_map.emplace( |
| static_cast<int>(BioEnrollmentResponseKey::kRemainingSamples), |
| --mutable_state()->bio_remaining_samples); |
| break; |
| case SubCmd::kEnrollCaptureNextSample: |
| if (!mutable_state()->bio_current_template_id || |
| mutable_state()->bio_current_template_id != *template_id) { |
| NOTREACHED() << "Invalid current enrollment or template id parameter."; |
| return CtapDeviceResponseCode::kCtap2ErrInvalidCBOR; |
| } |
| if (mutable_state()->bio_enrollment_next_sample_error) { |
| response_map.emplace( |
| static_cast<int>(BioEnrollmentResponseKey::kLastEnrollSampleStatus), |
| static_cast<int>(BioEnrollmentSampleStatus::kTooHigh)); |
| response_map.emplace( |
| static_cast<int>(BioEnrollmentResponseKey::kRemainingSamples), |
| mutable_state()->bio_remaining_samples); |
| mutable_state()->bio_enrollment_next_sample_error = false; |
| break; |
| } |
| if (mutable_state()->bio_enrollment_next_sample_timeout) { |
| response_map.emplace( |
| static_cast<int>(BioEnrollmentResponseKey::kLastEnrollSampleStatus), |
| static_cast<int>(BioEnrollmentSampleStatus::kNoUserActivity)); |
| response_map.emplace( |
| static_cast<int>(BioEnrollmentResponseKey::kRemainingSamples), |
| mutable_state()->bio_remaining_samples); |
| mutable_state()->bio_enrollment_next_sample_timeout = false; |
| break; |
| } |
| response_map.emplace( |
| static_cast<int>(BioEnrollmentResponseKey::kLastEnrollSampleStatus), |
| static_cast<int>(BioEnrollmentSampleStatus::kGood)); |
| response_map.emplace( |
| static_cast<int>(BioEnrollmentResponseKey::kRemainingSamples), |
| --mutable_state()->bio_remaining_samples); |
| |
| if (mutable_state()->bio_remaining_samples == 0) { |
| mutable_state() |
| ->bio_templates[*mutable_state()->bio_current_template_id] = |
| base::StrCat( |
| {"Template", base::NumberToString( |
| *mutable_state()->bio_current_template_id)}); |
| mutable_state()->bio_current_template_id = base::nullopt; |
| mutable_state()->fingerprints_enrolled = true; |
| } |
| break; |
| case SubCmd::kEnumerateEnrollments: { |
| if (mutable_state()->bio_templates.empty()) { |
| return CtapDeviceResponseCode::kCtap2ErrInvalidOption; |
| } |
| cbor::Value::ArrayValue template_infos; |
| for (const auto& enroll : mutable_state()->bio_templates) { |
| cbor::Value::MapValue template_info; |
| template_info.emplace(cbor::Value(static_cast<int>( |
| BioEnrollmentTemplateInfoParam::kTemplateId)), |
| std::vector<uint8_t>{enroll.first}); |
| template_info.emplace( |
| cbor::Value(static_cast<int>( |
| BioEnrollmentTemplateInfoParam::kTemplateFriendlyName)), |
| cbor::Value(enroll.second)); |
| template_infos.emplace_back(std::move(template_info)); |
| } |
| response_map.emplace( |
| static_cast<int>(BioEnrollmentResponseKey::kTemplateInfos), |
| std::move(template_infos)); |
| break; |
| } |
| case SubCmd::kSetFriendlyName: |
| if (!template_id || !name) { |
| NOTREACHED() << "Could not parse template_id or name from parameters."; |
| return CtapDeviceResponseCode::kCtap2ErrInvalidCBOR; |
| } |
| |
| // Template ID from parameter does not exist, cannot rename. |
| if (mutable_state()->bio_templates.find(*template_id) == |
| mutable_state()->bio_templates.end()) { |
| return CtapDeviceResponseCode::kCtap2ErrInvalidOption; |
| } |
| |
| mutable_state()->bio_templates[*template_id] = *name; |
| return CtapDeviceResponseCode::kSuccess; |
| case SubCmd::kRemoveEnrollment: |
| if (!template_id) { |
| NOTREACHED() << "Could not parse template_id or name from parameters."; |
| return CtapDeviceResponseCode::kCtap2ErrInvalidCBOR; |
| } |
| |
| // Template ID from parameter does not exist, cannot remove. |
| if (mutable_state()->bio_templates.find(*template_id) == |
| mutable_state()->bio_templates.end()) { |
| return CtapDeviceResponseCode::kCtap2ErrInvalidOption; |
| } |
| |
| mutable_state()->bio_templates.erase(*template_id); |
| return CtapDeviceResponseCode::kSuccess; |
| case SubCmd::kCancelCurrentEnrollment: |
| mutable_state()->bio_current_template_id = base::nullopt; |
| return CtapDeviceResponseCode::kSuccess; |
| default: |
| // Handle all other commands as if they were unsupported (will change |
| // when support is added). |
| return CtapDeviceResponseCode::kCtap2ErrUnsupportedOption; |
| } |
| *response = cbor::Writer::Write(cbor::Value(std::move(response_map))).value(); |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| CtapDeviceResponseCode VirtualCtap2Device::OnLargeBlobs( |
| base::span<const uint8_t> request_bytes, |
| std::vector<uint8_t>* response) { |
| if (!config_.large_blob_support) { |
| DLOG(ERROR) << "Large blob not supported"; |
| return CtapDeviceResponseCode::kCtap2ErrUnsupportedExtension; |
| } |
| |
| // Read request bytes into |cbor::Value::MapValue|. |
| const auto& cbor_request = cbor::Reader::Read(request_bytes); |
| if (!cbor_request || !cbor_request->is_map()) { |
| return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; |
| } |
| const auto& request_map = cbor_request->GetMap(); |
| |
| const auto offset_it = request_map.find( |
| cbor::Value(static_cast<uint8_t>(LargeBlobsRequestKey::kOffset))); |
| if (offset_it == request_map.end() || !offset_it->second.is_unsigned()) { |
| return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; |
| } |
| const uint64_t offset = offset_it->second.GetUnsigned(); |
| |
| const auto get_it = request_map.find( |
| cbor::Value(static_cast<uint8_t>(LargeBlobsRequestKey::kGet))); |
| const auto set_it = request_map.find( |
| cbor::Value(static_cast<uint8_t>(LargeBlobsRequestKey::kSet))); |
| if ((get_it == request_map.end() && set_it == request_map.end()) || |
| (get_it != request_map.end() && set_it != request_map.end())) { |
| return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; |
| } |
| if ((get_it != request_map.end() && !get_it->second.is_unsigned()) || |
| (set_it != request_map.end() && !set_it->second.is_bytestring())) { |
| return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; |
| } |
| const auto length_it = request_map.find( |
| cbor::Value(static_cast<uint8_t>(LargeBlobsRequestKey::kLength))); |
| const size_t max_fragment_length = kLargeBlobDefaultMaxFragmentLength; |
| |
| if (get_it != request_map.end()) { |
| if (length_it != request_map.end() || |
| request_map.find(cbor::Value(static_cast<uint8_t>( |
| LargeBlobsRequestKey::kPinUvAuthParam))) != request_map.end() || |
| request_map.find(cbor::Value(static_cast<uint8_t>( |
| LargeBlobsRequestKey::kPinUvAuthProtocol))) != request_map.end()) { |
| return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; |
| } |
| const uint64_t get = get_it->second.GetUnsigned(); |
| if (get > max_fragment_length) { |
| return CtapDeviceResponseCode::kCtap1ErrInvalidLength; |
| } |
| if (offset > mutable_state()->large_blob.size()) { |
| return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; |
| } |
| cbor::Value::MapValue response_map; |
| response_map.emplace( |
| static_cast<uint8_t>(LargeBlobsResponseKey::kConfig), |
| base::make_span( |
| mutable_state()->large_blob.data() + offset, |
| std::min(get, mutable_state()->large_blob.size() - offset))); |
| *response = |
| cbor::Writer::Write(cbor::Value(std::move(response_map))).value(); |
| } else { |
| DCHECK(set_it != request_map.end()); |
| const std::vector<uint8_t>& set = set_it->second.GetBytestring(); |
| if (set.size() > max_fragment_length) { |
| return CtapDeviceResponseCode::kCtap1ErrInvalidLength; |
| } |
| if (offset == 0) { |
| if (length_it == request_map.end() || !length_it->second.is_unsigned()) { |
| return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; |
| } |
| const uint64_t length = length_it->second.GetUnsigned(); |
| if (length > config_.available_large_blob_storage) { |
| return CtapDeviceResponseCode::kCtap2ErrLargeBlobStorageFull; |
| } |
| constexpr size_t kMinBlobLength = 17; |
| if (length < kMinBlobLength) { |
| return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; |
| } |
| mutable_state()->large_blob_expected_length = length; |
| mutable_state()->large_blob_expected_next_offset = 0; |
| } else { |
| if (length_it != request_map.end()) { |
| return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; |
| } |
| } |
| |
| if (offset != mutable_state()->large_blob_expected_next_offset) { |
| return CtapDeviceResponseCode::kCtap1ErrInvalidSeq; |
| } |
| |
| // If the device is protected by some sort of user verification or alwaysUv |
| // is true. |
| if (device_info_->options.client_pin_availability == |
| AuthenticatorSupportedOptions::ClientPinAvailability:: |
| kSupportedAndPinSet || |
| device_info_->options.user_verification_availability == |
| AuthenticatorSupportedOptions::UserVerificationAvailability:: |
| kSupportedAndConfigured || |
| config_.always_uv) { |
| // verify(pinUvAuthToken, |
| // 32×0xff || h’0c00' || uint32LittleEndian(offset) || SHA-256( |
| // contents of set byte string, i.e. not including an outer CBOR |
| // tag with major type two), |
| // pinUvAuthParam) |
| std::vector<uint8_t> pinauth_bytes; |
| pinauth_bytes.insert(pinauth_bytes.begin(), |
| pin::kPinUvAuthTokenSafetyPadding.begin(), |
| pin::kPinUvAuthTokenSafetyPadding.end()); |
| pinauth_bytes.insert(pinauth_bytes.end(), kLargeBlobPinPrefix.begin(), |
| kLargeBlobPinPrefix.end()); |
| auto offset_vec = fido_parsing_utils::Uint32LittleEndian(offset); |
| pinauth_bytes.insert(pinauth_bytes.end(), offset_vec.begin(), |
| offset_vec.end()); |
| std::array<uint8_t, crypto::kSHA256Length> set_hash = |
| crypto::SHA256Hash(set); |
| pinauth_bytes.insert(pinauth_bytes.end(), set_hash.begin(), |
| set_hash.end()); |
| CtapDeviceResponseCode pin_status = VerifyPINUVAuthToken( |
| *device_info_, mutable_state()->pin_token, request_map, |
| cbor::Value( |
| static_cast<uint8_t>(LargeBlobsRequestKey::kPinUvAuthProtocol)), |
| cbor::Value( |
| static_cast<uint8_t>(LargeBlobsRequestKey::kPinUvAuthParam)), |
| pinauth_bytes); |
| if (pin_status != CtapDeviceResponseCode::kSuccess) { |
| return pin_status; |
| } |
| |
| if (!(mutable_state()->pin_uv_token_permissions & |
| static_cast<uint8_t>(pin::Permissions::kLargeBlobWrite))) { |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; |
| } |
| } |
| if (offset + set.size() > mutable_state()->large_blob_expected_length) { |
| return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; |
| } |
| if (offset == 0) { |
| mutable_state()->large_blob_buffer.clear(); |
| } |
| mutable_state()->large_blob_buffer.insert( |
| mutable_state()->large_blob_buffer.end(), set.begin(), set.end()); |
| mutable_state()->large_blob_expected_next_offset = |
| mutable_state()->large_blob_buffer.size(); |
| if (mutable_state()->large_blob_buffer.size() == |
| mutable_state()->large_blob_expected_length) { |
| if (!VerifyLargeBlobArrayIntegrity(mutable_state()->large_blob_buffer)) { |
| return CtapDeviceResponseCode::kCtap2ErrIntegrityFailure; |
| } |
| mutable_state()->large_blob = mutable_state()->large_blob_buffer; |
| } |
| } |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| void VirtualCtap2Device::InitPendingRPs() { |
| mutable_state()->pending_rps.clear(); |
| std::set<std::string> rp_ids; |
| for (const auto& registration : mutable_state()->registrations) { |
| if (!registration.second.is_resident) { |
| continue; |
| } |
| DCHECK(!registration.second.is_u2f); |
| DCHECK(registration.second.user); |
| DCHECK(registration.second.rp); |
| if (!base::Contains(rp_ids, registration.second.rp->id)) { |
| mutable_state()->pending_rps.push_back(*registration.second.rp); |
| } |
| } |
| } |
| |
| void VirtualCtap2Device::InitPendingRegistrations( |
| base::span<const uint8_t> rp_id_hash) { |
| DCHECK_EQ(rp_id_hash.size(), kRpIdHashLength); |
| mutable_state()->pending_registrations.clear(); |
| for (const auto& registration : mutable_state()->registrations) { |
| if (!registration.second.is_resident || |
| !std::equal(rp_id_hash.begin(), rp_id_hash.end(), |
| registration.second.application_parameter.begin())) { |
| continue; |
| } |
| DCHECK(!registration.second.is_u2f && registration.second.user && |
| registration.second.rp); |
| cbor::Value::MapValue response_map; |
| response_map.emplace( |
| static_cast<int>(CredentialManagementResponseKey::kUser), |
| *UserEntityAsCBOR(*registration.second.user, |
| config_.allow_invalid_utf8_in_credential_entities)); |
| response_map.emplace( |
| static_cast<int>(CredentialManagementResponseKey::kCredentialID), |
| AsCBOR(PublicKeyCredentialDescriptor(CredentialType::kPublicKey, |
| registration.first))); |
| |
| base::Optional<cbor::Value> cose_key = cbor::Reader::Read( |
| registration.second.private_key->GetPublicKey()->cose_key_bytes); |
| response_map.emplace( |
| static_cast<int>(CredentialManagementResponseKey::kPublicKey), |
| cose_key->GetMap()); |
| mutable_state()->pending_registrations.emplace_back( |
| std::move(response_map)); |
| } |
| } |
| |
| void VirtualCtap2Device::RegenerateKeyAgreementKey() { |
| bssl::UniquePtr<EC_KEY> key(EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)); |
| CHECK(EC_KEY_generate_key(key.get())); |
| mutable_state()->ecdh_key = std::move(key); |
| } |
| |
| void VirtualCtap2Device::GetNextRP(cbor::Value::MapValue* response_map) { |
| DCHECK(!mutable_state()->pending_rps.empty()); |
| response_map->emplace( |
| static_cast<int>(CredentialManagementResponseKey::kRP), |
| *RpEntityAsCBOR(mutable_state()->pending_rps.front(), |
| config_.allow_invalid_utf8_in_credential_entities)); |
| response_map->emplace( |
| static_cast<int>(CredentialManagementResponseKey::kRPIDHash), |
| fido_parsing_utils::CreateSHA256Hash( |
| mutable_state()->pending_rps.front().id)); |
| mutable_state()->pending_rps.pop_front(); |
| } |
| |
| CtapDeviceResponseCode VirtualCtap2Device::OnAuthenticatorGetInfo( |
| std::vector<uint8_t>* response) const { |
| *response = AuthenticatorGetInfoResponse::EncodeToCBOR(*device_info_); |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| AttestedCredentialData VirtualCtap2Device::ConstructAttestedCredentialData( |
| base::span<const uint8_t> key_handle, |
| std::unique_ptr<PublicKey> public_key) { |
| constexpr std::array<uint8_t, 2> sha256_length = {0, crypto::kSHA256Length}; |
| constexpr std::array<uint8_t, 16> kZeroAaguid = {0, 0, 0, 0, 0, 0, 0, 0, |
| 0, 0, 0, 0, 0, 0, 0, 0}; |
| base::span<const uint8_t, 16> aaguid(kDeviceAaguid); |
| if (mutable_state()->self_attestation && |
| !mutable_state()->non_zero_aaguid_with_self_attestation) { |
| aaguid = kZeroAaguid; |
| } |
| return AttestedCredentialData(aaguid, sha256_length, |
| fido_parsing_utils::Materialize(key_handle), |
| std::move(public_key)); |
| } |
| |
| size_t VirtualCtap2Device::remaining_resident_credentials() const { |
| size_t num_resident_keys = 0; |
| for (const auto& registration : mutable_state()->registrations) { |
| if (registration.second.is_resident) { |
| num_resident_keys++; |
| } |
| } |
| |
| DCHECK_LE(num_resident_keys, config_.resident_credential_storage); |
| return config_.resident_credential_storage - num_resident_keys; |
| } |
| |
| bool VirtualCtap2Device::SupportsAtLeast(Ctap2Version ctap2_version) const { |
| return base::ranges::any_of(config_.ctap2_versions, |
| [ctap2_version](const Ctap2Version& version) { |
| return version >= ctap2_version; |
| }); |
| } |
| |
| } // namespace device |