| // 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/bind_helpers.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/util.h" |
| |
| namespace u2f { |
| |
| namespace { |
| |
| // User a big timeout for cryptohome. See b/172945202. |
| constexpr base::TimeDelta kCryptohomeTimeout = base::TimeDelta::FromMinutes(2); |
| constexpr int kVerificationTimeoutMs = 10000; |
| constexpr int kVerificationRetryDelayUs = 500 * 1000; |
| constexpr int kCancelUVFlowTimeoutMs = 5000; |
| |
| // Cr50 Response codes. |
| // TODO(louiscollard): Don't duplicate these. |
| constexpr uint32_t kCr50StatusNotAllowed = 0x507; |
| |
| constexpr char kAttestationFormatNone[] = "none"; |
| // \xa0 is empty map in CBOR |
| constexpr char kAttestationStatementNone = '\xa0'; |
| |
| // AAGUID should be empty for none-attestation. |
| const std::vector<uint8_t> kAaguid(16); |
| |
| // 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, |
| }; |
| |
| // COSE key parameters. |
| // https://tools.ietf.org/html/rfc8152#section-7.1 |
| const int kCoseKeyKtyLabel = 1; |
| const int kCoseKeyKtyEC2 = 2; |
| const int kCoseKeyAlgLabel = 3; |
| const int kCoseKeyAlgES256 = -7; |
| |
| // Double coordinate curve parameters. |
| // https://tools.ietf.org/html/rfc8152#section-13.1.1 |
| const int kCoseECKeyCrvLabel = -1; |
| const int kCoseECKeyXLabel = -2; |
| const int kCoseECKeyYLabel = -3; |
| |
| // 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> EncodeCredentialPublicKeyInCBOR( |
| const std::vector<uint8_t>& credential_public_key) { |
| DCHECK_EQ(credential_public_key.size(), sizeof(struct u2f_ec_point)); |
| cbor::Value::MapValue cbor_map; |
| cbor_map[cbor::Value(kCoseKeyKtyLabel)] = cbor::Value(kCoseKeyKtyEC2); |
| cbor_map[cbor::Value(kCoseKeyAlgLabel)] = cbor::Value(kCoseKeyAlgES256); |
| cbor_map[cbor::Value(kCoseECKeyCrvLabel)] = cbor::Value(1); |
| cbor_map[cbor::Value(kCoseECKeyXLabel)] = cbor::Value(base::make_span( |
| credential_public_key.data() + offsetof(struct u2f_ec_point, x), |
| U2F_EC_KEY_SIZE)); |
| cbor_map[cbor::Value(kCoseECKeyYLabel)] = cbor::Value(base::make_span( |
| credential_public_key.data() + offsetof(struct u2f_ec_point, y), |
| U2F_EC_KEY_SIZE)); |
| return *cbor::Writer::Write(cbor::Value(std::move(cbor_map))); |
| } |
| |
| } // namespace |
| |
| WebAuthnHandler::WebAuthnHandler() |
| : tpm_proxy_(nullptr), |
| user_state_(nullptr), |
| webauthn_storage_(std::make_unique<WebAuthnStorage>()) {} |
| |
| void WebAuthnHandler::Initialize(dbus::Bus* bus, |
| TpmVendorCommandProxy* tpm_proxy, |
| UserState* user_state, |
| std::function<void()> request_presence) { |
| if (Initialized()) { |
| LOG(INFO) << "WebAuthn handler already initialized, doing nothing."; |
| return; |
| } |
| |
| tpm_proxy_ = tpm_proxy; |
| user_state_ = user_state; |
| user_state_->SetSessionStartedCallback( |
| base::Bind(&WebAuthnHandler::OnSessionStarted, base::Unretained(this))); |
| user_state_->SetSessionStoppedCallback( |
| base::Bind(&WebAuthnHandler::OnSessionStopped, base::Unretained(this))); |
| request_presence_ = request_presence; |
| 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::CryptohomeInterfaceProxy>(bus_); |
| DCHECK(auth_dialog_dbus_proxy_); |
| |
| 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 tpm_proxy_ != nullptr && user_state_ != nullptr; |
| } |
| |
| void WebAuthnHandler::OnSessionStarted(const std::string& account_id) { |
| // Do this first because there's a timeout for reading the secret. |
| const bool received_secret_from_cryptohome = GetWebAuthnSecret(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; |
| } |
| |
| if (received_secret_from_cryptohome) { |
| // Persist to daemon-store in case we crash during a user session. |
| webauthn_storage_->PersistAuthTimeSecretHash(*auth_time_secret_hash_); |
| } else { |
| // If this is login, we should have received the secret from cryptohomed. |
| // Since we did not, either we crashed during a user session (in which case |
| // cryptohomed would not have the secret when we restarted), or there's an |
| // internal error in cryptohome. Either way, read the backup secret hash. |
| auth_time_secret_hash_ = webauthn_storage_->LoadAuthTimeSecretHash(); |
| } |
| } |
| |
| void WebAuthnHandler::OnSessionStopped() { |
| auth_time_secret_hash_.reset(); |
| webauthn_storage_->Reset(); |
| } |
| |
| bool WebAuthnHandler::GetWebAuthnSecret(const std::string& account_id) { |
| cryptohome::AccountIdentifier id; |
| id.set_account_id(account_id); |
| cryptohome::GetWebAuthnSecretRequest req; |
| cryptohome::BaseReply reply; |
| brillo::ErrorPtr error; |
| |
| if (!cryptohome_proxy_->GetWebAuthnSecret( |
| id, req, &reply, &error, kCryptohomeTimeout.InMilliseconds())) { |
| LOG(ERROR) << "Failed to call GetWebAuthnSecret on cryptohome, error: " |
| << error->GetMessage(); |
| return false; |
| } |
| |
| if (reply.has_error()) { |
| LOG(ERROR) << "GetWebAuthnSecret reply has error " << reply.error(); |
| return false; |
| } |
| |
| if (!reply.HasExtension(cryptohome::GetWebAuthnSecretReply::reply)) { |
| LOG(ERROR) << "GetWebAuthnSecret reply doesn't have the correct extension."; |
| return false; |
| } |
| |
| brillo::SecureBlob secret( |
| reply.GetExtension(cryptohome::GetWebAuthnSecretReply::reply) |
| .webauthn_secret()); |
| if (secret.size() != SHA256_DIGEST_LENGTH) { |
| LOG(ERROR) << "WebAuthn auth time secret size is wrong."; |
| return false; |
| } |
| |
| auth_time_secret_hash_ = std::make_unique<brillo::Blob>(util::Sha256(secret)); |
| return true; |
| } |
| |
| 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)}; |
| |
| // Upgrade UP requests to UV. |
| session.request.set_verification_type( |
| VerificationType::VERIFICATION_USER_VERIFICATION); |
| |
| dbus::MethodCall call( |
| chromeos::kUserAuthenticationServiceInterface, |
| chromeos::kUserAuthenticationServiceShowAuthDialogMethod); |
| dbus::MessageWriter writer(&call); |
| writer.AppendString(request.rp_id()); |
| writer.AppendInt32(request.verification_type()); |
| writer.AppendUint64(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))); |
| } |
| |
| 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; |
| |
| // TODO(yichengli): Make this a parameter of MakeCredential once we support |
| // UP-only (non-consumer) credentials in WebAuthnHandler. |
| // UV-compatible means the credential works with power button, fingerprint or |
| // PIN. |
| bool uv_compatible = true; |
| |
| brillo::Blob credential_secret(kCredentialSecretSize); |
| if (RAND_bytes(&credential_secret.front(), credential_secret.size()) != 1) { |
| LOG(ERROR) << "Failed to generate secret for new credential."; |
| response.set_status(MakeCredentialResponse::INTERNAL_ERROR); |
| session.response->Return(response); |
| return; |
| } |
| |
| MakeCredentialResponse::MakeCredentialStatus generate_status = |
| DoU2fGenerate(rp_id_hash, credential_secret, presence_requirement, |
| uv_compatible, &credential_id, &credential_public_key); |
| |
| 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; |
| } |
| |
| if (uv_compatible) |
| InsertAuthTimeSecretHashToCredentialId(&credential_id); |
| |
| 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; |
| } |
| |
| WebAuthnRecord record; |
| AppendToString(credential_id, &record.credential_id); |
| record.secret = std::move(credential_secret); |
| record.rp_id = session.request.rp_id(); |
| record.user_id = session.request.user_id(); |
| record.user_display_name = session.request.user_display_name(); |
| record.timestamp = base::Time::Now().ToDoubleT(); |
| if (!webauthn_storage_->WriteRecord(std::move(record))) { |
| response.set_status(MakeCredentialResponse::INTERNAL_ERROR); |
| session.response->Return(response); |
| return; |
| } |
| |
| AppendToString(MakeAuthenticatorData( |
| rp_id_hash, credential_id, |
| EncodeCredentialPublicKeyInCBOR(credential_public_key), |
| session.request.verification_type() == |
| VerificationType::VERIFICATION_USER_VERIFICATION, |
| true), |
| response.mutable_authenticator_data()); |
| AppendNoneAttestation(&response); |
| |
| 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::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) { |
| 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); |
| util::AppendToVector(GetTimestampSignatureCounter(), &authenticator_data); |
| |
| if (include_attested_credential_data) { |
| util::AppendToVector(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); |
| } |
| |
| void WebAuthnHandler::CallAndWaitForPresence(std::function<uint32_t()> fn, |
| uint32_t* status) { |
| *status = fn(); |
| base::TimeTicks verification_start = base::TimeTicks::Now(); |
| while (*status == kCr50StatusNotAllowed && |
| base::TimeTicks::Now() - verification_start < |
| base::TimeDelta::FromMilliseconds(kVerificationTimeoutMs)) { |
| // We need user presence. Show a notification requesting it, and try again. |
| request_presence_(); |
| usleep(kVerificationRetryDelayUs); |
| *status = fn(); |
| } |
| } |
| |
| MakeCredentialResponse::MakeCredentialStatus WebAuthnHandler::DoU2fGenerate( |
| const std::vector<uint8_t>& rp_id_hash, |
| const std::vector<uint8_t>& credential_secret, |
| PresenceRequirement presence_requirement, |
| bool uv_compatible, |
| std::vector<uint8_t>* credential_id, |
| std::vector<uint8_t>* credential_public_key) { |
| DCHECK(rp_id_hash.size() == SHA256_DIGEST_LENGTH); |
| |
| struct u2f_generate_req generate_req = {}; |
| if (!util::VectorToObject(rp_id_hash, generate_req.appId, |
| sizeof(generate_req.appId))) { |
| return MakeCredentialResponse::INVALID_REQUEST; |
| } |
| if (!util::VectorToObject(credential_secret, generate_req.userSecret, |
| sizeof(generate_req.userSecret))) { |
| return MakeCredentialResponse::INVALID_REQUEST; |
| } |
| |
| if (uv_compatible) { |
| if (!auth_time_secret_hash_) { |
| LOG(ERROR) << "No auth-time secret hash to use for u2f_generate."; |
| return MakeCredentialResponse::INTERNAL_ERROR; |
| } |
| generate_req.flags |= U2F_UV_ENABLED_KH; |
| memcpy(generate_req.authTimeSecretHash, auth_time_secret_hash_->data(), |
| auth_time_secret_hash_->size()); |
| struct u2f_generate_versioned_resp generate_resp = {}; |
| |
| if (presence_requirement != PresenceRequirement::kPowerButton) { |
| uint32_t generate_status = |
| tpm_proxy_->SendU2fGenerate(generate_req, &generate_resp); |
| if (generate_status != 0) |
| return MakeCredentialResponse::INTERNAL_ERROR; |
| |
| util::AppendToVector(generate_resp.pubKey, credential_public_key); |
| util::AppendToVector(generate_resp.keyHandle, credential_id); |
| return MakeCredentialResponse::SUCCESS; |
| } |
| |
| // Require user presence, consume. |
| generate_req.flags |= U2F_AUTH_ENFORCE; |
| return SendU2fGenerateWaitForPresence(&generate_req, &generate_resp, |
| credential_id, credential_public_key); |
| } else { |
| // Non-versioned KH must be signed with power button press. |
| if (presence_requirement != PresenceRequirement::kPowerButton) |
| return MakeCredentialResponse::INTERNAL_ERROR; |
| // Require user presence, consume. |
| generate_req.flags |= U2F_AUTH_ENFORCE; |
| struct u2f_generate_resp generate_resp = {}; |
| return SendU2fGenerateWaitForPresence(&generate_req, &generate_resp, |
| credential_id, credential_public_key); |
| } |
| } |
| |
| template <typename Response> |
| MakeCredentialResponse::MakeCredentialStatus |
| WebAuthnHandler::SendU2fGenerateWaitForPresence( |
| struct u2f_generate_req* generate_req, |
| Response* generate_resp, |
| std::vector<uint8_t>* credential_id, |
| std::vector<uint8_t>* credential_public_key) { |
| uint32_t generate_status = -1; |
| base::AutoLock(tpm_proxy_->GetLock()); |
| CallAndWaitForPresence( |
| [this, generate_req, generate_resp]() { |
| return tpm_proxy_->SendU2fGenerate(*generate_req, generate_resp); |
| }, |
| &generate_status); |
| brillo::SecureClear(&generate_req->userSecret, |
| sizeof(generate_req->userSecret)); |
| |
| if (generate_status == 0) { |
| util::AppendToVector(generate_resp->pubKey, credential_public_key); |
| util::AppendToVector(generate_resp->keyHandle, credential_id); |
| return MakeCredentialResponse::SUCCESS; |
| } |
| |
| return MakeCredentialResponse::VERIFICATION_FAILED; |
| } |
| |
| // TODO(b/172971998): Remove this workaround once cr50 handles this. |
| void WebAuthnHandler::InsertAuthTimeSecretHashToCredentialId( |
| std::vector<uint8_t>* input) { |
| CHECK(input->size() == sizeof(u2f_versioned_key_handle)); |
| // The auth time secret hash should be inserted right after the header and |
| // the authorization salt, before the authorization hmac. |
| input->insert( |
| input->cbegin() + offsetof(u2f_versioned_key_handle, authorization_hmac), |
| auth_time_secret_hash_->cbegin(), auth_time_secret_hash_->cend()); |
| } |
| |
| // TODO(b/172971998): Remove this workaround once cr50 handles this. |
| void WebAuthnHandler::RemoveAuthTimeSecretHashFromCredentialId( |
| std::vector<uint8_t>* input) { |
| CHECK_EQ(input->size(), |
| sizeof(u2f_versioned_key_handle) + SHA256_DIGEST_LENGTH); |
| // The auth time secret hash is after the header and the authorization salt, |
| // before the authorization hmac. Remove it so that cr50 recognizes the KH. |
| const std::vector<uint8_t>::const_iterator remove_begin = |
| input->cbegin() + offsetof(u2f_versioned_key_handle, authorization_hmac); |
| input->erase(remove_begin, remove_begin + SHA256_DIGEST_LENGTH); |
| } |
| |
| HasCredentialsResponse::HasCredentialsStatus |
| WebAuthnHandler::HasExcludedCredentials(const MakeCredentialRequest& request) { |
| std::vector<uint8_t> rp_id_hash = util::Sha256(request.rp_id()); |
| for (auto credential : request.excluded_credential_id()) { |
| base::Optional<std::vector<uint8_t>> credential_secret = |
| webauthn_storage_->GetSecretByCredentialId(credential); |
| if (!credential_secret) |
| continue; |
| |
| auto ret = DoU2fSignCheckOnly(rp_id_hash, util::ToVector(credential), |
| *credential_secret); |
| if (ret == HasCredentialsResponse::SUCCESS) |
| return ret; |
| if (ret == HasCredentialsResponse::INTERNAL_ERROR) |
| return ret; |
| } |
| return HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID; |
| } |
| |
| 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. |
| |
| const std::vector<uint8_t> rp_id_hash = util::Sha256(request.rp_id()); |
| int matched_index = -1; |
| |
| for (int index = 0; index < request.allowed_credential_id_size(); index++) { |
| base::Optional<std::vector<uint8_t>> credential_secret = |
| webauthn_storage_->GetSecretByCredentialId( |
| request.allowed_credential_id(index)); |
| if (!credential_secret) |
| continue; |
| |
| const HasCredentialsResponse::HasCredentialsStatus ret = DoU2fSignCheckOnly( |
| rp_id_hash, util::ToVector(request.allowed_credential_id(index)), |
| *credential_secret); |
| |
| if (ret == HasCredentialsResponse::INTERNAL_ERROR) { |
| // If there's an internal error then the remaining credentials won't |
| // succeed. |
| response.set_status(GetAssertionResponse::INTERNAL_ERROR); |
| method_response->Return(response); |
| return; |
| } |
| if (ret != HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID) { |
| matched_index = index; |
| break; |
| } |
| } |
| |
| if (matched_index == -1) { |
| // No credential_id matched. |
| response.set_status(GetAssertionResponse::UNKNOWN_CREDENTIAL_ID); |
| method_response->Return(response); |
| return; |
| } |
| |
| struct GetAssertionSession session = { |
| static_cast<uint64_t>(base::Time::Now().ToTimeT()), request, |
| request.allowed_credential_id(matched_index), std::move(method_response)}; |
| |
| // Upgrade UP requests to UV. |
| session.request.set_verification_type( |
| VerificationType::VERIFICATION_USER_VERIFICATION); |
| |
| dbus::MethodCall call( |
| chromeos::kUserAuthenticationServiceInterface, |
| chromeos::kUserAuthenticationServiceShowAuthDialogMethod); |
| dbus::MessageWriter writer(&call); |
| writer.AppendString(request.rp_id()); |
| writer.AppendInt32(request.verification_type()); |
| writer.AppendUint64(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))); |
| } |
| |
| // 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; |
| const std::vector<uint8_t> rp_id_hash = util::Sha256(session.request.rp_id()); |
| std::vector<uint8_t> authenticator_data = MakeAuthenticatorData( |
| rp_id_hash, std::vector<uint8_t>(), std::vector<uint8_t>(), |
| session.request.verification_type() == |
| VerificationType::VERIFICATION_USER_VERIFICATION, |
| false); |
| 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); |
| |
| base::Optional<std::vector<uint8_t>> credential_secret = |
| webauthn_storage_->GetSecretByCredentialId(session.credential_id); |
| if (!credential_secret) { |
| LOG(ERROR) << "No credential secret for credential id " |
| << session.credential_id << ", aborting GetAssertion."; |
| response.set_status(GetAssertionResponse::UNKNOWN_CREDENTIAL_ID); |
| session.response->Return(response); |
| } |
| std::vector<uint8_t> signature; |
| GetAssertionResponse::GetAssertionStatus sign_status = |
| DoU2fSign(rp_id_hash, hash_to_sign, util::ToVector(session.credential_id), |
| *credential_secret, presence_requirement, &signature); |
| response.set_status(sign_status); |
| if (sign_status == GetAssertionResponse::SUCCESS) { |
| auto* assertion = response.add_assertion(); |
| assertion->set_credential_id( |
| session.request.allowed_credential_id().Get(0)); |
| AppendToString(authenticator_data, assertion->mutable_authenticator_data()); |
| AppendToString(signature, assertion->mutable_signature()); |
| } |
| |
| session.response->Return(response); |
| } |
| |
| GetAssertionResponse::GetAssertionStatus WebAuthnHandler::DoU2fSign( |
| const std::vector<uint8_t>& rp_id_hash, |
| const std::vector<uint8_t>& hash_to_sign, |
| const std::vector<uint8_t>& credential_id, |
| const std::vector<uint8_t>& credential_secret, |
| PresenceRequirement presence_requirement, |
| std::vector<uint8_t>* signature) { |
| DCHECK(rp_id_hash.size() == SHA256_DIGEST_LENGTH); |
| |
| if (credential_id.size() == |
| sizeof(u2f_versioned_key_handle) + SHA256_DIGEST_SIZE) { |
| // Allow waiving presence if sign_req.authTimeSecret is correct. |
| struct u2f_sign_versioned_req sign_req = {}; |
| if (!util::VectorToObject(rp_id_hash, sign_req.appId, |
| sizeof(sign_req.appId))) { |
| return GetAssertionResponse::INVALID_REQUEST; |
| } |
| if (!util::VectorToObject(credential_secret, sign_req.userSecret, |
| sizeof(sign_req.userSecret))) { |
| return GetAssertionResponse::INVALID_REQUEST; |
| } |
| std::vector<uint8_t> key_handle(credential_id); |
| RemoveAuthTimeSecretHashFromCredentialId(&key_handle); |
| if (!util::VectorToObject(key_handle, &sign_req.keyHandle, |
| sizeof(sign_req.keyHandle))) { |
| return GetAssertionResponse::INVALID_REQUEST; |
| } |
| if (!util::VectorToObject(hash_to_sign, sign_req.hash, |
| sizeof(sign_req.hash))) { |
| return GetAssertionResponse::INVALID_REQUEST; |
| } |
| struct u2f_sign_resp sign_resp = {}; |
| |
| if (presence_requirement != PresenceRequirement::kPowerButton) { |
| uint32_t sign_status = tpm_proxy_->SendU2fSign(sign_req, &sign_resp); |
| if (sign_status != 0) |
| return GetAssertionResponse::INTERNAL_ERROR; |
| |
| base::Optional<std::vector<uint8_t>> opt_signature = |
| util::SignatureToDerBytes(sign_resp.sig_r, sign_resp.sig_s); |
| if (!opt_signature.has_value()) { |
| return GetAssertionResponse::INTERNAL_ERROR; |
| } |
| *signature = *opt_signature; |
| return GetAssertionResponse::SUCCESS; |
| } |
| |
| // Require user presence, consume. |
| sign_req.flags |= U2F_AUTH_ENFORCE; |
| return SendU2fSignWaitForPresence(&sign_req, &sign_resp, signature); |
| } else if (credential_id.size() == sizeof(u2f_key_handle)) { |
| // Non-versioned KH must be signed with power button press. |
| if (presence_requirement != PresenceRequirement::kPowerButton) |
| return GetAssertionResponse::INTERNAL_ERROR; |
| |
| struct u2f_sign_req sign_req = { |
| .flags = U2F_AUTH_ENFORCE // Require user presence, consume. |
| }; |
| if (!util::VectorToObject(rp_id_hash, sign_req.appId, |
| sizeof(sign_req.appId))) { |
| return GetAssertionResponse::INVALID_REQUEST; |
| } |
| if (!util::VectorToObject(credential_secret, sign_req.userSecret, |
| sizeof(sign_req.userSecret))) { |
| return GetAssertionResponse::INVALID_REQUEST; |
| } |
| if (!util::VectorToObject(credential_id, &sign_req.keyHandle, |
| sizeof(sign_req.keyHandle))) { |
| return GetAssertionResponse::INVALID_REQUEST; |
| } |
| if (!util::VectorToObject(hash_to_sign, sign_req.hash, |
| sizeof(sign_req.hash))) { |
| return GetAssertionResponse::INVALID_REQUEST; |
| } |
| |
| struct u2f_sign_resp sign_resp = {}; |
| return SendU2fSignWaitForPresence(&sign_req, &sign_resp, signature); |
| } else { |
| return GetAssertionResponse::INVALID_REQUEST; |
| } |
| } |
| |
| template <typename Request> |
| GetAssertionResponse::GetAssertionStatus |
| WebAuthnHandler::SendU2fSignWaitForPresence(Request* sign_req, |
| struct u2f_sign_resp* sign_resp, |
| std::vector<uint8_t>* signature) { |
| uint32_t sign_status = -1; |
| base::AutoLock(tpm_proxy_->GetLock()); |
| CallAndWaitForPresence( |
| [this, sign_req, sign_resp]() { |
| return tpm_proxy_->SendU2fSign(*sign_req, sign_resp); |
| }, |
| &sign_status); |
| brillo::SecureClear(&sign_req->userSecret, sizeof(sign_req->userSecret)); |
| |
| if (sign_status == 0) { |
| base::Optional<std::vector<uint8_t>> opt_signature = |
| util::SignatureToDerBytes(sign_resp->sig_r, sign_resp->sig_s); |
| if (!opt_signature.has_value()) { |
| return GetAssertionResponse::INTERNAL_ERROR; |
| } |
| *signature = *opt_signature; |
| return GetAssertionResponse::SUCCESS; |
| } |
| |
| return GetAssertionResponse::VERIFICATION_FAILED; |
| } |
| |
| 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; |
| } |
| |
| std::vector<uint8_t> rp_id_hash = util::Sha256(request.rp_id()); |
| for (const auto& credential_id : request.credential_id()) { |
| base::Optional<std::vector<uint8_t>> credential_secret = |
| webauthn_storage_->GetSecretByCredentialId(credential_id); |
| if (!credential_secret) |
| continue; |
| |
| auto ret = DoU2fSignCheckOnly(rp_id_hash, util::ToVector(credential_id), |
| *credential_secret); |
| if (ret == HasCredentialsResponse::INTERNAL_ERROR) { |
| response.set_status(ret); |
| return response; |
| } else if (ret == HasCredentialsResponse::SUCCESS) { |
| *response.add_credential_id() = credential_id; |
| } |
| } |
| |
| response.set_status((response.credential_id_size() > 0) |
| ? HasCredentialsResponse::SUCCESS |
| : HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID); |
| return response; |
| } |
| |
| HasCredentialsResponse::HasCredentialsStatus |
| WebAuthnHandler::DoU2fSignCheckOnly( |
| const std::vector<uint8_t>& rp_id_hash, |
| const std::vector<uint8_t>& credential_id, |
| const std::vector<uint8_t>& credential_secret) { |
| uint32_t sign_status; |
| |
| if (credential_id.size() == |
| sizeof(u2f_versioned_key_handle) + SHA256_DIGEST_SIZE) { |
| struct u2f_sign_versioned_req sign_req = {.flags = U2F_AUTH_CHECK_ONLY}; |
| if (!util::VectorToObject(rp_id_hash, sign_req.appId, |
| sizeof(sign_req.appId))) { |
| return HasCredentialsResponse::INVALID_REQUEST; |
| } |
| if (!util::VectorToObject(credential_secret, sign_req.userSecret, |
| sizeof(sign_req.userSecret))) { |
| return HasCredentialsResponse::INVALID_REQUEST; |
| } |
| std::vector<uint8_t> key_handle(credential_id); |
| RemoveAuthTimeSecretHashFromCredentialId(&key_handle); |
| if (!util::VectorToObject(key_handle, &sign_req.keyHandle, |
| sizeof(sign_req.keyHandle))) { |
| return HasCredentialsResponse::INVALID_REQUEST; |
| } |
| |
| struct u2f_sign_resp sign_resp; |
| base::AutoLock(tpm_proxy_->GetLock()); |
| sign_status = tpm_proxy_->SendU2fSign(sign_req, &sign_resp); |
| brillo::SecureClear(&sign_req.userSecret, sizeof(sign_req.userSecret)); |
| } else if (credential_id.size() == sizeof(u2f_key_handle)) { |
| struct u2f_sign_req sign_req = {.flags = U2F_AUTH_CHECK_ONLY}; |
| if (!util::VectorToObject(rp_id_hash, sign_req.appId, |
| sizeof(sign_req.appId))) { |
| return HasCredentialsResponse::INVALID_REQUEST; |
| } |
| if (!util::VectorToObject(credential_secret, sign_req.userSecret, |
| sizeof(sign_req.userSecret))) { |
| return HasCredentialsResponse::INVALID_REQUEST; |
| } |
| if (!util::VectorToObject(credential_id, &sign_req.keyHandle, |
| sizeof(sign_req.keyHandle))) { |
| return HasCredentialsResponse::INVALID_REQUEST; |
| } |
| |
| struct u2f_sign_resp sign_resp; |
| base::AutoLock(tpm_proxy_->GetLock()); |
| sign_status = tpm_proxy_->SendU2fSign(sign_req, &sign_resp); |
| brillo::SecureClear(&sign_req.userSecret, sizeof(sign_req.userSecret)); |
| } else { |
| return HasCredentialsResponse::INVALID_REQUEST; |
| } |
| |
| // Return status of 0 indicates the credential is valid. |
| return (sign_status == 0) ? HasCredentialsResponse::SUCCESS |
| : HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID; |
| } |
| |
| 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; |
| } |
| |
| 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); |
| } |
| |
| bool WebAuthnHandler::HasPin(const std::string& account_id) { |
| cryptohome::AccountIdentifier id; |
| id.set_account_id(account_id); |
| cryptohome::AuthorizationRequest auth; |
| cryptohome::GetKeyDataRequest req; |
| req.mutable_key()->mutable_data()->set_label(kCryptohomePinLabel); |
| cryptohome::BaseReply reply; |
| brillo::ErrorPtr error; |
| |
| if (!cryptohome_proxy_->GetKeyDataEx(id, auth, req, &reply, &error, |
| kCryptohomeTimeout.InMilliseconds())) { |
| LOG(ERROR) << "Cannot query PIN availability from cryptohome, error: " |
| << error->GetMessage(); |
| return false; |
| } |
| |
| if (reply.has_error()) { |
| LOG(ERROR) << "GetKeyData response has error " << reply.error(); |
| return false; |
| } |
| |
| if (!reply.HasExtension(cryptohome::GetKeyDataReply::reply)) { |
| LOG(ERROR) << "GetKeyData response doesn't have the correct extension."; |
| return false; |
| } |
| |
| return reply.GetExtension(cryptohome::GetKeyDataReply::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::CryptohomeInterfaceProxyInterface> |
| cryptohome_proxy) { |
| cryptohome_proxy_ = std::move(cryptohome_proxy); |
| } |
| |
| } // namespace u2f |