| // Copyright 2019 The Chromium OS 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 "u2fd/webauthn_handler.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include <base/callback_helpers.h> |
| #include <base/check.h> |
| #include <base/check_op.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 <openssl/rand.h> |
| #include <u2f/proto_bindings/u2f_interface.pb.h> |
| |
| #include "u2fd/u2f_command_processor_gsc.h" |
| #include "u2fd/util.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, |
| }; |
| |
| // Key label in cryptohome. |
| constexpr char kCryptohomePinLabel[] = "pin"; |
| |
| // Relative DBus object path for fingerprint manager in biod. |
| const char kCrosFpBiometricsManagerRelativePath[] = "/CrosFpBiometricsManager"; |
| |
| 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().ToDoubleT()); |
| 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) { |
| if (Initialized()) { |
| LOG(INFO) << "WebAuthn handler already initialized, doing nothing."; |
| return; |
| } |
| |
| metrics_ = metrics; |
| user_state_ = user_state; |
| user_state_->SetSessionStartedCallback( |
| base::Bind(&WebAuthnHandler::OnSessionStarted, base::Unretained(this))); |
| user_state_->SetSessionStoppedCallback( |
| base::Bind(&WebAuthnHandler::OnSessionStopped, base::Unretained(this))); |
| u2f_mode_ = u2f_mode; |
| 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. |
| base::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); |
| base::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) { |
| // In case there's any error, read the backup hash first. |
| auth_time_secret_hash_ = webauthn_storage_->LoadAuthTimeSecretHash(); |
| |
| 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; |
| } |
| |
| // Persist to daemon-store in case we crash during a user session. |
| webauthn_storage_->PersistAuthTimeSecretHash(secret_hash); |
| 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; |
| |
| if (!Initialized()) { |
| response.set_status(MakeCredentialResponse::INTERNAL_ERROR); |
| method_response->Return(response); |
| return; |
| } |
| |
| if (pending_uv_make_credential_session_ || |
| pending_uv_get_assertion_session_) { |
| response.set_status(MakeCredentialResponse::REQUEST_PENDING); |
| method_response->Return(response); |
| return; |
| } |
| |
| if (request.rp_id().empty()) { |
| response.set_status(MakeCredentialResponse::INVALID_REQUEST); |
| method_response->Return(response); |
| return; |
| } |
| |
| if (request.verification_type() == VerificationType::VERIFICATION_UNKNOWN) { |
| response.set_status(MakeCredentialResponse::VERIFICATION_FAILED); |
| 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::kUserAuthenticationServiceShowAuthDialogMethod); |
| dbus::MessageWriter writer(&call); |
| writer.AppendString(session.request.rp_id()); |
| writer.AppendInt32(session.request.verification_type()); |
| writer.AppendUint64(session.request.request_id()); |
| |
| pending_uv_make_credential_session_ = std::move(session); |
| auth_dialog_dbus_proxy_->CallMethod( |
| &call, dbus::ObjectProxy::TIMEOUT_INFINITE, |
| base::Bind(&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_) { |
| LOG(ERROR) << "No pending session to cancel."; |
| response.set_canceled(false); |
| return response; |
| } |
| |
| if (pending_uv_make_credential_session_ && |
| pending_uv_make_credential_session_->request.request_id() != |
| request.request_id()) { |
| LOG(ERROR) |
| << "MakeCredential session has a different request_id, not cancelling."; |
| response.set_canceled(false); |
| return response; |
| } |
| |
| if (pending_uv_get_assertion_session_ && |
| pending_uv_get_assertion_session_->request.request_id() != |
| request.request_id()) { |
| 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); |
| |
| 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) { |
| LOG(INFO) << "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) { |
| LOG(INFO) << "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); |
| 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; |
| std::vector<uint8_t> 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); |
| |
| brillo::Blob credential_secret(kCredentialSecretSize); |
| if (uv_compatible) { |
| if (RAND_bytes(credential_secret.data(), credential_secret.size()) != 1) { |
| LOG(ERROR) << "Failed to generate secret for new credential."; |
| response.set_status(MakeCredentialResponse::INTERNAL_ERROR); |
| session.response->Return(response); |
| return; |
| } |
| } else { |
| // 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. |
| base::Optional<brillo::SecureBlob> legacy_secret = |
| user_state_->GetUserSecret(); |
| if (!legacy_secret) { |
| LOG(ERROR) << "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::vector<uint8_t>(legacy_secret->begin(), legacy_secret->end()); |
| } |
| |
| 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) { |
| response.set_status(generate_status); |
| session.response->Return(response); |
| return; |
| } |
| |
| if (credential_id.empty() || credential_public_key.empty()) { |
| response.set_status(MakeCredentialResponse::INTERNAL_ERROR); |
| session.response->Return(response); |
| return; |
| } |
| |
| auto ret = HasExcludedCredentials(session.request); |
| if (ret == HasCredentialsResponse::INTERNAL_ERROR) { |
| response.set_status(MakeCredentialResponse::INTERNAL_ERROR); |
| session.response->Return(response); |
| return; |
| } else if (ret == HasCredentialsResponse::SUCCESS) { |
| response.set_status(MakeCredentialResponse::EXCLUDED_CREDENTIAL_ID); |
| session.response->Return(response); |
| return; |
| } |
| |
| const base::Optional<std::vector<uint8_t>> authenticator_data = |
| MakeAuthenticatorData( |
| rp_id_hash, credential_id, credential_public_key, |
| /* user_verified = */ session.request.verification_type() == |
| VerificationType::VERIFICATION_USER_VERIFICATION, |
| /* include_attested_credential_data = */ true, |
| /* is_u2f_authenticator_credential = */ !uv_compatible); |
| if (!authenticator_data) { |
| LOG(ERROR) << "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 { |
| const std::vector<uint8_t> data_to_sign = |
| util::BuildU2fRegisterResponseSignedData( |
| rp_id_hash, util::ToVector(session.request.client_data_hash()), |
| credential_public_key, credential_id); |
| base::Optional<std::vector<uint8_t>> attestation_statement = |
| MakeFidoU2fAttestationStatement( |
| data_to_sign, session.request.attestation_conveyance_preference()); |
| if (!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); |
| record.secret = std::move(credential_secret); |
| 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().ToDoubleT(); |
| record.is_resident_key = session.request.resident_key_required(); |
| if (!webauthn_storage_->WriteRecord(std::move(record))) { |
| response.set_status(MakeCredentialResponse::INTERNAL_ERROR); |
| session.response->Return(response); |
| return; |
| } |
| } |
| |
| 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 |
| base::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, |
| bool is_u2f_authenticator_credential) { |
| 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 (is_u2f_authenticator_credential) { |
| base::Optional<std::vector<uint8_t>> counter = user_state_->GetCounter(); |
| if (!counter || !user_state_->IncrementCounter()) { |
| // UserState logs an error in this case. |
| return base::nullopt; |
| } |
| util::AppendToVector(*counter, &authenticator_data); |
| } else { |
| util::AppendToVector(GetTimestampSignatureCounter(), &authenticator_data); |
| } |
| |
| if (include_attested_credential_data) { |
| util::AppendToVector(is_u2f_authenticator_credential |
| ? std::vector<uint8_t>(kAaguid.size(), 0) |
| : kAaguid, |
| &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); |
| } |
| |
| base::Optional<std::vector<uint8_t>> |
| WebAuthnHandler::MakeFidoU2fAttestationStatement( |
| const std::vector<uint8_t>& data_to_sign, |
| 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) { |
| base::Optional<std::vector<uint8_t>> g2f_cert = |
| u2f_command_processor_->GetG2fCert(); |
| if (g2f_cert.has_value()) { |
| attestation_cert = *g2f_cert; |
| } else { |
| LOG(ERROR) << "Failed to get G2f cert for MakeCredential"; |
| return base::nullopt; |
| } |
| |
| base::Optional<brillo::SecureBlob> user_secret = |
| user_state_->GetUserSecret(); |
| if (!user_secret.has_value()) { |
| LOG(ERROR) << "No user secret."; |
| return base::nullopt; |
| } |
| |
| MakeCredentialResponse::MakeCredentialStatus attest_status = |
| u2f_command_processor_->G2fAttest( |
| data_to_sign, *user_secret, U2F_ATTEST_FORMAT_REG_RESP, &signature); |
| |
| if (attest_status != MakeCredentialResponse::SUCCESS) { |
| LOG(ERROR) << "Failed to do G2f attestation for MakeCredential"; |
| return base::nullopt; |
| } |
| |
| if (allowlisting_util_ != nullptr && |
| !allowlisting_util_->AppendDataToCert(&attestation_cert)) { |
| LOG(ERROR) << "Failed to get allowlisting data for G2F Enroll Request"; |
| return base::nullopt; |
| } |
| } else { |
| if (!util::DoSoftwareAttest(data_to_sign, &attestation_cert, &signature)) { |
| LOG(ERROR) << "Failed to do software attestation for MakeCredential"; |
| return base::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) { |
| GetAssertionResponse response; |
| |
| if (!Initialized()) { |
| response.set_status(GetAssertionResponse::INTERNAL_ERROR); |
| method_response->Return(response); |
| return; |
| } |
| |
| if (pending_uv_make_credential_session_ || |
| pending_uv_get_assertion_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) { |
| response.set_status(GetAssertionResponse::INVALID_REQUEST); |
| method_response->Return(response); |
| return; |
| } |
| |
| if (request.verification_type() == VerificationType::VERIFICATION_UNKNOWN) { |
| response.set_status(GetAssertionResponse::VERIFICATION_FAILED); |
| method_response->Return(response); |
| return; |
| } |
| |
| // TODO(louiscollard): Support resident credentials. |
| |
| std::string* credential_to_use; |
| bool is_legacy_credential = false; |
| bool use_app_id = false; |
| |
| MatchedCredentials matched = FindMatchedCredentials( |
| request.allowed_credential_id(), request.rp_id(), request.app_id()); |
| if (matched.has_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 { |
| 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::kUserAuthenticationServiceShowAuthDialogMethod); |
| dbus::MessageWriter writer(&call); |
| writer.AppendString(session.request.rp_id()); |
| writer.AppendInt32(session.request.verification_type()); |
| writer.AppendUint64(session.request.request_id()); |
| |
| pending_uv_get_assertion_session_ = std::move(session); |
| auth_dialog_dbus_proxy_->CallMethod( |
| &call, dbus::ObjectProxy::TIMEOUT_INFINITE, |
| base::Bind(&WebAuthnHandler::HandleUVFlowResultGetAssertion, |
| base::Unretained(this))); |
| return; |
| } |
| |
| DoGetAssertion(std::move(session), PresenceRequirement::kPowerButton); |
| } |
| |
| // 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) { |
| GetAssertionResponse response; |
| |
| bool is_u2f_authenticator_credential = false; |
| std::vector<uint8_t> credential_secret, credential_key_blob; |
| if (!webauthn_storage_->GetSecretAndKeyBlobByCredentialId( |
| session.credential_id, &credential_secret, &credential_key_blob)) { |
| if (!AllowPresenceMode()) { |
| LOG(ERROR) << "No credential secret for credential id " |
| << session.credential_id << ", aborting GetAssertion."; |
| response.set_status(GetAssertionResponse::UNKNOWN_CREDENTIAL_ID); |
| session.response->Return(response); |
| return; |
| } |
| |
| // Maybe signing u2fhid credentials. Use legacy secret instead. |
| base::Optional<brillo::SecureBlob> legacy_secret = |
| user_state_->GetUserSecret(); |
| if (!legacy_secret) { |
| LOG(ERROR) |
| << "Cannot find user secret when trying to sign u2fhid credentials"; |
| response.set_status(GetAssertionResponse::INTERNAL_ERROR); |
| session.response->Return(response); |
| return; |
| } |
| credential_secret = |
| std::vector<uint8_t>(legacy_secret->begin(), legacy_secret->end()); |
| is_u2f_authenticator_credential = true; |
| } |
| |
| const std::vector<uint8_t> rp_id_hash = util::Sha256(session.request.rp_id()); |
| const base::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, |
| is_u2f_authenticator_credential); |
| if (!authenticator_data) { |
| LOG(ERROR) << "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) { |
| 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) { |
| 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) { |
| std::vector<uint8_t> credential_secret, 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) { |
| result.has_internal_error = true; |
| return result; |
| } else if (ret == HasCredentialsResponse::SUCCESS) { |
| result.platform_credentials.emplace_back(credential_id); |
| } |
| } |
| |
| const base::Optional<brillo::SecureBlob> user_secret = |
| user_state_->GetUserSecret(); |
| if (!user_secret) { |
| result.has_internal_error = true; |
| 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), |
| std::vector<uint8_t>(user_secret->begin(), user_secret->end()), |
| 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: |
| 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), |
| std::vector<uint8_t>(user_secret->begin(), user_secret->end()), |
| 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: |
| 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()) { |
| response.set_status(HasCredentialsResponse::INTERNAL_ERROR); |
| return response; |
| } |
| |
| if (request.rp_id().empty() || request.credential_id().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) { |
| 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.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()) { |
| response.set_status(HasCredentialsResponse::INTERNAL_ERROR); |
| return response; |
| } |
| |
| if (request.credential_id().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) { |
| response.set_status(HasCredentialsResponse::INTERNAL_ERROR); |
| return response; |
| } |
| |
| // Do not include platform credentials. |
| 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; |
| } |
| |
| 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(INFO) << "IsUvpaa called but WebAuthnHandler not initialized. Maybe " |
| "U2F is on."; |
| response.set_available(false); |
| method_response->Return(response); |
| return; |
| } |
| |
| if (!auth_time_secret_hash_) { |
| LOG(ERROR) << "No auth-time secret hash. MakeCredential will fail, so " |
| "reporting IsUVPAA=false."; |
| response.set_available(false); |
| method_response->Return(response); |
| return; |
| } |
| |
| base::Optional<std::string> account_id = user_state_->GetUser(); |
| if (!account_id) { |
| LOG(ERROR) << "IsUvpaa called but no user."; |
| response.set_available(false); |
| method_response->Return(response); |
| return; |
| } |
| |
| if (HasPin(*account_id)) { |
| response.set_available(true); |
| method_response->Return(response); |
| return; |
| } |
| |
| base::Optional<std::string> sanitized_user = user_state_->GetSanitizedUser(); |
| DCHECK(sanitized_user); |
| if (HasFingerprint(*sanitized_user)) { |
| response.set_available(true); |
| method_response->Return(response); |
| return; |
| } |
| |
| response.set_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; |
| } |
| |
| bool WebAuthnHandler::HasPin(const std::string& account_id) { |
| user_data_auth::GetKeyDataRequest request; |
| request.mutable_account_id()->set_account_id(account_id); |
| // Touch mutable_authorization_request() so that has_authorization_request() |
| // would return true. |
| request.mutable_authorization_request(); |
| request.mutable_key()->mutable_data()->set_label(kCryptohomePinLabel); |
| |
| user_data_auth::GetKeyDataReply reply; |
| brillo::ErrorPtr error; |
| |
| if (!cryptohome_proxy_->GetKeyData(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) << "GetKeyData response has error " << reply.error(); |
| return false; |
| } |
| |
| return reply.key_data_size() > 0; |
| } |
| |
| 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); |
| 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 |