blob: 36c104677b57cb87dfcc2ca43f065441a18b13b3 [file] [log] [blame]
// 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::TimeDelta::FromMinutes(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,
TpmVendorCommandProxy* tpm_proxy,
UserState* user_state,
U2fMode u2f_mode,
std::function<void()> request_presence,
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::make_unique<U2fCommandProcessorGsc>(tpm_proxy, request_presence);
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;
// 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);
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.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;
base::Optional<std::vector<uint8_t>> credential_secret =
webauthn_storage_->GetSecretByCredentialId(session.credential_id);
if (!credential_secret) {
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, 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) {
base::Optional<std::vector<uint8_t>> credential_secret =
webauthn_storage_->GetSecretByCredentialId(credential_id);
if (!credential_secret)
continue;
auto ret = u2f_command_processor_->U2fSignCheckOnly(
rp_id_hash, util::ToVector(credential_id), *credential_secret);
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()));
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()));
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);
}
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);
}
void WebAuthnHandler::SetU2fCommandProcessorForTesting(
std::unique_ptr<U2fCommandProcessor> helper) {
u2f_command_processor_ = std::move(helper);
}
} // namespace u2f