| // 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 <array> |
| #include <string> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/logging.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/threading/thread_task_runner_handle.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/ctap_get_assertion_request.h" |
| #include "device/fido/ctap_make_credential_request.h" |
| #include "device/fido/ec_public_key.h" |
| #include "device/fido/fido_constants.h" |
| #include "device/fido/fido_parsing_utils.h" |
| #include "device/fido/opaque_attestation_statement.h" |
| #include "device/fido/pin.h" |
| #include "device/fido/pin_internal.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/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}}; |
| |
| 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; |
| } |
| |
| 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>{})))); |
| } |
| |
| // CheckPINToken returns true iff |pin_auth| is a valid authentication of |
| // |client_data_hash| given that the PIN token in effect is |pin_token|. |
| bool CheckPINToken(base::span<const uint8_t> pin_token, |
| base::span<const uint8_t> pin_auth, |
| base::span<const uint8_t> client_data_hash) { |
| uint8_t calculated_pin_auth[SHA256_DIGEST_LENGTH]; |
| unsigned hmac_bytes; |
| CHECK(HMAC(EVP_sha256(), pin_token.data(), pin_token.size(), |
| client_data_hash.data(), client_data_hash.size(), |
| calculated_pin_auth, &hmac_bytes)); |
| DCHECK_EQ(sizeof(calculated_pin_auth), static_cast<size_t>(hmac_bytes)); |
| |
| return pin_auth.size() == 16 && |
| CRYPTO_memcmp(pin_auth.data(), calculated_pin_auth, 16) == 0; |
| } |
| |
| // CheckUserVerification implements the first, common steps of |
| // makeCredential and getAssertion from the CTAP2 spec. |
| CtapDeviceResponseCode CheckUserVerification( |
| bool is_make_credential, |
| const AuthenticatorSupportedOptions& options, |
| const base::Optional<std::vector<uint8_t>>& pin_auth, |
| const base::Optional<uint8_t>& pin_protocol, |
| base::span<const uint8_t> pin_token, |
| base::span<const uint8_t> client_data_hash, |
| UserVerificationRequirement user_verification, |
| base::RepeatingCallback<void(void)> simulate_press_callback, |
| bool* out_user_verified) { |
| // 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 (simulate_press_callback) { |
| simulate_press_callback.Run(); |
| } |
| switch (options.client_pin_availability) { |
| case AuthenticatorSupportedOptions::ClientPinAvailability:: |
| kSupportedAndPinSet: |
| return CtapDeviceResponseCode::kCtap2ErrPinInvalid; |
| case AuthenticatorSupportedOptions::ClientPinAvailability:: |
| kSupportedButPinNotSet: |
| return CtapDeviceResponseCode::kCtap2ErrPinNotSet; |
| case AuthenticatorSupportedOptions::ClientPinAvailability::kNotSupported: |
| NOTREACHED(); |
| } |
| } |
| |
| // 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 || *pin_protocol != 1)) { |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; |
| } |
| |
| // 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." |
| const bool can_do_uv = |
| options.user_verification_availability == |
| AuthenticatorSupportedOptions::UserVerificationAvailability:: |
| kSupportedAndConfigured || |
| options.client_pin_availability == |
| AuthenticatorSupportedOptions::ClientPinAvailability:: |
| kSupportedAndPinSet; |
| if (!can_do_uv && |
| (user_verification == UserVerificationRequirement::kRequired || |
| pin_auth)) { |
| return CtapDeviceResponseCode::kCtap2ErrInvalidOption; |
| } |
| |
| // Step 4. |
| bool uv = false; |
| if (can_do_uv) { |
| if (options.user_verification_availability == |
| AuthenticatorSupportedOptions::UserVerificationAvailability:: |
| kSupportedAndConfigured && |
| user_verification == UserVerificationRequirement::kRequired) { |
| // Internal UV is assumed to always succeed. |
| if (simulate_press_callback) { |
| simulate_press_callback.Run(); |
| } |
| uv = true; |
| } |
| |
| if (pin_auth && options.client_pin_availability == |
| AuthenticatorSupportedOptions::ClientPinAvailability:: |
| kSupportedAndPinSet) { |
| DCHECK(pin_protocol && *pin_protocol == 1); |
| if (CheckPINToken(pin_token, *pin_auth, client_data_hash)) { |
| uv = true; |
| } else { |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; |
| } |
| } |
| |
| if (is_make_credential && !uv) { |
| return CtapDeviceResponseCode::kCtap2ErrPinRequired; |
| } |
| } |
| |
| *out_user_verified = uv; |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| // Checks that whether the received MakeCredential request includes EA256 |
| // algorithm in publicKeyCredParam. |
| bool AreMakeCredentialParamsValid(const CtapMakeCredentialRequest& request) { |
| const auto& params = |
| request.public_key_credential_params().public_key_credential_params(); |
| return std::any_of( |
| params.begin(), params.end(), [](const auto& credential_info) { |
| return credential_info.algorithm == |
| base::strict_cast<int>(CoseAlgorithmIdentifier::kCoseEs256); |
| }); |
| } |
| |
| std::unique_ptr<ECPublicKey> ConstructECPublicKey( |
| std::string public_key_string) { |
| DCHECK_EQ(64u, public_key_string.size()); |
| |
| const auto public_key_x_coordinate = |
| base::as_bytes(base::make_span(public_key_string)).first(32); |
| const auto public_key_y_coordinate = |
| base::as_bytes(base::make_span(public_key_string)).last(32); |
| return std::make_unique<ECPublicKey>( |
| fido_parsing_utils::kEs256, |
| fido_parsing_utils::Materialize(public_key_x_coordinate), |
| fido_parsing_utils::Materialize(public_key_y_coordinate)); |
| } |
| |
| 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::vector<uint8_t> ConstructMakeCredentialResponse( |
| const base::Optional<std::vector<uint8_t>> attestation_certificate, |
| base::span<const uint8_t> signature, |
| AuthenticatorData authenticator_data) { |
| 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))))); |
| return GetSerializedCtapDeviceResponse(make_credential_response); |
| } |
| |
| std::vector<uint8_t> ConstructGetAssertionResponse( |
| AuthenticatorData authenticator_data, |
| base::span<const uint8_t> signature, |
| base::span<const uint8_t> key_handle) { |
| AuthenticatorGetAssertionResponse response( |
| std::move(authenticator_data), |
| fido_parsing_utils::Materialize(signature)); |
| |
| response.SetCredential({CredentialType::kPublicKey, |
| fido_parsing_utils::Materialize(key_handle)}); |
| response.SetNumCredentials(1); |
| return GetSerializedCtapDeviceResponse(response); |
| } |
| |
| bool IsMakeCredentialOptionMapFormatCorrect( |
| const cbor::Value::MapValue& option_map) { |
| return std::all_of( |
| option_map.begin(), option_map.end(), [](const auto& param) { |
| if (!param.first.is_string()) |
| return false; |
| |
| const auto& key = param.first.GetString(); |
| return ((key == kResidentKeyMapKey || key == kUserVerificationMapKey) && |
| param.second.is_bool()); |
| }); |
| } |
| |
| bool AreMakeCredentialRequestMapKeysCorrect( |
| const cbor::Value::MapValue& request_map) { |
| return std::all_of(request_map.begin(), request_map.end(), |
| [](const auto& param) { |
| if (!param.first.is_integer()) |
| return false; |
| |
| const auto& key = param.first.GetInteger(); |
| return (key <= 9u && key >= 1u); |
| }); |
| } |
| |
| bool IsGetAssertionOptionMapFormatCorrect( |
| const cbor::Value::MapValue& option_map) { |
| return std::all_of( |
| option_map.begin(), option_map.end(), [](const auto& param) { |
| if (!param.first.is_string()) |
| return false; |
| |
| const auto& key = param.first.GetString(); |
| return (key == kUserPresenceMapKey || key == kUserVerificationMapKey) && |
| param.second.is_bool(); |
| }); |
| } |
| |
| bool AreGetAssertionRequestMapKeysCorrect( |
| const cbor::Value::MapValue& request_map) { |
| return std::all_of(request_map.begin(), request_map.end(), |
| [](const auto& param) { |
| if (!param.first.is_integer()) |
| return false; |
| |
| const auto& key = param.first.GetInteger(); |
| return (key <= 7u || key >= 1u); |
| }); |
| } |
| |
| 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( |
| VirtualCtap2Device::State* state, |
| const uint8_t shared_key[SHA256_DIGEST_LENGTH], |
| const std::vector<uint8_t>& encrypted_pin_hash) { |
| if (state->retries == 0) { |
| return CtapDeviceResponseCode::kCtap2ErrPinBlocked; |
| } |
| if (state->soft_locked) { |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthBlocked; |
| } |
| |
| state->retries--; |
| state->retries_since_insertion++; |
| |
| DCHECK((encrypted_pin_hash.size() % AES_BLOCK_SIZE) == 0); |
| uint8_t pin_hash[AES_BLOCK_SIZE]; |
| pin::Decrypt(shared_key, encrypted_pin_hash, 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); |
| |
| if (state->pin.empty() || |
| CRYPTO_memcmp(pin_hash, calculated_pin_hash, sizeof(pin_hash)) != 0) { |
| if (state->retries == 0) { |
| return CtapDeviceResponseCode::kCtap2ErrPinBlocked; |
| } |
| if (state->retries_since_insertion == 3) { |
| state->soft_locked = true; |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthBlocked; |
| } |
| return CtapDeviceResponseCode::kCtap2ErrPinInvalid; |
| } |
| |
| state->retries = 8; |
| state->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(VirtualCtap2Device::State* state, |
| const uint8_t shared_key[SHA256_DIGEST_LENGTH], |
| const std::vector<uint8_t>& encrypted_pin, |
| const std::vector<uint8_t>& pin_auth) { |
| // See |
| // https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-client-to-authenticator-protocol-v2.0-rd-20180702.html#settingNewPin |
| uint8_t calculated_pin_auth[SHA256_DIGEST_LENGTH]; |
| unsigned hmac_bytes; |
| CHECK(HMAC(EVP_sha256(), shared_key, SHA256_DIGEST_LENGTH, |
| encrypted_pin.data(), encrypted_pin.size(), calculated_pin_auth, |
| &hmac_bytes)); |
| DCHECK_EQ(sizeof(calculated_pin_auth), static_cast<size_t>(hmac_bytes)); |
| |
| if (pin_auth.size() != sizeof(calculated_pin_auth) || |
| CRYPTO_memcmp(calculated_pin_auth, pin_auth.data(), |
| sizeof(calculated_pin_auth)) != 0) { |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; |
| } |
| |
| if (encrypted_pin.size() < 64) { |
| return CtapDeviceResponseCode::kCtap2ErrPinPolicyViolation; |
| } |
| |
| std::vector<uint8_t> plaintext_pin(encrypted_pin.size()); |
| pin::Decrypt(shared_key, encrypted_pin, plaintext_pin.data()); |
| |
| 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() < 4 || |
| plaintext_pin.size() > 63) { |
| return CtapDeviceResponseCode::kCtap2ErrPinPolicyViolation; |
| } |
| |
| state->pin = std::string(reinterpret_cast<const char*>(plaintext_pin.data()), |
| plaintext_pin.size()); |
| state->retries = 8; |
| |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| } // namespace |
| |
| VirtualCtap2Device::Config::Config() = default; |
| |
| VirtualCtap2Device::VirtualCtap2Device() |
| : VirtualFidoDevice(), weak_factory_(this) { |
| device_info_ = |
| AuthenticatorGetInfoResponse({ProtocolVersion::kCtap}, kDeviceAaguid); |
| } |
| |
| VirtualCtap2Device::VirtualCtap2Device(scoped_refptr<State> state, |
| const Config& config) |
| : VirtualFidoDevice(std::move(state)), weak_factory_(this) { |
| device_info_ = |
| AuthenticatorGetInfoResponse({ProtocolVersion::kCtap}, kDeviceAaguid); |
| |
| 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; |
| } |
| } |
| |
| if (options_updated) { |
| device_info_->SetOptions(options); |
| } |
| } |
| |
| VirtualCtap2Device::~VirtualCtap2Device() = default; |
| |
| // As all operations for VirtualCtap2Device are synchronous and we do not wait |
| // for user touch, Cancel command is no-op. |
| void VirtualCtap2Device::Cancel() {} |
| |
| void VirtualCtap2Device::DeviceTransact(std::vector<uint8_t> command, |
| DeviceCallback cb) { |
| if (command.empty()) { |
| ReturnCtap2Response(std::move(cb), CtapDeviceResponseCode::kCtap2ErrOther); |
| return; |
| } |
| |
| auto cmd_type = command[0]; |
| const auto request_bytes = base::make_span(command).subspan(1); |
| CtapDeviceResponseCode response_code = CtapDeviceResponseCode::kCtap2ErrOther; |
| std::vector<uint8_t> response_data; |
| |
| switch (static_cast<CtapRequestCommand>(cmd_type)) { |
| case CtapRequestCommand::kAuthenticatorGetInfo: |
| if (!request_bytes.empty()) { |
| ReturnCtap2Response(std::move(cb), |
| CtapDeviceResponseCode::kCtap2ErrOther); |
| return; |
| } |
| |
| response_code = OnAuthenticatorGetInfo(&response_data); |
| break; |
| case CtapRequestCommand::kAuthenticatorMakeCredential: |
| response_code = OnMakeCredential(request_bytes, &response_data); |
| break; |
| case CtapRequestCommand::kAuthenticatorGetAssertion: |
| response_code = OnGetAssertion(request_bytes, &response_data); |
| break; |
| case CtapRequestCommand::kAuthenticatorClientPin: |
| response_code = OnPINCommand(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)); |
| } |
| |
| base::WeakPtr<FidoDevice> VirtualCtap2Device::GetWeakPtr() { |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| void VirtualCtap2Device::SetAuthenticatorSupportedOptions( |
| const AuthenticatorSupportedOptions& options) { |
| device_info_->SetOptions(options); |
| } |
| |
| CtapDeviceResponseCode VirtualCtap2Device::OnMakeCredential( |
| base::span<const uint8_t> request_bytes, |
| std::vector<uint8_t>* response) { |
| auto request_and_hash = ParseCtapMakeCredentialRequest(request_bytes); |
| if (!request_and_hash) { |
| DLOG(ERROR) << "Incorrectly formatted MakeCredential request."; |
| return CtapDeviceResponseCode::kCtap2ErrOther; |
| } |
| CtapMakeCredentialRequest request = std::get<0>(*request_and_hash); |
| CtapMakeCredentialRequest::ClientDataHash client_data_hash = |
| std::get<1>(*request_and_hash); |
| const AuthenticatorSupportedOptions& options = device_info_->options(); |
| |
| bool user_verified; |
| const CtapDeviceResponseCode uv_error = CheckUserVerification( |
| true /* is makeCredential */, options, request.pin_auth(), |
| request.pin_protocol(), mutable_state()->pin_token, client_data_hash, |
| request.user_verification(), mutable_state()->simulate_press_callback, |
| &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().rp_id()); |
| if (request.exclude_list()) { |
| for (const auto& excluded_credential : *request.exclude_list()) { |
| if (FindRegistrationData(excluded_credential.id(), rp_id_hash)) { |
| if (mutable_state()->simulate_press_callback) { |
| mutable_state()->simulate_press_callback.Run(); |
| } |
| return CtapDeviceResponseCode::kCtap2ErrCredentialExcluded; |
| } |
| } |
| } |
| |
| // Step 7. |
| if (!AreMakeCredentialParamsValid(request)) { |
| DLOG(ERROR) << "Virtual CTAP2 device does not support options required by " |
| "the request."; |
| return CtapDeviceResponseCode::kCtap2ErrUnsupportedAlgorithm; |
| } |
| |
| // Step 8. |
| if ((request.resident_key_required() && !options.supports_resident_key) || |
| !options.supports_user_presence) { |
| return CtapDeviceResponseCode::kCtap2ErrUnsupportedOption; |
| } |
| |
| // Step 10. |
| if (!user_verified && mutable_state()->simulate_press_callback) { |
| mutable_state()->simulate_press_callback.Run(); |
| } |
| |
| // Create key to register. |
| // Note: Non-deterministic, you need to mock this out if you rely on |
| // deterministic behavior. |
| auto private_key = crypto::ECPrivateKey::Create(); |
| std::string public_key; |
| bool status = private_key->ExportRawPublicKey(&public_key); |
| DCHECK(status); |
| |
| // Our key handles are simple hashes of the public key. |
| auto hash = fido_parsing_utils::CreateSHA256Hash(public_key); |
| std::vector<uint8_t> key_handle(hash.begin(), hash.end()); |
| std::array<uint8_t, 2> sha256_length = {0, crypto::kSHA256Length}; |
| |
| 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; |
| } |
| |
| AttestedCredentialData attested_credential_data( |
| aaguid, sha256_length, key_handle, ConstructECPublicKey(public_key)); |
| |
| base::Optional<cbor::Value> extensions; |
| if (request.hmac_secret()) { |
| cbor::Value::MapValue extensions_map; |
| extensions_map.emplace(cbor::Value(kExtensionHmacSecret), |
| cbor::Value(true)); |
| extensions = cbor::Value(std::move(extensions_map)); |
| } |
| |
| auto authenticator_data = ConstructAuthenticatorData( |
| rp_id_hash, user_verified, 01ul, std::move(attested_credential_data), |
| std::move(extensions)); |
| auto sign_buffer = |
| ConstructSignatureBuffer(authenticator_data, 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()); |
| status = Sign(attestation_private_key.get(), std::move(sign_buffer), &sig); |
| DCHECK(status); |
| |
| base::Optional<std::vector<uint8_t>> attestation_cert; |
| if (!mutable_state()->self_attestation) { |
| attestation_cert = GenerateAttestationCertificate( |
| false /* individual_attestation_requested */); |
| if (!attestation_cert) { |
| DLOG(ERROR) << "Failed to generate attestation certificate."; |
| return CtapDeviceResponseCode::kCtap2ErrOther; |
| } |
| } |
| |
| *response = ConstructMakeCredentialResponse(std::move(attestation_cert), sig, |
| std::move(authenticator_data)); |
| |
| StoreNewKey(rp_id_hash, key_handle, std::move(private_key)); |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| CtapDeviceResponseCode VirtualCtap2Device::OnGetAssertion( |
| base::span<const uint8_t> request_bytes, |
| std::vector<uint8_t>* response) { |
| auto request_and_hash = ParseCtapGetAssertionRequest(request_bytes); |
| if (!request_and_hash) { |
| DLOG(ERROR) << "Incorrectly formatted GetAssertion request."; |
| return CtapDeviceResponseCode::kCtap2ErrOther; |
| } |
| CtapGetAssertionRequest request = std::get<0>(*request_and_hash); |
| CtapGetAssertionRequest::ClientDataHash client_data_hash = |
| std::get<1>(*request_and_hash); |
| const AuthenticatorSupportedOptions& options = device_info_->options(); |
| |
| bool user_verified; |
| const CtapDeviceResponseCode uv_error = CheckUserVerification( |
| false /* not makeCredential */, options, request.pin_auth(), |
| request.pin_protocol(), mutable_state()->pin_token, client_data_hash, |
| request.user_verification(), mutable_state()->simulate_press_callback, |
| &user_verified); |
| if (uv_error != CtapDeviceResponseCode::kSuccess) { |
| return uv_error; |
| } |
| |
| // Resident keys are not supported. |
| if (!request.allow_list() || request.allow_list()->empty()) { |
| DLOG(ERROR) << "Allowed credential list is empty, but Virtual CTAP2 device " |
| "does not support resident keys."; |
| return CtapDeviceResponseCode::kCtap2ErrNoCredentials; |
| } |
| |
| const auto rp_id_hash = fido_parsing_utils::CreateSHA256Hash(request.rp_id()); |
| |
| RegistrationData* found_data = nullptr; |
| base::span<const uint8_t> credential_id; |
| for (const auto& allowed_credential : *request.allow_list()) { |
| if ((found_data = |
| FindRegistrationData(allowed_credential.id(), rp_id_hash))) { |
| credential_id = allowed_credential.id(); |
| break; |
| } |
| } |
| |
| if (!found_data) |
| return CtapDeviceResponseCode::kCtap2ErrNoCredentials; |
| |
| // Step 6. |
| if (!options.supports_user_presence && request.user_presence_required()) { |
| return CtapDeviceResponseCode::kCtap2ErrUnsupportedOption; |
| } |
| |
| // Step 8. |
| if (request.user_presence_required() && !user_verified && |
| mutable_state()->simulate_press_callback) { |
| mutable_state()->simulate_press_callback.Run(); |
| } |
| |
| found_data->counter++; |
| auto authenticator_data = |
| ConstructAuthenticatorData(rp_id_hash, user_verified, found_data->counter, |
| base::nullopt, base::nullopt); |
| auto signature_buffer = |
| ConstructSignatureBuffer(authenticator_data, client_data_hash); |
| |
| // Sign with the private key of the received key handle. |
| std::vector<uint8_t> sig; |
| auto* private_key = found_data->private_key.get(); |
| bool status = Sign(private_key, std::move(signature_buffer), &sig); |
| DCHECK(status); |
| |
| *response = ConstructGetAssertionResponse(std::move(authenticator_data), |
| std::move(sig), credential_id); |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| CtapDeviceResponseCode VirtualCtap2Device::OnPINCommand( |
| base::span<const uint8_t> request_bytes, |
| std::vector<uint8_t>* response) { |
| if (device_info_->options().client_pin_availability == |
| AuthenticatorSupportedOptions::ClientPinAvailability::kNotSupported) { |
| return CtapDeviceResponseCode::kCtap1ErrInvalidCommand; |
| } |
| |
| 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; |
| } |
| if (protocol_it->second.GetUnsigned() != pin::kProtocolVersion) { |
| return CtapDeviceResponseCode::kCtap1ErrInvalidCommand; |
| } |
| |
| 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(); |
| |
| 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()->retries); |
| break; |
| |
| case static_cast<int>(device::pin::Subcommand::kGetKeyAgreement): { |
| bssl::UniquePtr<EC_KEY> key( |
| EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)); |
| CHECK(EC_KEY_generate_key(key.get())); |
| response_map.emplace(static_cast<int>(pin::ResponseKey::kKeyAgreement), |
| pin::EncodeCOSEPublicKey(key.get())); |
| mutable_state()->ecdh_key = std::move(key); |
| 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 || (encrypted_pin->size() % AES_BLOCK_SIZE) != 0 || |
| !pin_auth || !peer_key) { |
| return CtapDeviceResponseCode::kCtap2ErrMissingParameter; |
| } |
| |
| if (!mutable_state()->pin.empty()) { |
| return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; |
| } |
| |
| uint8_t shared_key[SHA256_DIGEST_LENGTH]; |
| if (!mutable_state()->ecdh_key) { |
| // kGetKeyAgreement should have been called first. |
| NOTREACHED(); |
| return CtapDeviceResponseCode::kCtap2ErrPinTokenExpired; |
| } |
| pin::CalculateSharedKey(mutable_state()->ecdh_key.get(), peer_key->get(), |
| shared_key); |
| |
| CtapDeviceResponseCode err = |
| SetPIN(mutable_state(), shared_key, *encrypted_pin, *pin_auth); |
| if (err != CtapDeviceResponseCode::kSuccess) { |
| return err; |
| }; |
| |
| AuthenticatorSupportedOptions options = device_info_->options(); |
| options.client_pin_availability = AuthenticatorSupportedOptions:: |
| ClientPinAvailability::kSupportedAndPinSet; |
| device_info_->SetOptions(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_pin_hash->size() != AES_BLOCK_SIZE || |
| !encrypted_new_pin || |
| (encrypted_new_pin->size() % AES_BLOCK_SIZE) != 0 || !pin_auth || |
| !peer_key) { |
| return CtapDeviceResponseCode::kCtap2ErrMissingParameter; |
| } |
| |
| uint8_t shared_key[SHA256_DIGEST_LENGTH]; |
| if (!mutable_state()->ecdh_key) { |
| // kGetKeyAgreement should have been called first. |
| NOTREACHED(); |
| return CtapDeviceResponseCode::kCtap2ErrPinTokenExpired; |
| } |
| pin::CalculateSharedKey(mutable_state()->ecdh_key.get(), peer_key->get(), |
| shared_key); |
| |
| CtapDeviceResponseCode err = |
| ConfirmPresentedPIN(mutable_state(), shared_key, *encrypted_pin_hash); |
| if (err != CtapDeviceResponseCode::kSuccess) { |
| return err; |
| }; |
| |
| err = SetPIN(mutable_state(), shared_key, *encrypted_new_pin, *pin_auth); |
| if (err != CtapDeviceResponseCode::kSuccess) { |
| return err; |
| }; |
| |
| break; |
| } |
| |
| case static_cast<int>(device::pin::Subcommand::kGetPINToken): { |
| 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 || encrypted_pin_hash->size() != AES_BLOCK_SIZE || |
| !peer_key) { |
| return CtapDeviceResponseCode::kCtap2ErrMissingParameter; |
| } |
| |
| uint8_t shared_key[SHA256_DIGEST_LENGTH]; |
| if (!mutable_state()->ecdh_key) { |
| // kGetKeyAgreement should have been called first. |
| NOTREACHED(); |
| return CtapDeviceResponseCode::kCtap2ErrPinTokenExpired; |
| } |
| pin::CalculateSharedKey(mutable_state()->ecdh_key.get(), peer_key->get(), |
| shared_key); |
| |
| CtapDeviceResponseCode err = |
| ConfirmPresentedPIN(mutable_state(), shared_key, *encrypted_pin_hash); |
| if (err != CtapDeviceResponseCode::kSuccess) { |
| return err; |
| }; |
| |
| RAND_bytes(mutable_state()->pin_token, |
| sizeof(mutable_state()->pin_token)); |
| uint8_t encrypted_pin_token[sizeof(mutable_state()->pin_token)]; |
| pin::Encrypt(shared_key, mutable_state()->pin_token, encrypted_pin_token); |
| response_map.emplace(static_cast<int>(pin::ResponseKey::kPINToken), |
| base::span<const uint8_t>(encrypted_pin_token)); |
| break; |
| } |
| |
| default: |
| return CtapDeviceResponseCode::kCtap1ErrInvalidCommand; |
| } |
| |
| *response = cbor::Writer::Write(cbor::Value(std::move(response_map))).value(); |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| CtapDeviceResponseCode VirtualCtap2Device::OnAuthenticatorGetInfo( |
| std::vector<uint8_t>* response) const { |
| *response = EncodeToCBOR(*device_info_); |
| return CtapDeviceResponseCode::kSuccess; |
| } |
| |
| AuthenticatorData VirtualCtap2Device::ConstructAuthenticatorData( |
| base::span<const uint8_t, kRpIdHashLength> rp_id_hash, |
| bool user_verified, |
| uint32_t current_signature_count, |
| base::Optional<AttestedCredentialData> attested_credential_data, |
| base::Optional<cbor::Value> extensions) { |
| uint8_t flag = |
| base::strict_cast<uint8_t>(AuthenticatorData::Flag::kTestOfUserPresence); |
| if (user_verified) { |
| flag |= base::strict_cast<uint8_t>( |
| AuthenticatorData::Flag::kTestOfUserVerification); |
| } |
| if (attested_credential_data) |
| flag |= base::strict_cast<uint8_t>(AuthenticatorData::Flag::kAttestation); |
| if (extensions) { |
| flag |= base::strict_cast<uint8_t>( |
| AuthenticatorData::Flag::kExtensionDataIncluded); |
| } |
| |
| std::array<uint8_t, kSignCounterLength> signature_counter; |
| signature_counter[0] = (current_signature_count >> 24) & 0xff; |
| signature_counter[1] = (current_signature_count >> 16) & 0xff; |
| signature_counter[2] = (current_signature_count >> 8) & 0xff; |
| signature_counter[3] = (current_signature_count)&0xff; |
| |
| return AuthenticatorData(rp_id_hash, flag, signature_counter, |
| std::move(attested_credential_data), |
| std::move(extensions)); |
| } |
| |
| base::Optional<std::pair<CtapMakeCredentialRequest, |
| CtapMakeCredentialRequest::ClientDataHash>> |
| ParseCtapMakeCredentialRequest(base::span<const uint8_t> request_bytes) { |
| const auto& cbor_request = cbor::Reader::Read(request_bytes); |
| if (!cbor_request || !cbor_request->is_map()) |
| return base::nullopt; |
| |
| const auto& request_map = cbor_request->GetMap(); |
| if (!AreMakeCredentialRequestMapKeysCorrect(request_map)) |
| return base::nullopt; |
| |
| const auto client_data_hash_it = request_map.find(cbor::Value(1)); |
| if (client_data_hash_it == request_map.end() || |
| !client_data_hash_it->second.is_bytestring()) |
| return base::nullopt; |
| |
| const auto client_data_hash = |
| base::make_span(client_data_hash_it->second.GetBytestring()) |
| .subspan<0, kClientDataHashLength>(); |
| |
| const auto rp_entity_it = request_map.find(cbor::Value(2)); |
| if (rp_entity_it == request_map.end() || !rp_entity_it->second.is_map()) |
| return base::nullopt; |
| |
| auto rp_entity = |
| PublicKeyCredentialRpEntity::CreateFromCBORValue(rp_entity_it->second); |
| if (!rp_entity) |
| return base::nullopt; |
| |
| const auto user_entity_it = request_map.find(cbor::Value(3)); |
| if (user_entity_it == request_map.end() || !user_entity_it->second.is_map()) |
| return base::nullopt; |
| |
| auto user_entity = PublicKeyCredentialUserEntity::CreateFromCBORValue( |
| user_entity_it->second); |
| if (!user_entity) |
| return base::nullopt; |
| |
| const auto credential_params_it = request_map.find(cbor::Value(4)); |
| if (credential_params_it == request_map.end()) |
| return base::nullopt; |
| |
| auto credential_params = PublicKeyCredentialParams::CreateFromCBORValue( |
| credential_params_it->second); |
| if (!credential_params) |
| return base::nullopt; |
| |
| CtapMakeCredentialRequest request( |
| std::string() /* client_data_json */, std::move(*rp_entity), |
| std::move(*user_entity), std::move(*credential_params)); |
| |
| const auto exclude_list_it = request_map.find(cbor::Value(5)); |
| if (exclude_list_it != request_map.end()) { |
| if (!exclude_list_it->second.is_array()) |
| return base::nullopt; |
| |
| const auto& credential_descriptors = exclude_list_it->second.GetArray(); |
| std::vector<PublicKeyCredentialDescriptor> exclude_list; |
| for (const auto& credential_descriptor : credential_descriptors) { |
| auto excluded_credential = |
| PublicKeyCredentialDescriptor::CreateFromCBORValue( |
| credential_descriptor); |
| if (!excluded_credential) |
| return base::nullopt; |
| |
| exclude_list.push_back(std::move(*excluded_credential)); |
| } |
| request.SetExcludeList(std::move(exclude_list)); |
| } |
| |
| const auto extensions_it = request_map.find(cbor::Value(6)); |
| if (extensions_it != request_map.end()) { |
| if (!extensions_it->second.is_map()) { |
| return base::nullopt; |
| } |
| |
| const auto& extensions = extensions_it->second.GetMap(); |
| const auto hmac_secret_it = |
| extensions.find(cbor::Value(kExtensionHmacSecret)); |
| if (hmac_secret_it != extensions.end()) { |
| if (!hmac_secret_it->second.is_bool()) { |
| return base::nullopt; |
| } |
| request.SetHmacSecret(hmac_secret_it->second.GetBool()); |
| } |
| } |
| |
| const auto option_it = request_map.find(cbor::Value(7)); |
| if (option_it != request_map.end()) { |
| if (!option_it->second.is_map()) |
| return base::nullopt; |
| |
| const auto& option_map = option_it->second.GetMap(); |
| if (!IsMakeCredentialOptionMapFormatCorrect(option_map)) |
| return base::nullopt; |
| |
| const auto resident_key_option = |
| option_map.find(cbor::Value(kResidentKeyMapKey)); |
| if (resident_key_option != option_map.end()) |
| request.SetResidentKeyRequired(resident_key_option->second.GetBool()); |
| |
| const auto uv_option = |
| option_map.find(cbor::Value(kUserVerificationMapKey)); |
| if (uv_option != option_map.end()) |
| request.SetUserVerification( |
| uv_option->second.GetBool() |
| ? UserVerificationRequirement::kRequired |
| : UserVerificationRequirement::kDiscouraged); |
| } |
| |
| const auto pin_auth_it = request_map.find(cbor::Value(8)); |
| if (pin_auth_it != request_map.end()) { |
| if (!pin_auth_it->second.is_bytestring()) |
| return base::nullopt; |
| request.SetPinAuth(pin_auth_it->second.GetBytestring()); |
| } |
| |
| const auto pin_protocol_it = request_map.find(cbor::Value(9)); |
| if (pin_protocol_it != request_map.end()) { |
| if (!pin_protocol_it->second.is_unsigned() || |
| pin_protocol_it->second.GetUnsigned() > |
| std::numeric_limits<uint8_t>::max()) |
| return base::nullopt; |
| request.SetPinProtocol(pin_protocol_it->second.GetUnsigned()); |
| } |
| |
| return std::make_pair(std::move(request), |
| fido_parsing_utils::Materialize(client_data_hash)); |
| } |
| |
| base::Optional< |
| std::pair<CtapGetAssertionRequest, CtapGetAssertionRequest::ClientDataHash>> |
| ParseCtapGetAssertionRequest(base::span<const uint8_t> request_bytes) { |
| const auto& cbor_request = cbor::Reader::Read(request_bytes); |
| if (!cbor_request || !cbor_request->is_map()) |
| return base::nullopt; |
| |
| const auto& request_map = cbor_request->GetMap(); |
| if (!AreGetAssertionRequestMapKeysCorrect(request_map)) |
| return base::nullopt; |
| |
| const auto rp_id_it = request_map.find(cbor::Value(1)); |
| if (rp_id_it == request_map.end() || !rp_id_it->second.is_string()) |
| return base::nullopt; |
| |
| const auto client_data_hash_it = request_map.find(cbor::Value(2)); |
| if (client_data_hash_it == request_map.end() || |
| !client_data_hash_it->second.is_bytestring()) |
| return base::nullopt; |
| |
| const auto client_data_hash = |
| base::make_span(client_data_hash_it->second.GetBytestring()) |
| .subspan<0, kClientDataHashLength>(); |
| |
| CtapGetAssertionRequest request(rp_id_it->second.GetString(), |
| std::string() /* client_data_json */); |
| |
| const auto allow_list_it = request_map.find(cbor::Value(3)); |
| if (allow_list_it != request_map.end()) { |
| if (!allow_list_it->second.is_array()) |
| return base::nullopt; |
| |
| const auto& credential_descriptors = allow_list_it->second.GetArray(); |
| std::vector<PublicKeyCredentialDescriptor> allow_list; |
| for (const auto& credential_descriptor : credential_descriptors) { |
| auto allowed_credential = |
| PublicKeyCredentialDescriptor::CreateFromCBORValue( |
| credential_descriptor); |
| if (!allowed_credential) |
| return base::nullopt; |
| |
| allow_list.push_back(std::move(*allowed_credential)); |
| } |
| request.SetAllowList(std::move(allow_list)); |
| } |
| |
| const auto option_it = request_map.find(cbor::Value(5)); |
| if (option_it != request_map.end()) { |
| if (!option_it->second.is_map()) |
| return base::nullopt; |
| |
| const auto& option_map = option_it->second.GetMap(); |
| if (!IsGetAssertionOptionMapFormatCorrect(option_map)) |
| return base::nullopt; |
| |
| const auto user_presence_option = |
| option_map.find(cbor::Value(kUserPresenceMapKey)); |
| if (user_presence_option != option_map.end()) |
| request.SetUserPresenceRequired(user_presence_option->second.GetBool()); |
| |
| const auto uv_option = |
| option_map.find(cbor::Value(kUserVerificationMapKey)); |
| if (uv_option != option_map.end()) |
| request.SetUserVerification( |
| uv_option->second.GetBool() |
| ? UserVerificationRequirement::kRequired |
| : UserVerificationRequirement::kPreferred); |
| } |
| |
| const auto pin_auth_it = request_map.find(cbor::Value(6)); |
| if (pin_auth_it != request_map.end()) { |
| if (!pin_auth_it->second.is_bytestring()) |
| return base::nullopt; |
| request.SetPinAuth(pin_auth_it->second.GetBytestring()); |
| } |
| |
| const auto pin_protocol_it = request_map.find(cbor::Value(7)); |
| if (pin_protocol_it != request_map.end()) { |
| if (!pin_protocol_it->second.is_unsigned() || |
| pin_protocol_it->second.GetUnsigned() > |
| std::numeric_limits<uint8_t>::max()) |
| return base::nullopt; |
| request.SetPinProtocol(pin_protocol_it->second.GetUnsigned()); |
| } |
| |
| return std::make_pair(std::move(request), |
| fido_parsing_utils::Materialize(client_data_hash)); |
| } |
| |
| } // namespace device |