| // Copyright 2019 The ChromiumOS Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "u2fd/webauthn_handler.h" | |
| #include <memory> | |
| #include <optional> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include <base/check.h> | |
| #include <base/check_op.h> | |
| #include <base/functional/callback_helpers.h> | |
| #include <base/logging.h> | |
| #include <base/notreached.h> | |
| #include <base/time/time.h> | |
| #include <chromeos/cbor/values.h> | |
| #include <chromeos/cbor/writer.h> | |
| #include <chromeos/dbus/service_constants.h> | |
| #include <cryptohome/proto_bindings/UserDataAuth.pb.h> | |
| #include <cryptohome/proto_bindings/auth_factor.pb.h> | |
| #include <openssl/rand.h> | |
| #include <u2f/proto_bindings/u2f_interface.pb.h> | |
| #include "u2fd/client/user_state.h" | |
| #include "u2fd/client/util.h" | |
| #include "u2fd/u2f_command_processor.h" | |
| namespace u2f { | |
| namespace { | |
| // User a big timeout for cryptohome. See b/172945202. | |
| constexpr base::TimeDelta kCryptohomeTimeout = base::Minutes(2); | |
| constexpr int kCancelUVFlowTimeoutMs = 5000; | |
| constexpr char kAttestationFormatNone[] = "none"; | |
| // \xa0 is empty map in CBOR | |
| constexpr char kAttestationStatementNone = '\xa0'; | |
| constexpr char kAttestationFormatU2f[] = "fido-u2f"; | |
| // Keys for attestation statement CBOR map. | |
| constexpr char kSignatureKey[] = "sig"; | |
| constexpr char kX509CertKey[] = "x5c"; | |
| // The AAGUID for none-attestation (for platform-authenticator). For u2f/g2f | |
| // attestation, empty AAGUID should be used. | |
| const std::vector<uint8_t> kAaguid = {0x84, 0x03, 0x98, 0x77, 0xa5, 0x4b, | |
| 0xdf, 0xbb, 0x04, 0xa8, 0x2d, 0xf2, | |
| 0xfa, 0x2a, 0x11, 0x6e}; | |
| // AuthenticatorData flags are defined in | |
| // https://www.w3.org/TR/webauthn-2/#sctn-authenticator-data | |
| enum class AuthenticatorDataFlag : uint8_t { | |
| kTestOfUserPresence = 1u << 0, | |
| kTestOfUserVerification = 1u << 2, | |
| kAttestedCredentialData = 1u << 6, | |
| kExtensionDataIncluded = 1u << 7, | |
| }; | |
| // Relative DBus object path for fingerprint manager in biod. | |
| const char kCrosFpBiometricsManagerRelativePath[] = "/CrosFpBiometricsManager"; | |
| constexpr char kGoogleRpId[] = "google.com"; | |
| std::vector<uint8_t> Uint16ToByteVector(uint16_t value) { | |
| return std::vector<uint8_t>({static_cast<uint8_t>((value >> 8) & 0xff), | |
| static_cast<uint8_t>(value & 0xff)}); | |
| } | |
| void AppendToString(const std::vector<uint8_t>& vect, std::string* str) { | |
| str->append(reinterpret_cast<const char*>(vect.data()), vect.size()); | |
| } | |
| void AppendAttestedCredential(const std::vector<uint8_t>& credential_id, | |
| const std::vector<uint8_t>& credential_public_key, | |
| std::vector<uint8_t>* authenticator_data) { | |
| util::AppendToVector(credential_id, authenticator_data); | |
| util::AppendToVector(credential_public_key, authenticator_data); | |
| } | |
| // Returns the current time in seconds since epoch as a privacy-preserving | |
| // signature counter. Because of the conversion to a 32-bit unsigned integer, | |
| // the counter will overflow in the year 2108. | |
| std::vector<uint8_t> GetTimestampSignatureCounter() { | |
| uint32_t sign_counter = | |
| static_cast<uint32_t>(base::Time::Now().InSecondsFSinceUnixEpoch()); | |
| return std::vector<uint8_t>{ | |
| static_cast<uint8_t>((sign_counter >> 24) & 0xff), | |
| static_cast<uint8_t>((sign_counter >> 16) & 0xff), | |
| static_cast<uint8_t>((sign_counter >> 8) & 0xff), | |
| static_cast<uint8_t>(sign_counter & 0xff), | |
| }; | |
| } | |
| std::vector<uint8_t> EncodeU2fAttestationStatementInCBOR( | |
| const std::vector<uint8_t>& signature, const std::vector<uint8_t>& cert) { | |
| cbor::Value::MapValue attestation_statement_map; | |
| attestation_statement_map[cbor::Value(kSignatureKey)] = | |
| cbor::Value(signature); | |
| // The "x5c" field is an array of just one cert. | |
| std::vector<cbor::Value> certificate_array; | |
| certificate_array.push_back(cbor::Value(cert)); | |
| attestation_statement_map[cbor::Value(kX509CertKey)] = | |
| cbor::Value(std::move(certificate_array)); | |
| return *cbor::Writer::Write( | |
| cbor::Value(std::move(attestation_statement_map))); | |
| } | |
| } // namespace | |
| WebAuthnHandler::WebAuthnHandler() | |
| : user_state_(nullptr), | |
| webauthn_storage_(std::make_unique<WebAuthnStorage>()), | |
| u2f_command_processor_(std::unique_ptr<U2fCommandProcessor>()) {} | |
| WebAuthnHandler::~WebAuthnHandler() {} | |
| void WebAuthnHandler::Initialize( | |
| dbus::Bus* bus, | |
| UserState* user_state, | |
| U2fMode u2f_mode, | |
| std::unique_ptr<U2fCommandProcessor> u2f_command_processor, | |
| std::unique_ptr<AllowlistingUtil> allowlisting_util, | |
| MetricsLibraryInterface* metrics, | |
| bool enable_global_key) { | |
| if (Initialized()) { | |
| VLOG(1) << "WebAuthn handler already initialized, doing nothing."; | |
| return; | |
| } | |
| metrics_ = metrics; | |
| user_state_ = user_state; | |
| user_state_->SetSessionStartedCallback(base::BindRepeating( | |
| &WebAuthnHandler::OnSessionStarted, base::Unretained(this))); | |
| user_state_->SetSessionStoppedCallback(base::BindRepeating( | |
| &WebAuthnHandler::OnSessionStopped, base::Unretained(this))); | |
| u2f_mode_ = u2f_mode; | |
| enable_global_key_ = enable_global_key; | |
| allowlisting_util_ = std::move(allowlisting_util); | |
| bus_ = bus; | |
| auth_dialog_dbus_proxy_ = bus_->GetObjectProxy( | |
| chromeos::kUserAuthenticationServiceName, | |
| dbus::ObjectPath(chromeos::kUserAuthenticationServicePath)); | |
| // Testing can inject a mock. | |
| if (!cryptohome_proxy_) | |
| cryptohome_proxy_ = | |
| std::make_unique<org::chromium::UserDataAuthInterfaceProxy>(bus_); | |
| DCHECK(auth_dialog_dbus_proxy_); | |
| u2f_command_processor_ = std::move(u2f_command_processor); | |
| if (user_state_->HasUser()) { | |
| // WebAuthnHandler should normally initialize on boot, before any user has | |
| // logged in. If there's already a user, then we have crashed during a user | |
| // session, so catch up on the state. | |
| std::optional<std::string> user = user_state_->GetUser(); | |
| DCHECK(user); | |
| OnSessionStarted(*user); | |
| } | |
| } | |
| bool WebAuthnHandler::Initialized() { | |
| return u2f_command_processor_ && user_state_; | |
| } | |
| bool WebAuthnHandler::AllowPresenceMode() { | |
| return u2f_mode_ == U2fMode::kU2f || u2f_mode_ == U2fMode::kU2fExtended; | |
| } | |
| void WebAuthnHandler::OnSessionStarted(const std::string& account_id) { | |
| // Do this first because there's a timeout for reading the secret. | |
| GetWebAuthnSecretHashAsync(account_id); | |
| webauthn_storage_->set_allow_access(true); | |
| std::optional<std::string> sanitized_user = user_state_->GetSanitizedUser(); | |
| DCHECK(sanitized_user); | |
| webauthn_storage_->set_sanitized_user(*sanitized_user); | |
| if (!webauthn_storage_->LoadRecords()) { | |
| LOG(ERROR) << "Did not load all records for user " << *sanitized_user; | |
| return; | |
| } | |
| webauthn_storage_->SendRecordCountToUMA(metrics_); | |
| } | |
| void WebAuthnHandler::OnSessionStopped() { | |
| auth_time_secret_hash_.reset(); | |
| webauthn_storage_->Reset(); | |
| } | |
| void WebAuthnHandler::GetWebAuthnSecretHashAsync( | |
| const std::string& account_id) { | |
| user_data_auth::GetWebAuthnSecretHashRequest request; | |
| request.mutable_account_id()->set_account_id(account_id); | |
| cryptohome_proxy_->GetWebAuthnSecretHashAsync( | |
| request, | |
| base::BindOnce(&WebAuthnHandler::OnGetWebAuthnSecretHashResp, | |
| base::Unretained(this)), | |
| base::BindOnce(&WebAuthnHandler::OnGetWebAuthnSecretHashCallFailed, | |
| base::Unretained(this)), | |
| kCryptohomeTimeout.InMilliseconds()); | |
| } | |
| void WebAuthnHandler::OnGetWebAuthnSecretHashCallFailed(brillo::Error* error) { | |
| LOG(ERROR) << "Failed to call GetWebAuthnSecretHash on cryptohome, error: " | |
| << error->GetMessage(); | |
| } | |
| void WebAuthnHandler::OnGetWebAuthnSecretHashResp( | |
| const user_data_auth::GetWebAuthnSecretHashReply& reply) { | |
| if (reply.error() != | |
| user_data_auth::CryptohomeErrorCode::CRYPTOHOME_ERROR_NOT_SET) { | |
| LOG(ERROR) << "GetWebAuthnSecretHash reply has error " << reply.error(); | |
| return; | |
| } | |
| brillo::Blob secret_hash = | |
| brillo::BlobFromString(reply.webauthn_secret_hash()); | |
| if (secret_hash.size() != SHA256_DIGEST_LENGTH) { | |
| LOG(ERROR) << "WebAuthn auth time secret hash size is wrong."; | |
| return; | |
| } | |
| auth_time_secret_hash_ = | |
| std::make_unique<brillo::Blob>(std::move(secret_hash)); | |
| } | |
| void WebAuthnHandler::MakeCredential( | |
| std::unique_ptr<MakeCredentialMethodResponse> method_response, | |
| const MakeCredentialRequest& request) { | |
| MakeCredentialResponse response; | |
| VLOG(1) << "Received a MakeCredential request."; | |
| if (!Initialized()) { | |
| LOG(WARNING) << "MakeCredential: WebAuthnHandler not initialized."; | |
| response.set_status(MakeCredentialResponse::INTERNAL_ERROR); | |
| method_response->Return(response); | |
| return; | |
| } | |
| if (pending_uv_make_credential_session_ || | |
| pending_uv_get_assertion_session_) { | |
| LOG(WARNING) << "MakeCredential: There is a pending session."; | |
| response.set_status(MakeCredentialResponse::REQUEST_PENDING); | |
| method_response->Return(response); | |
| return; | |
| } | |
| if (request.rp_id().empty()) { | |
| LOG(ERROR) << "MakeCredential: Invalid request format: no rp_id."; | |
| response.set_status(MakeCredentialResponse::INVALID_REQUEST); | |
| method_response->Return(response); | |
| return; | |
| } | |
| if (request.verification_type() == VerificationType::VERIFICATION_UNKNOWN) { | |
| LOG(ERROR) << "MakeCredential: Unknown verification type."; | |
| response.set_status(MakeCredentialResponse::INVALID_REQUEST); | |
| method_response->Return(response); | |
| return; | |
| } | |
| struct MakeCredentialSession session = { | |
| static_cast<uint64_t>(base::Time::Now().ToTimeT()), request, | |
| std::move(method_response)}; | |
| if (!AllowPresenceMode()) { | |
| // Upgrade UP requests to UV. | |
| session.request.set_verification_type( | |
| VerificationType::VERIFICATION_USER_VERIFICATION); | |
| } | |
| if (session.request.verification_type() == | |
| VerificationType::VERIFICATION_USER_VERIFICATION) { | |
| dbus::MethodCall call( | |
| chromeos::kUserAuthenticationServiceInterface, | |
| chromeos::kUserAuthenticationServiceShowAuthDialogV2Method); | |
| dbus::MessageWriter writer(&call); | |
| writer.AppendString(session.request.rp_id()); | |
| writer.AppendInt32(session.request.verification_type()); | |
| writer.AppendString(session.request.request_id_str()); | |
| pending_uv_make_credential_session_ = std::move(session); | |
| auth_dialog_dbus_proxy_->CallMethod( | |
| &call, dbus::ObjectProxy::TIMEOUT_INFINITE, | |
| base::BindOnce(&WebAuthnHandler::HandleUVFlowResultMakeCredential, | |
| base::Unretained(this))); | |
| return; | |
| } | |
| DoMakeCredential(std::move(session), PresenceRequirement::kPowerButton); | |
| } | |
| CancelWebAuthnFlowResponse WebAuthnHandler::Cancel( | |
| const CancelWebAuthnFlowRequest& request) { | |
| CancelWebAuthnFlowResponse response; | |
| if (!pending_uv_make_credential_session_ && | |
| !pending_uv_get_assertion_session_) { | |
| VLOG(1) << "No pending session to cancel."; | |
| response.set_canceled(false); | |
| return response; | |
| } | |
| if (pending_uv_make_credential_session_) { | |
| if (pending_uv_make_credential_session_->request.request_id_str() != | |
| request.request_id_str()) { | |
| LOG(ERROR) << "MakeCredential session has a different request_id, not " | |
| "cancelling."; | |
| response.set_canceled(false); | |
| return response; | |
| } | |
| } | |
| if (pending_uv_get_assertion_session_) { | |
| if (pending_uv_get_assertion_session_->request.request_id_str() != | |
| request.request_id_str()) { | |
| LOG(ERROR) << "GetAssertion session has a different request_id, not " | |
| "cancelling."; | |
| response.set_canceled(false); | |
| return response; | |
| } | |
| } | |
| dbus::MethodCall call(chromeos::kUserAuthenticationServiceInterface, | |
| chromeos::kUserAuthenticationServiceCancelMethod); | |
| std::unique_ptr<dbus::Response> cancel_ui_resp = | |
| auth_dialog_dbus_proxy_->CallMethodAndBlock(&call, kCancelUVFlowTimeoutMs) | |
| .value_or(nullptr); | |
| if (!cancel_ui_resp) { | |
| LOG(ERROR) << "Failed to dismiss WebAuthn user verification UI."; | |
| response.set_canceled(false); | |
| return response; | |
| } | |
| // We do not reset |pending_uv_make_credential_session_| or | |
| // |pending_uv_get_assertion_session_| here because UI will still respond | |
| // to the cancelled request through these, though the response will be | |
| // ignored by Chrome. | |
| if (pending_uv_make_credential_session_) { | |
| pending_uv_make_credential_session_->canceled = true; | |
| } else { | |
| pending_uv_get_assertion_session_->canceled = true; | |
| } | |
| response.set_canceled(true); | |
| return response; | |
| } | |
| void WebAuthnHandler::HandleUVFlowResultMakeCredential( | |
| dbus::Response* flow_response) { | |
| MakeCredentialResponse response; | |
| DCHECK(pending_uv_make_credential_session_); | |
| if (!flow_response) { | |
| LOG(ERROR) << "User auth flow had no response."; | |
| response.set_status(MakeCredentialResponse::INTERNAL_ERROR); | |
| pending_uv_make_credential_session_->response->Return(response); | |
| pending_uv_make_credential_session_.reset(); | |
| return; | |
| } | |
| dbus::MessageReader response_reader(flow_response); | |
| bool success; | |
| if (!response_reader.PopBool(&success)) { | |
| LOG(ERROR) << "Failed to parse user auth flow result."; | |
| response.set_status(MakeCredentialResponse::INTERNAL_ERROR); | |
| pending_uv_make_credential_session_->response->Return(response); | |
| pending_uv_make_credential_session_.reset(); | |
| return; | |
| } | |
| if (!success) { | |
| if (pending_uv_make_credential_session_->canceled) { | |
| VLOG(1) << "WebAuthn MakeCredential operation canceled."; | |
| response.set_status(MakeCredentialResponse::CANCELED); | |
| } else { | |
| LOG(ERROR) << "User auth flow failed. Aborting MakeCredential."; | |
| response.set_status(MakeCredentialResponse::VERIFICATION_FAILED); | |
| } | |
| pending_uv_make_credential_session_->response->Return(response); | |
| pending_uv_make_credential_session_.reset(); | |
| return; | |
| } | |
| DoMakeCredential(std::move(*pending_uv_make_credential_session_), | |
| PresenceRequirement::kNone); | |
| pending_uv_make_credential_session_.reset(); | |
| } | |
| void WebAuthnHandler::HandleUVFlowResultGetAssertion( | |
| dbus::Response* flow_response) { | |
| GetAssertionResponse response; | |
| DCHECK(pending_uv_get_assertion_session_); | |
| if (!flow_response) { | |
| LOG(ERROR) << "User auth flow had no response."; | |
| response.set_status(GetAssertionResponse::INTERNAL_ERROR); | |
| pending_uv_get_assertion_session_->response->Return(response); | |
| pending_uv_get_assertion_session_.reset(); | |
| return; | |
| } | |
| dbus::MessageReader response_reader(flow_response); | |
| bool success; | |
| if (!response_reader.PopBool(&success)) { | |
| LOG(ERROR) << "Failed to parse user auth flow result."; | |
| response.set_status(GetAssertionResponse::INTERNAL_ERROR); | |
| pending_uv_get_assertion_session_->response->Return(response); | |
| pending_uv_get_assertion_session_.reset(); | |
| return; | |
| } | |
| if (!success) { | |
| if (pending_uv_get_assertion_session_->canceled) { | |
| VLOG(1) << "WebAuthn GetAssertion operation canceled."; | |
| response.set_status(GetAssertionResponse::CANCELED); | |
| } else { | |
| LOG(ERROR) << "User auth flow failed. Aborting GetAssertion."; | |
| response.set_status(GetAssertionResponse::VERIFICATION_FAILED); | |
| } | |
| pending_uv_get_assertion_session_->response->Return(response); | |
| pending_uv_get_assertion_session_.reset(); | |
| return; | |
| } | |
| DoGetAssertion(std::move(*pending_uv_get_assertion_session_), | |
| PresenceRequirement::kAuthorizationSecret, | |
| CredentialSecretType::kPlatformStored); | |
| pending_uv_get_assertion_session_.reset(); | |
| } | |
| void WebAuthnHandler::DoMakeCredential( | |
| struct MakeCredentialSession session, | |
| PresenceRequirement presence_requirement) { | |
| MakeCredentialResponse response; | |
| const std::vector<uint8_t> rp_id_hash = util::Sha256(session.request.rp_id()); | |
| std::vector<uint8_t> credential_id; | |
| CredentialPublicKey credential_public_key; | |
| std::vector<uint8_t> credential_key_blob; | |
| // If we are in u2f or g2f mode, and the request says it wants presence only, | |
| // make a non-versioned (i.e. non-uv-compatible) credential. | |
| bool uv_compatible = !(AllowPresenceMode() && | |
| session.request.verification_type() == | |
| VerificationType::VERIFICATION_USER_PRESENCE); | |
| CredentialSecretType secret_type; | |
| if (uv_compatible) { | |
| secret_type = CredentialSecretType::kPlatformStored; | |
| } else if (session.request.rp_id() == kGoogleRpId && | |
| u2f_mode_ == U2fMode::kU2fExtended && enable_global_key_) { | |
| secret_type = CredentialSecretType::kGlobalWellKnownSecret; | |
| } else { | |
| secret_type = CredentialSecretType::kUserSecret; | |
| } | |
| brillo::SecureBlob credential_secret(kCredentialSecretSize); | |
| switch (secret_type) { | |
| case CredentialSecretType::kPlatformStored: { | |
| if (RAND_bytes(credential_secret.data(), credential_secret.size()) != 1) { | |
| LOG(ERROR) | |
| << "MakeCredential: Failed to generate secret for new credential."; | |
| response.set_status(MakeCredentialResponse::INTERNAL_ERROR); | |
| session.response->Return(response); | |
| return; | |
| } | |
| break; | |
| } | |
| case CredentialSecretType::kUserSecret: { | |
| // We are creating a credential that can only be signed with power button | |
| // press, and can be signed by u2f/g2f, so we must use the legacy secret. | |
| std::optional<brillo::SecureBlob> legacy_secret = | |
| user_state_->GetUserSecret(); | |
| if (!legacy_secret) { | |
| LOG(ERROR) << "MakeCredential: Cannot find user secret when trying to " | |
| "create u2f/g2f credential."; | |
| response.set_status(MakeCredentialResponse::INTERNAL_ERROR); | |
| session.response->Return(response); | |
| return; | |
| } | |
| credential_secret = std::move(*legacy_secret); | |
| break; | |
| } | |
| case CredentialSecretType::kGlobalWellKnownSecret: { | |
| credential_secret = brillo::SecureBlob(kUserSecretSizeBytes); | |
| break; | |
| } | |
| } | |
| MakeCredentialResponse::MakeCredentialStatus generate_status = | |
| u2f_command_processor_->U2fGenerate( | |
| rp_id_hash, credential_secret, presence_requirement, uv_compatible, | |
| auth_time_secret_hash_.get(), &credential_id, &credential_public_key, | |
| &credential_key_blob); | |
| if (generate_status != MakeCredentialResponse::SUCCESS) { | |
| LOG(ERROR) << "MakeCredential: U2fGenerate failed with status " | |
| << static_cast<int>(generate_status) << "."; | |
| response.set_status(generate_status); | |
| session.response->Return(response); | |
| return; | |
| } | |
| if (credential_id.empty() || credential_public_key.cbor.empty()) { | |
| LOG(ERROR) << "MakeCredential: Returned credential is empty."; | |
| response.set_status(MakeCredentialResponse::INTERNAL_ERROR); | |
| session.response->Return(response); | |
| return; | |
| } | |
| auto ret = HasExcludedCredentials(session.request); | |
| if (ret == HasCredentialsResponse::INTERNAL_ERROR) { | |
| LOG(ERROR) << "MakeCredential: HasExcludedCredentials failed with an " | |
| "internal error."; | |
| response.set_status(MakeCredentialResponse::INTERNAL_ERROR); | |
| session.response->Return(response); | |
| return; | |
| } else if (ret == HasCredentialsResponse::SUCCESS) { | |
| LOG(ERROR) << "MakeCredential: Credential is excluded in the request."; | |
| response.set_status(MakeCredentialResponse::EXCLUDED_CREDENTIAL_ID); | |
| session.response->Return(response); | |
| return; | |
| } | |
| const std::optional<std::vector<uint8_t>> authenticator_data = | |
| MakeAuthenticatorData( | |
| rp_id_hash, credential_id, credential_public_key.cbor, | |
| /* user_verified = */ session.request.verification_type() == | |
| VerificationType::VERIFICATION_USER_VERIFICATION, | |
| /* include_attested_credential_data = */ true, secret_type); | |
| if (!authenticator_data) { | |
| LOG(ERROR) << "MakeCredential: MakeAuthenticatorData failed"; | |
| response.set_status(MakeCredentialResponse::INTERNAL_ERROR); | |
| session.response->Return(response); | |
| return; | |
| } | |
| AppendToString(*authenticator_data, response.mutable_authenticator_data()); | |
| // If a credential is not UV-compatible, it is a legacy U2F/G2F credential | |
| // and should come with U2F/G2F attestation for backward compatibility. | |
| if (uv_compatible) { | |
| AppendNoneAttestation(&response); | |
| } else { | |
| if (credential_public_key.raw.empty()) { | |
| LOG(ERROR) << "MakeCredential: Authenticator doesn't support FIDO U2F " | |
| "attestation statement format."; | |
| response.set_status(MakeCredentialResponse::INTERNAL_ERROR); | |
| session.response->Return(response); | |
| return; | |
| } | |
| std::optional<std::vector<uint8_t>> attestation_statement = | |
| MakeFidoU2fAttestationStatement( | |
| rp_id_hash, util::ToVector(session.request.client_data_hash()), | |
| credential_public_key.raw, credential_id, credential_secret, | |
| session.request.attestation_conveyance_preference()); | |
| if (!attestation_statement) { | |
| LOG(ERROR) | |
| << "MakeCredential: Failed to make FIDO attestation statement."; | |
| response.set_status(MakeCredentialResponse::INTERNAL_ERROR); | |
| session.response->Return(response); | |
| return; | |
| } | |
| response.set_attestation_format(kAttestationFormatU2f); | |
| AppendToString(*attestation_statement, | |
| response.mutable_attestation_statement()); | |
| } | |
| // u2f/g2f credentials should not be written to record. | |
| if (uv_compatible) { | |
| // All steps succeeded, so write to record. | |
| WebAuthnRecord record; | |
| AppendToString(credential_id, &record.credential_id); | |
| // Because the credential secret is more like a salt in the protocol | |
| // and loading too much secure blob might cause RLIMIT_MEMLOCK, the | |
| // underlying storage class use blob to handle it. Logically it's still | |
| // a secret so we don't want to change interfaces elsewhere to take a blob, | |
| // instead just perform the conversion here. | |
| record.secret = | |
| brillo::Blob(credential_secret.begin(), credential_secret.end()); | |
| record.key_blob = std::move(credential_key_blob); | |
| record.rp_id = session.request.rp_id(); | |
| record.rp_display_name = session.request.rp_display_name(); | |
| record.user_id = session.request.user_id(); | |
| record.user_display_name = session.request.user_display_name(); | |
| record.timestamp = base::Time::Now().InSecondsFSinceUnixEpoch(); | |
| record.is_resident_key = session.request.resident_key_required(); | |
| if (!webauthn_storage_->WriteRecord(std::move(record))) { | |
| LOG(ERROR) | |
| << "MakeCredential: Failed to write record into WebAuthn storage."; | |
| response.set_status(MakeCredentialResponse::INTERNAL_ERROR); | |
| session.response->Return(response); | |
| return; | |
| } | |
| } | |
| VLOG(1) << "Finished processing MakeCredential request."; | |
| response.set_status(MakeCredentialResponse::SUCCESS); | |
| session.response->Return(response); | |
| } | |
| // AuthenticatorData layout: | |
| // (See https://www.w3.org/TR/webauthn-2/#table-authData) | |
| // ----------------------------------------------------------------------- | |
| // | RP ID hash: 32 bytes | |
| // | Flags: 1 byte | |
| // | Signature counter: 4 bytes | |
| // | ------------------------------------------- | |
| // | | AAGUID: 16 bytes | |
| // | Attested Credential Data: | Credential ID length (L): 2 bytes | |
| // | (if present) | Credential ID: L bytes | |
| // | | Credential public key: variable length | |
| std::optional<std::vector<uint8_t>> WebAuthnHandler::MakeAuthenticatorData( | |
| const std::vector<uint8_t>& rp_id_hash, | |
| const std::vector<uint8_t>& credential_id, | |
| const std::vector<uint8_t>& credential_public_key, | |
| bool user_verified, | |
| bool include_attested_credential_data, | |
| CredentialSecretType secret_type) { | |
| std::vector<uint8_t> authenticator_data(rp_id_hash); | |
| uint8_t flags = | |
| static_cast<uint8_t>(AuthenticatorDataFlag::kTestOfUserPresence); | |
| if (user_verified) | |
| flags |= | |
| static_cast<uint8_t>(AuthenticatorDataFlag::kTestOfUserVerification); | |
| if (include_attested_credential_data) | |
| flags |= | |
| static_cast<uint8_t>(AuthenticatorDataFlag::kAttestedCredentialData); | |
| authenticator_data.emplace_back(flags); | |
| // The U2F authenticator keeps a user-global signature counter in UserState. | |
| // For platform authenticator credentials, we derive a counter from a | |
| // timestamp instead. | |
| if (secret_type == CredentialSecretType::kUserSecret) { | |
| std::optional<std::vector<uint8_t>> counter = user_state_->GetCounter(); | |
| if (!counter || !user_state_->IncrementCounter()) { | |
| // UserState logs an error in this case. | |
| return std::nullopt; | |
| } | |
| util::AppendToVector(*counter, &authenticator_data); | |
| } else { | |
| util::AppendToVector(GetTimestampSignatureCounter(), &authenticator_data); | |
| } | |
| if (include_attested_credential_data) { | |
| util::AppendToVector(secret_type == CredentialSecretType::kPlatformStored | |
| ? kAaguid | |
| : std::vector<uint8_t>(kAaguid.size(), 0), | |
| &authenticator_data); | |
| uint16_t length = credential_id.size(); | |
| util::AppendToVector(Uint16ToByteVector(length), &authenticator_data); | |
| AppendAttestedCredential(credential_id, credential_public_key, | |
| &authenticator_data); | |
| } | |
| return authenticator_data; | |
| } | |
| void WebAuthnHandler::AppendNoneAttestation(MakeCredentialResponse* response) { | |
| response->set_attestation_format(kAttestationFormatNone); | |
| response->mutable_attestation_statement()->push_back( | |
| kAttestationStatementNone); | |
| } | |
| std::optional<std::vector<uint8_t>> | |
| WebAuthnHandler::MakeFidoU2fAttestationStatement( | |
| const std::vector<uint8_t>& app_id, | |
| const std::vector<uint8_t>& challenge, | |
| const std::vector<uint8_t>& pub_key, | |
| const std::vector<uint8_t>& key_handle, | |
| const brillo::SecureBlob& credential_secret, | |
| const MakeCredentialRequest::AttestationConveyancePreference | |
| attestation_conveyance_preference) { | |
| std::vector<uint8_t> attestation_cert; | |
| std::vector<uint8_t> signature; | |
| if (attestation_conveyance_preference == MakeCredentialRequest::G2F && | |
| u2f_mode_ == U2fMode::kU2fExtended) { | |
| MakeCredentialResponse::MakeCredentialStatus attest_status = | |
| u2f_command_processor_->G2fAttest(app_id, credential_secret, challenge, | |
| pub_key, key_handle, | |
| &attestation_cert, &signature); | |
| if (attest_status != MakeCredentialResponse::SUCCESS) { | |
| LOG(ERROR) << "Failed to do G2f attestation for MakeCredential"; | |
| return std::nullopt; | |
| } | |
| if (allowlisting_util_ != nullptr && | |
| !allowlisting_util_->AppendDataToCert(&attestation_cert)) { | |
| LOG(ERROR) << "Failed to get allowlisting data for G2F Enroll Request"; | |
| return std::nullopt; | |
| } | |
| } else { | |
| if (!u2f_command_processor_->G2fSoftwareAttest( | |
| app_id, challenge, pub_key, key_handle, &attestation_cert, | |
| &signature)) { | |
| LOG(ERROR) << "Failed to do software attestation for MakeCredential"; | |
| return std::nullopt; | |
| } | |
| } | |
| return EncodeU2fAttestationStatementInCBOR(signature, attestation_cert); | |
| } | |
| HasCredentialsResponse::HasCredentialsStatus | |
| WebAuthnHandler::HasExcludedCredentials(const MakeCredentialRequest& request) { | |
| MatchedCredentials matched = | |
| FindMatchedCredentials(request.excluded_credential_id(), request.rp_id(), | |
| request.app_id_exclude()); | |
| if (matched.has_internal_error) { | |
| return HasCredentialsResponse::INTERNAL_ERROR; | |
| } | |
| if (matched.platform_credentials.empty() && | |
| matched.legacy_credentials_for_rp_id.empty() && | |
| matched.legacy_credentials_for_app_id.empty()) { | |
| return HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID; | |
| } | |
| return HasCredentialsResponse::SUCCESS; | |
| } | |
| void WebAuthnHandler::GetAssertion( | |
| std::unique_ptr<GetAssertionMethodResponse> method_response, | |
| const GetAssertionRequest& request) { | |
| VLOG(1) << "Received a GetAssertion request."; | |
| GetAssertionResponse response; | |
| if (!Initialized()) { | |
| LOG(WARNING) << "GetAssertion: WebAuthnHandler not initialized."; | |
| response.set_status(GetAssertionResponse::INTERNAL_ERROR); | |
| method_response->Return(response); | |
| return; | |
| } | |
| if (pending_uv_make_credential_session_ || | |
| pending_uv_get_assertion_session_) { | |
| LOG(WARNING) << "GetAssertion: There is a pending session."; | |
| response.set_status(GetAssertionResponse::REQUEST_PENDING); | |
| method_response->Return(response); | |
| return; | |
| } | |
| if (request.rp_id().empty() || | |
| request.client_data_hash().size() != SHA256_DIGEST_LENGTH) { | |
| LOG(ERROR) << "GetAssertion: Invalid request format: no rp_id or incorrect " | |
| "hash length."; | |
| response.set_status(GetAssertionResponse::INVALID_REQUEST); | |
| method_response->Return(response); | |
| return; | |
| } | |
| if (request.verification_type() == VerificationType::VERIFICATION_UNKNOWN) { | |
| LOG(ERROR) << "GetAssertion: Unknown verification type."; | |
| response.set_status(GetAssertionResponse::INVALID_REQUEST); | |
| method_response->Return(response); | |
| return; | |
| } | |
| // TODO(b/180502218): Support resident credentials. | |
| std::string* credential_to_use; | |
| bool is_legacy_credential = false; | |
| bool use_app_id = false; | |
| bool use_global_user_secret = false; | |
| MatchedCredentials matched = FindMatchedCredentials( | |
| request.allowed_credential_id(), request.rp_id(), request.app_id()); | |
| if (matched.has_internal_error) { | |
| LOG(ERROR) << "GetAssertion: FindMatchedCredentials failed with an " | |
| "internal error."; | |
| response.set_status(GetAssertionResponse::INTERNAL_ERROR); | |
| method_response->Return(response); | |
| return; | |
| } | |
| if (!matched.platform_credentials.empty()) { | |
| credential_to_use = &matched.platform_credentials[0]; | |
| } else if (!matched.legacy_credentials_for_rp_id.empty()) { | |
| credential_to_use = &matched.legacy_credentials_for_rp_id[0]; | |
| is_legacy_credential = true; | |
| } else if (!matched.legacy_credentials_for_app_id.empty()) { | |
| credential_to_use = &matched.legacy_credentials_for_app_id[0]; | |
| is_legacy_credential = true; | |
| use_app_id = true; | |
| } else if (!matched.global_legacy_credentials.empty()) { | |
| credential_to_use = &matched.global_legacy_credentials[0]; | |
| is_legacy_credential = true; | |
| use_global_user_secret = true; | |
| } else { | |
| LOG(ERROR) << "GetAssertion: Failed to find matched credentials"; | |
| response.set_status(GetAssertionResponse::UNKNOWN_CREDENTIAL_ID); | |
| method_response->Return(response); | |
| return; | |
| } | |
| struct GetAssertionSession session = { | |
| static_cast<uint64_t>(base::Time::Now().ToTimeT()), request, | |
| *credential_to_use, std::move(method_response)}; | |
| if (use_app_id) { | |
| // App id was matched instead of rp id, so discard rp id. | |
| session.request.set_rp_id(request.app_id()); | |
| } | |
| if (!AllowPresenceMode()) { | |
| // Upgrade UP requests to UV. | |
| session.request.set_verification_type( | |
| VerificationType::VERIFICATION_USER_VERIFICATION); | |
| } | |
| // Legacy credentials should go through power button, not UV. | |
| if (session.request.verification_type() == | |
| VerificationType::VERIFICATION_USER_VERIFICATION && | |
| !is_legacy_credential) { | |
| dbus::MethodCall call( | |
| chromeos::kUserAuthenticationServiceInterface, | |
| chromeos::kUserAuthenticationServiceShowAuthDialogV2Method); | |
| dbus::MessageWriter writer(&call); | |
| writer.AppendString(session.request.rp_id()); | |
| writer.AppendInt32(session.request.verification_type()); | |
| writer.AppendString(session.request.request_id_str()); | |
| pending_uv_get_assertion_session_ = std::move(session); | |
| auth_dialog_dbus_proxy_->CallMethod( | |
| &call, dbus::ObjectProxy::TIMEOUT_INFINITE, | |
| base::BindOnce(&WebAuthnHandler::HandleUVFlowResultGetAssertion, | |
| base::Unretained(this))); | |
| return; | |
| } | |
| DoGetAssertion(std::move(session), PresenceRequirement::kPowerButton, | |
| use_global_user_secret | |
| ? CredentialSecretType::kGlobalWellKnownSecret | |
| : CredentialSecretType::kUserSecret); | |
| } | |
| // If already seeing failure, then no need to get user secret. This means | |
| // in the fingerprint case, this signal should ideally come from UI instead of | |
| // biod because only UI knows about retry. | |
| void WebAuthnHandler::DoGetAssertion(struct GetAssertionSession session, | |
| PresenceRequirement presence_requirement, | |
| CredentialSecretType secret_type) { | |
| GetAssertionResponse response; | |
| brillo::SecureBlob credential_secret; | |
| std::vector<uint8_t> credential_key_blob; | |
| // Set credential_secret depending on the secret_type. | |
| switch (secret_type) { | |
| case CredentialSecretType::kPlatformStored: { | |
| if (!webauthn_storage_->GetSecretAndKeyBlobByCredentialId( | |
| session.credential_id, &credential_secret, | |
| &credential_key_blob)) { | |
| LOG(ERROR) << "GetAssertion: No credential secret for credential id " | |
| << session.credential_id << ", aborting GetAssertion."; | |
| response.set_status(GetAssertionResponse::UNKNOWN_CREDENTIAL_ID); | |
| session.response->Return(response); | |
| return; | |
| } | |
| break; | |
| } | |
| case CredentialSecretType::kUserSecret: { | |
| std::optional<brillo::SecureBlob> legacy_secret = | |
| user_state_->GetUserSecret(); | |
| if (!legacy_secret) { | |
| LOG(ERROR) | |
| << "GetAssertion: Cannot find user secret when trying to sign " | |
| "u2fhid credentials"; | |
| response.set_status(GetAssertionResponse::INTERNAL_ERROR); | |
| session.response->Return(response); | |
| return; | |
| } | |
| credential_secret = std::move(*legacy_secret); | |
| break; | |
| } | |
| case CredentialSecretType::kGlobalWellKnownSecret: { | |
| credential_secret = brillo::SecureBlob(kUserSecretSizeBytes); | |
| break; | |
| } | |
| } | |
| const std::vector<uint8_t> rp_id_hash = util::Sha256(session.request.rp_id()); | |
| const std::optional<std::vector<uint8_t>> authenticator_data = | |
| MakeAuthenticatorData( | |
| rp_id_hash, std::vector<uint8_t>(), std::vector<uint8_t>(), | |
| // If presence requirement is "power button" then the user was not | |
| // verified. Otherwise the user was verified through UI. | |
| /* user_verified = */ presence_requirement != | |
| PresenceRequirement::kPowerButton, | |
| /* include_attested_credential_data = */ false, secret_type); | |
| if (!authenticator_data) { | |
| LOG(ERROR) << "GetAssertion: MakeAuthenticatorData failed"; | |
| response.set_status(GetAssertionResponse::INTERNAL_ERROR); | |
| session.response->Return(response); | |
| return; | |
| } | |
| std::vector<uint8_t> data_to_sign(*authenticator_data); | |
| util::AppendToVector(session.request.client_data_hash(), &data_to_sign); | |
| std::vector<uint8_t> hash_to_sign = util::Sha256(data_to_sign); | |
| std::vector<uint8_t> signature; | |
| GetAssertionResponse::GetAssertionStatus sign_status = | |
| u2f_command_processor_->U2fSign(rp_id_hash, hash_to_sign, | |
| util::ToVector(session.credential_id), | |
| credential_secret, &credential_key_blob, | |
| presence_requirement, &signature); | |
| response.set_status(sign_status); | |
| if (sign_status != GetAssertionResponse::SUCCESS) { | |
| LOG(ERROR) << "GetAssertion: U2fSign failed with status " | |
| << static_cast<int>(sign_status) << "."; | |
| session.response->Return(response); | |
| return; | |
| } | |
| VLOG(1) << "Finished processing GetAssertion request."; | |
| auto* assertion = response.add_assertion(); | |
| assertion->set_credential_id(session.credential_id); | |
| AppendToString(*authenticator_data, assertion->mutable_authenticator_data()); | |
| AppendToString(signature, assertion->mutable_signature()); | |
| session.response->Return(response); | |
| } | |
| MatchedCredentials WebAuthnHandler::FindMatchedCredentials( | |
| const RepeatedPtrField<std::string>& all_credentials, | |
| const std::string& rp_id, | |
| const std::string& app_id) { | |
| const brillo::SecureBlob global_user_secret(kUserSecretSizeBytes); | |
| std::vector<uint8_t> rp_id_hash = util::Sha256(rp_id); | |
| std::vector<uint8_t> app_id_hash = util::Sha256(app_id); | |
| MatchedCredentials result; | |
| // Platform authenticator credentials. | |
| for (const auto& credential_id : all_credentials) { | |
| brillo::SecureBlob credential_secret; | |
| std::vector<uint8_t> credential_key_blob; | |
| if (!webauthn_storage_->GetSecretAndKeyBlobByCredentialId( | |
| credential_id, &credential_secret, &credential_key_blob)) | |
| continue; | |
| auto ret = u2f_command_processor_->U2fSignCheckOnly( | |
| rp_id_hash, util::ToVector(credential_id), credential_secret, | |
| &credential_key_blob); | |
| if (ret == HasCredentialsResponse::INTERNAL_ERROR) { | |
| LOG(ERROR) << "U2fSignCheckOnly failed with an internal error."; | |
| result.has_internal_error = true; | |
| return result; | |
| } else if (ret == HasCredentialsResponse::SUCCESS) { | |
| result.platform_credentials.emplace_back(credential_id); | |
| } | |
| } | |
| if (enable_global_key_) { | |
| // Credentials created by global user secret (an all-zero salt). | |
| for (const auto& credential_id : all_credentials) { | |
| // We only need to match using rp_id because we were no longer using | |
| // app_id for new credentials by the time we start using the global user | |
| // secret. | |
| HasCredentialsResponse::HasCredentialsStatus ret = | |
| u2f_command_processor_->U2fSignCheckOnly( | |
| rp_id_hash, util::ToVector(credential_id), global_user_secret, | |
| nullptr); | |
| DCHECK(HasCredentialsResponse::HasCredentialsStatus_IsValid(ret)); | |
| switch (ret) { | |
| case HasCredentialsResponse::SUCCESS: | |
| // rp_id matched, it's a credential registered with u2fhid on WebAuthn | |
| // API. | |
| result.global_legacy_credentials.emplace_back(credential_id); | |
| continue; | |
| case HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID: | |
| break; | |
| case HasCredentialsResponse::UNKNOWN: | |
| case HasCredentialsResponse::INVALID_REQUEST: | |
| case HasCredentialsResponse::INTERNAL_ERROR: | |
| LOG(ERROR) << "U2fSignCheckOnly failed with an internal error."; | |
| result.has_internal_error = true; | |
| return result; | |
| default: | |
| LOG(ERROR) << "Invalid HasCredentialsStatus proto value: " << ret | |
| << "."; | |
| result.has_internal_error = true; | |
| return result; | |
| } | |
| } | |
| } | |
| const std::optional<brillo::SecureBlob> user_secret = | |
| user_state_->GetUserSecret(); | |
| if (!user_secret) { | |
| LOG(WARNING) << "Failed to get user secret."; | |
| return result; | |
| } | |
| // Legacy credentials. If a legacy credential matches both rp_id and app_id, | |
| // it will only appear in result.legacy_credentials_for_rp_id. | |
| for (const auto& credential_id : all_credentials) { | |
| // First try matching rp_id. | |
| HasCredentialsResponse::HasCredentialsStatus ret = | |
| u2f_command_processor_->U2fSignCheckOnly( | |
| rp_id_hash, util::ToVector(credential_id), *user_secret, nullptr); | |
| DCHECK(HasCredentialsResponse::HasCredentialsStatus_IsValid(ret)); | |
| switch (ret) { | |
| case HasCredentialsResponse::SUCCESS: | |
| // rp_id matched, it's a credential registered with u2fhid on WebAuthn | |
| // API. | |
| result.legacy_credentials_for_rp_id.emplace_back(credential_id); | |
| continue; | |
| case HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID: | |
| break; | |
| case HasCredentialsResponse::UNKNOWN: | |
| case HasCredentialsResponse::INVALID_REQUEST: | |
| case HasCredentialsResponse::INTERNAL_ERROR: | |
| LOG(ERROR) << "U2fSignCheckOnly failed with an internal error."; | |
| result.has_internal_error = true; | |
| return result; | |
| case google::protobuf::kint32min: | |
| case google::protobuf::kint32max: | |
| NOTREACHED(); | |
| } | |
| // Try matching app_id. | |
| ret = u2f_command_processor_->U2fSignCheckOnly( | |
| app_id_hash, util::ToVector(credential_id), *user_secret, nullptr); | |
| DCHECK(HasCredentialsResponse::HasCredentialsStatus_IsValid(ret)); | |
| switch (ret) { | |
| case HasCredentialsResponse::SUCCESS: | |
| // App id extension matched. It's a legacy credential registered with | |
| // the U2F interface. | |
| result.legacy_credentials_for_app_id.emplace_back(credential_id); | |
| continue; | |
| case HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID: | |
| break; | |
| case HasCredentialsResponse::UNKNOWN: | |
| case HasCredentialsResponse::INVALID_REQUEST: | |
| case HasCredentialsResponse::INTERNAL_ERROR: | |
| LOG(ERROR) << "U2fSignCheckOnly failed with an internal error."; | |
| result.has_internal_error = true; | |
| return result; | |
| case google::protobuf::kint32min: | |
| case google::protobuf::kint32max: | |
| NOTREACHED(); | |
| } | |
| } | |
| return result; | |
| } | |
| HasCredentialsResponse WebAuthnHandler::HasCredentials( | |
| const HasCredentialsRequest& request) { | |
| HasCredentialsResponse response; | |
| if (!Initialized()) { | |
| LOG(WARNING) << "HasCredentials: WebAuthnHandler not initialized."; | |
| response.set_status(HasCredentialsResponse::INTERNAL_ERROR); | |
| return response; | |
| } | |
| if (request.rp_id().empty() || request.credential_id().empty()) { | |
| LOG(ERROR) << "HasCredentials: empty rp_id or credential_id."; | |
| response.set_status(HasCredentialsResponse::INVALID_REQUEST); | |
| return response; | |
| } | |
| MatchedCredentials matched = FindMatchedCredentials( | |
| request.credential_id(), request.rp_id(), request.app_id()); | |
| if (matched.has_internal_error) { | |
| LOG(ERROR) << "HasCredentials: FindMatchedCredentials failed with an " | |
| "internal error."; | |
| response.set_status(HasCredentialsResponse::INTERNAL_ERROR); | |
| return response; | |
| } | |
| for (const auto& credential_id : matched.platform_credentials) { | |
| *response.add_credential_id() = credential_id; | |
| } | |
| for (const auto& credential_id : matched.global_legacy_credentials) { | |
| *response.add_credential_id() = credential_id; | |
| } | |
| for (const auto& credential_id : matched.legacy_credentials_for_rp_id) { | |
| *response.add_credential_id() = credential_id; | |
| } | |
| for (const auto& credential_id : matched.legacy_credentials_for_app_id) { | |
| *response.add_credential_id() = credential_id; | |
| } | |
| response.set_status((response.credential_id_size() > 0) | |
| ? HasCredentialsResponse::SUCCESS | |
| : HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID); | |
| return response; | |
| } | |
| HasCredentialsResponse WebAuthnHandler::HasLegacyCredentials( | |
| const HasCredentialsRequest& request) { | |
| HasCredentialsResponse response; | |
| if (!Initialized()) { | |
| LOG(WARNING) << "HasCredentHasLegacyCredentialsials: WebAuthnHandler not " | |
| "initialized."; | |
| response.set_status(HasCredentialsResponse::INTERNAL_ERROR); | |
| return response; | |
| } | |
| if (request.credential_id().empty()) { | |
| LOG(ERROR) << "HasLegacyCredentials: credential_id is empty."; | |
| response.set_status(HasCredentialsResponse::INVALID_REQUEST); | |
| return response; | |
| } | |
| MatchedCredentials matched = FindMatchedCredentials( | |
| request.credential_id(), request.rp_id(), request.app_id()); | |
| if (matched.has_internal_error) { | |
| LOG(ERROR) << "HasLegacyCredentials: FindMatchedCredentials failed with an " | |
| "internal error."; | |
| response.set_status(HasCredentialsResponse::INTERNAL_ERROR); | |
| return response; | |
| } | |
| // Do not include platform credentials. | |
| for (const auto& credential_id : matched.global_legacy_credentials) { | |
| *response.add_credential_id() = credential_id; | |
| } | |
| for (const auto& credential_id : matched.legacy_credentials_for_rp_id) { | |
| *response.add_credential_id() = credential_id; | |
| } | |
| for (const auto& credential_id : matched.legacy_credentials_for_app_id) { | |
| *response.add_credential_id() = credential_id; | |
| } | |
| response.set_status((response.credential_id_size() > 0) | |
| ? HasCredentialsResponse::SUCCESS | |
| : HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID); | |
| return response; | |
| } | |
| IsPlatformAuthenticatorInitializedResponse | |
| WebAuthnHandler::IsPlatformAuthenticatorInitialized( | |
| const IsPlatformAuthenticatorInitializedRequest& request) { | |
| IsPlatformAuthenticatorInitializedResponse response; | |
| response.set_initialized(Initialized()); | |
| return response; | |
| } | |
| IsU2fEnabledResponse WebAuthnHandler::IsU2fEnabled( | |
| const IsU2fEnabledRequest& request) { | |
| IsU2fEnabledResponse response; | |
| response.set_enabled(AllowPresenceMode()); | |
| return response; | |
| } | |
| void WebAuthnHandler::IsUvpaa( | |
| std::unique_ptr<IsUvpaaMethodResponse> method_response, | |
| const IsUvpaaRequest& request) { | |
| // Checking with the authentication dialog (in Ash) will not work, because | |
| // currently in Chrome the IsUvpaa is a blocking call, and Ash can't respond | |
| // to us since it runs in the same process as Chrome. After the Chrome side | |
| // is refactored to take a callback or Ash is split into a separate binary, | |
| // we can change the implementation here to query with Ash. | |
| IsUvpaaResponse response; | |
| if (!Initialized()) { | |
| LOG(WARNING) << "IsUvpaa: WebAuthnHandler not initialized."; | |
| response.set_not_available(true); | |
| method_response->Return(response); | |
| return; | |
| } | |
| std::optional<std::string> account_id = user_state_->GetUser(); | |
| if (!account_id) { | |
| LOG(WARNING) << "IsUvpaa: No user."; | |
| response.set_not_available(true); | |
| method_response->Return(response); | |
| return; | |
| } | |
| if (!auth_time_secret_hash_) { | |
| LOG(WARNING) << "IsUvpaa: No auth-time secret hash."; | |
| response.set_not_available(true); | |
| method_response->Return(response); | |
| return; | |
| } | |
| response.set_not_available(false); | |
| method_response->Return(response); | |
| } | |
| CountCredentialsInTimeRangeResponse | |
| WebAuthnHandler::CountCredentialsInTimeRange( | |
| const CountCredentialsInTimeRangeRequest& request) { | |
| CountCredentialsInTimeRangeResponse response; | |
| if (!Initialized()) { | |
| response.set_status(CountCredentialsInTimeRangeResponse::INTERNAL_ERROR); | |
| return response; | |
| } | |
| int64_t created_not_before = request.created_not_before_seconds(); | |
| int64_t created_not_after = request.created_not_after_seconds(); | |
| if (created_not_before > created_not_after) { | |
| response.set_status(CountCredentialsInTimeRangeResponse::INVALID_REQUEST); | |
| return response; | |
| } | |
| response.set_num_credentials(webauthn_storage_->CountRecordsInTimeRange( | |
| created_not_before, created_not_after)); | |
| response.set_status(CountCredentialsInTimeRangeResponse::SUCCESS); | |
| return response; | |
| } | |
| DeleteCredentialsInTimeRangeResponse | |
| WebAuthnHandler::DeleteCredentialsInTimeRange( | |
| const DeleteCredentialsInTimeRangeRequest& request) { | |
| DeleteCredentialsInTimeRangeResponse response; | |
| if (!Initialized()) { | |
| response.set_status(DeleteCredentialsInTimeRangeResponse::INTERNAL_ERROR); | |
| return response; | |
| } | |
| int64_t created_not_before = request.created_not_before_seconds(); | |
| int64_t created_not_after = request.created_not_after_seconds(); | |
| if (created_not_before > created_not_after) { | |
| response.set_status(DeleteCredentialsInTimeRangeResponse::INVALID_REQUEST); | |
| return response; | |
| } | |
| response.set_num_credentials_deleted( | |
| webauthn_storage_->DeleteRecordsInTimeRange(created_not_before, | |
| created_not_after)); | |
| response.set_status(DeleteCredentialsInTimeRangeResponse::SUCCESS); | |
| return response; | |
| } | |
| GetAlgorithmsResponse WebAuthnHandler::GetAlgorithms( | |
| const GetAlgorithmsRequest&) { | |
| GetAlgorithmsResponse response; | |
| if (!Initialized()) { | |
| response.set_status(GetAlgorithmsResponse::INTERNAL_ERROR); | |
| return response; | |
| } | |
| response.set_status(GetAlgorithmsResponse::SUCCESS); | |
| response.add_algorithm( | |
| static_cast<int32_t>(u2f_command_processor_->GetAlgorithm())); | |
| return response; | |
| } | |
| GetSupportedFeaturesResponse WebAuthnHandler::GetSupportedFeatures( | |
| const GetSupportedFeaturesRequest& request) { | |
| GetSupportedFeaturesResponse response; | |
| response.set_support_lacros(true); | |
| return response; | |
| } | |
| bool WebAuthnHandler::HasPin(const std::string& account_id) { | |
| user_data_auth::ListAuthFactorsRequest request; | |
| request.mutable_account_id()->set_account_id(account_id); | |
| user_data_auth::ListAuthFactorsReply reply; | |
| brillo::ErrorPtr error; | |
| if (!cryptohome_proxy_->ListAuthFactors( | |
| request, &reply, &error, kCryptohomeTimeout.InMilliseconds())) { | |
| LOG(ERROR) << "Cannot query PIN availability from cryptohome, error: " | |
| << error->GetMessage(); | |
| return false; | |
| } | |
| if (reply.error() != | |
| user_data_auth::CryptohomeErrorCode::CRYPTOHOME_ERROR_NOT_SET) { | |
| LOG(ERROR) << "ListAuthFactors response has error " << reply.error(); | |
| return false; | |
| } | |
| for (const auto& factor : reply.configured_auth_factors()) { | |
| if (factor.type() == user_data_auth::AUTH_FACTOR_TYPE_PIN) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool WebAuthnHandler::HasFingerprint(const std::string& sanitized_user) { | |
| dbus::ObjectProxy* biod_proxy = bus_->GetObjectProxy( | |
| biod::kBiodServiceName, | |
| dbus::ObjectPath(std::string(biod::kBiodServicePath) | |
| .append(kCrosFpBiometricsManagerRelativePath))); | |
| dbus::MethodCall method_call(biod::kBiometricsManagerInterface, | |
| biod::kBiometricsManagerGetRecordsForUserMethod); | |
| dbus::MessageWriter method_writer(&method_call); | |
| method_writer.AppendString(sanitized_user); | |
| std::unique_ptr<dbus::Response> response = | |
| biod_proxy | |
| ->CallMethodAndBlock(&method_call, | |
| dbus::ObjectProxy::TIMEOUT_USE_DEFAULT) | |
| .value_or(nullptr); | |
| if (!response) { | |
| LOG(ERROR) | |
| << "Cannot check fingerprint availability: no response from biod."; | |
| return false; | |
| } | |
| dbus::MessageReader response_reader(response.get()); | |
| dbus::MessageReader records_reader(nullptr); | |
| if (!response_reader.PopArray(&records_reader)) { | |
| LOG(ERROR) << "Cannot parse GetRecordsForUser response from biod."; | |
| return false; | |
| } | |
| int records_count = 0; | |
| while (records_reader.HasMoreData()) { | |
| dbus::ObjectPath record_path; | |
| if (!records_reader.PopObjectPath(&record_path)) { | |
| LOG(WARNING) << "Cannot parse fingerprint record path"; | |
| continue; | |
| } | |
| records_count++; | |
| } | |
| return records_count > 0; | |
| } | |
| void WebAuthnHandler::SetWebAuthnStorageForTesting( | |
| std::unique_ptr<WebAuthnStorage> storage) { | |
| webauthn_storage_ = std::move(storage); | |
| } | |
| void WebAuthnHandler::SetCryptohomeInterfaceProxyForTesting( | |
| std::unique_ptr<org::chromium::UserDataAuthInterfaceProxyInterface> | |
| cryptohome_proxy) { | |
| cryptohome_proxy_ = std::move(cryptohome_proxy); | |
| } | |
| } // namespace u2f |