blob: 05936a30aa3e9dd526be837082d56632cb3e12a0 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "device/fido/win/webauthn_api.h"
#include <string>
#include <vector>
#include "base/bind.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/optional.h"
#include "base/strings/string16.h"
#include "base/strings/string_piece_forward.h"
#include "base/strings/utf_string_conversions.h"
#include "components/device_event_log/device_event_log.h"
#include "device/fido/win/logging.h"
#include "device/fido/win/type_conversions.h"
namespace device {
namespace {
base::string16 OptionalGURLToUTF16(const base::Optional<GURL>& in) {
return in ? base::UTF8ToUTF16(in->spec()) : base::string16();
}
} // namespace
// Time out all Windows API requests after 5 minutes. We maintain our own
// timeout and cancel the operation when it expires, so this value simply needs
// to be larger than the largest internal request timeout.
constexpr uint32_t kWinWebAuthnTimeoutMilliseconds = 1000 * 60 * 5;
class WinWebAuthnApiImpl : public WinWebAuthnApi {
public:
WinWebAuthnApiImpl() : WinWebAuthnApi(), is_bound_(false) {
webauthn_dll_ =
LoadLibraryExA("webauthn.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
if (!webauthn_dll_) {
return;
}
#define BIND_FN(fn_pointer, lib_handle, fn_name) \
DCHECK(!fn_pointer); \
fn_pointer = reinterpret_cast<decltype(fn_pointer)>( \
GetProcAddress(lib_handle, fn_name));
#define BIND_FN_OR_RETURN(fn_pointer, lib_handle, fn_name) \
BIND_FN(fn_pointer, lib_handle, fn_name); \
if (!fn_pointer) { \
DLOG(ERROR) << "failed to bind " << fn_name; \
return; \
}
BIND_FN_OR_RETURN(is_user_verifying_platform_authenticator_available_,
webauthn_dll_,
"WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable");
BIND_FN_OR_RETURN(authenticator_make_credential_, webauthn_dll_,
"WebAuthNAuthenticatorMakeCredential");
BIND_FN_OR_RETURN(authenticator_get_assertion_, webauthn_dll_,
"WebAuthNAuthenticatorGetAssertion");
BIND_FN_OR_RETURN(cancel_current_operation_, webauthn_dll_,
"WebAuthNCancelCurrentOperation");
BIND_FN_OR_RETURN(get_error_name_, webauthn_dll_, "WebAuthNGetErrorName");
BIND_FN_OR_RETURN(free_credential_attestation_, webauthn_dll_,
"WebAuthNFreeCredentialAttestation");
BIND_FN_OR_RETURN(free_assertion_, webauthn_dll_, "WebAuthNFreeAssertion");
is_bound_ = true;
// Determine the API version of webauthn.dll. There is a version currently
// shipping with Windows RS5 from before WebAuthNGetApiVersionNumber was
// added (i.e., before WEBAUTHN_API_VERSION_1). Therefore we allow this
// function to be missing.
BIND_FN(get_api_version_number_, webauthn_dll_,
"WebAuthNGetApiVersionNumber");
api_version_ = get_api_version_number_ ? get_api_version_number_() : 0;
FIDO_LOG(DEBUG) << "webauthn.dll version " << api_version_;
}
~WinWebAuthnApiImpl() override {}
// WinWebAuthnApi:
bool IsAvailable() const override {
return is_bound_ && (api_version_ >= WEBAUTHN_API_VERSION_1);
}
HRESULT IsUserVerifyingPlatformAuthenticatorAvailable(
BOOL* available) override {
DCHECK(is_bound_);
return is_user_verifying_platform_authenticator_available_(available);
}
HRESULT AuthenticatorMakeCredential(
HWND h_wnd,
PCWEBAUTHN_RP_ENTITY_INFORMATION rp,
PCWEBAUTHN_USER_ENTITY_INFORMATION user,
PCWEBAUTHN_COSE_CREDENTIAL_PARAMETERS cose_credential_parameters,
PCWEBAUTHN_CLIENT_DATA client_data,
PCWEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS options,
PWEBAUTHN_CREDENTIAL_ATTESTATION* credential_attestation_ptr) override {
DCHECK(is_bound_);
return authenticator_make_credential_(
h_wnd, rp, user, cose_credential_parameters, client_data, options,
credential_attestation_ptr);
}
HRESULT AuthenticatorGetAssertion(
HWND h_wnd,
LPCWSTR rp_id,
PCWEBAUTHN_CLIENT_DATA client_data,
PCWEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS options,
PWEBAUTHN_ASSERTION* assertion_ptr) override {
DCHECK(is_bound_);
return authenticator_get_assertion_(h_wnd, rp_id, client_data, options,
assertion_ptr);
}
HRESULT CancelCurrentOperation(GUID* cancellation_id) override {
DCHECK(is_bound_);
return cancel_current_operation_(cancellation_id);
}
PCWSTR GetErrorName(HRESULT hr) override {
DCHECK(is_bound_);
return get_error_name_(hr);
}
void FreeCredentialAttestation(
PWEBAUTHN_CREDENTIAL_ATTESTATION attestation_ptr) override {
DCHECK(is_bound_);
return free_credential_attestation_(attestation_ptr);
}
void FreeAssertion(PWEBAUTHN_ASSERTION assertion_ptr) override {
DCHECK(is_bound_);
return free_assertion_(assertion_ptr);
}
int Version() override { return api_version_; }
private:
bool is_bound_ = false;
uint32_t api_version_ = 0;
HMODULE webauthn_dll_;
decltype(&WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable)
is_user_verifying_platform_authenticator_available_ = nullptr;
decltype(
&WebAuthNAuthenticatorMakeCredential) authenticator_make_credential_ =
nullptr;
decltype(&WebAuthNAuthenticatorGetAssertion) authenticator_get_assertion_ =
nullptr;
decltype(&WebAuthNCancelCurrentOperation) cancel_current_operation_ = nullptr;
decltype(&WebAuthNGetErrorName) get_error_name_ = nullptr;
decltype(&WebAuthNFreeCredentialAttestation) free_credential_attestation_ =
nullptr;
decltype(&WebAuthNFreeAssertion) free_assertion_ = nullptr;
// This method is not available in all versions of webauthn.dll.
decltype(&WebAuthNGetApiVersionNumber) get_api_version_number_ = nullptr;
};
static WinWebAuthnApi* kDefaultForTesting = nullptr;
// static
WinWebAuthnApi* WinWebAuthnApi::GetDefault() {
if (kDefaultForTesting) {
return kDefaultForTesting;
}
static base::NoDestructor<WinWebAuthnApiImpl> api;
return api.get();
}
// static
void WinWebAuthnApi::SetDefaultForTesting(WinWebAuthnApi* api) {
DCHECK(!kDefaultForTesting);
kDefaultForTesting = api;
}
// static
void WinWebAuthnApi::ClearDefaultForTesting() {
DCHECK(kDefaultForTesting);
kDefaultForTesting = nullptr;
}
WinWebAuthnApi::WinWebAuthnApi() = default;
WinWebAuthnApi::~WinWebAuthnApi() = default;
std::pair<CtapDeviceResponseCode,
base::Optional<AuthenticatorMakeCredentialResponse>>
AuthenticatorMakeCredentialBlocking(WinWebAuthnApi* webauthn_api,
HWND h_wnd,
GUID cancellation_id,
CtapMakeCredentialRequest request) {
DCHECK(webauthn_api->IsAvailable());
base::string16 rp_id = base::UTF8ToUTF16(request.rp.id);
base::string16 rp_name = base::UTF8ToUTF16(request.rp.name.value_or(""));
base::string16 rp_icon_url = OptionalGURLToUTF16(request.rp.icon_url);
WEBAUTHN_RP_ENTITY_INFORMATION rp_info{
WEBAUTHN_RP_ENTITY_INFORMATION_CURRENT_VERSION, base::as_wcstr(rp_id),
base::as_wcstr(rp_name), base::as_wcstr(rp_icon_url)};
base::string16 user_name = base::UTF8ToUTF16(request.user.name.value_or(""));
base::string16 user_icon_url = OptionalGURLToUTF16(request.user.icon_url);
base::string16 user_display_name =
base::UTF8ToUTF16(request.user.display_name.value_or(""));
std::vector<uint8_t> user_id = request.user.id;
WEBAUTHN_USER_ENTITY_INFORMATION user_info{
WEBAUTHN_USER_ENTITY_INFORMATION_CURRENT_VERSION,
user_id.size(),
const_cast<unsigned char*>(user_id.data()),
base::as_wcstr(user_name),
base::as_wcstr(user_icon_url),
base::as_wcstr(user_display_name), // This appears to be ignored.
};
std::vector<WEBAUTHN_COSE_CREDENTIAL_PARAMETER>
cose_credential_parameter_values;
for (const PublicKeyCredentialParams::CredentialInfo& credential_info :
request.public_key_credential_params.public_key_credential_params()) {
if (credential_info.type != CredentialType::kPublicKey) {
continue;
}
cose_credential_parameter_values.push_back(
{WEBAUTHN_COSE_CREDENTIAL_PARAMETER_CURRENT_VERSION,
WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY, credential_info.algorithm});
}
WEBAUTHN_COSE_CREDENTIAL_PARAMETERS cose_credential_parameters{
cose_credential_parameter_values.size(),
cose_credential_parameter_values.data()};
std::string client_data_json = request.client_data_json;
WEBAUTHN_CLIENT_DATA client_data{
WEBAUTHN_CLIENT_DATA_CURRENT_VERSION, client_data_json.size(),
const_cast<unsigned char*>(
reinterpret_cast<const unsigned char*>(client_data_json.data())),
WEBAUTHN_HASH_ALGORITHM_SHA_256};
std::vector<WEBAUTHN_EXTENSION> extensions;
if (request.hmac_secret) {
static BOOL kHMACSecretTrue = TRUE;
extensions.emplace_back(
WEBAUTHN_EXTENSION{WEBAUTHN_EXTENSIONS_IDENTIFIER_HMAC_SECRET,
sizeof(BOOL), static_cast<void*>(&kHMACSecretTrue)});
}
WEBAUTHN_CRED_PROTECT_EXTENSION_IN maybe_cred_protect_extension;
if (request.cred_protect) {
// MakeCredentialRequestHandler rejects a request with credProtect
// enforced=true if webauthn.dll does not support credProtect.
if (request.cred_protect->second &&
webauthn_api->Version() < WEBAUTHN_API_VERSION_2) {
NOTREACHED();
return {CtapDeviceResponseCode::kCtap2ErrNotAllowed, base::nullopt};
}
maybe_cred_protect_extension = WEBAUTHN_CRED_PROTECT_EXTENSION_IN{
/*dwCredProtect=*/static_cast<uint8_t>(request.cred_protect->first),
/*bRequireCredProtect=*/request.cred_protect->second,
};
extensions.emplace_back(WEBAUTHN_EXTENSION{
/*pwszExtensionIdentifier=*/WEBAUTHN_EXTENSIONS_IDENTIFIER_CRED_PROTECT,
/*cbExtension=*/sizeof(WEBAUTHN_CRED_PROTECT_EXTENSION_IN),
/*pvExtension=*/&maybe_cred_protect_extension,
});
}
uint32_t authenticator_attachment;
if (request.is_u2f_only) {
authenticator_attachment =
WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM_U2F_V2;
} else if (request.is_incognito_mode) {
// Disable all platform authenticators in incognito mode. We are going to
// revisit this in crbug/908622.
authenticator_attachment = WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM;
} else {
authenticator_attachment =
ToWinAuthenticatorAttachment(request.authenticator_attachment);
}
// Note that entries in |exclude_list_credentials| hold pointers
// into request.exclude_list.
std::vector<WEBAUTHN_CREDENTIAL_EX> exclude_list_credentials =
ToWinCredentialExVector(&request.exclude_list.value());
std::vector<WEBAUTHN_CREDENTIAL_EX*> exclude_list_ptrs;
std::transform(
exclude_list_credentials.begin(), exclude_list_credentials.end(),
std::back_inserter(exclude_list_ptrs), [](auto& cred) { return &cred; });
WEBAUTHN_CREDENTIAL_LIST exclude_credential_list{exclude_list_ptrs.size(),
exclude_list_ptrs.data()};
WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS options{
WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_3,
kWinWebAuthnTimeoutMilliseconds,
WEBAUTHN_CREDENTIALS{
0, nullptr}, // Ignored because pExcludeCredentialList is set.
WEBAUTHN_EXTENSIONS{extensions.size(), extensions.data()},
authenticator_attachment,
request.resident_key_required,
ToWinUserVerificationRequirement(request.user_verification),
ToWinAttestationConveyancePreference(request.attestation_preference),
/*dwFlags=*/0,
&cancellation_id,
&exclude_credential_list,
};
WEBAUTHN_CREDENTIAL_ATTESTATION* credential_attestation = nullptr;
std::unique_ptr<WEBAUTHN_CREDENTIAL_ATTESTATION,
std::function<void(PWEBAUTHN_CREDENTIAL_ATTESTATION)>>
credential_attestation_deleter(
credential_attestation,
[webauthn_api](PWEBAUTHN_CREDENTIAL_ATTESTATION ptr) {
webauthn_api->FreeCredentialAttestation(ptr);
});
FIDO_LOG(DEBUG) << "WebAuthNAuthenticatorMakeCredential("
<< "rp=" << rp_info << ", user=" << user_info
<< ", cose_credential_parameters="
<< cose_credential_parameters
<< ", client_data=" << client_data << ", options=" << options
<< ")";
HRESULT hresult = webauthn_api->AuthenticatorMakeCredential(
h_wnd, &rp_info, &user_info, &cose_credential_parameters, &client_data,
&options, &credential_attestation);
if (hresult != S_OK) {
FIDO_LOG(DEBUG) << "WebAuthNAuthenticatorMakeCredential()="
<< webauthn_api->GetErrorName(hresult);
return {WinErrorNameToCtapDeviceResponseCode(
base::as_u16cstr(webauthn_api->GetErrorName(hresult))),
base::nullopt};
}
FIDO_LOG(DEBUG) << "WebAuthNAuthenticatorMakeCredential()="
<< *credential_attestation;
return {CtapDeviceResponseCode::kSuccess,
ToAuthenticatorMakeCredentialResponse(*credential_attestation)};
}
std::pair<CtapDeviceResponseCode,
base::Optional<AuthenticatorGetAssertionResponse>>
AuthenticatorGetAssertionBlocking(WinWebAuthnApi* webauthn_api,
HWND h_wnd,
GUID cancellation_id,
CtapGetAssertionRequest request) {
DCHECK(webauthn_api->IsAvailable());
base::string16 rp_id16 = base::UTF8ToUTF16(request.rp_id);
std::string client_data_json = request.client_data_json;
WEBAUTHN_CLIENT_DATA client_data{
WEBAUTHN_CLIENT_DATA_CURRENT_VERSION, client_data_json.size(),
const_cast<unsigned char*>(
reinterpret_cast<const unsigned char*>(client_data_json.data())),
WEBAUTHN_HASH_ALGORITHM_SHA_256};
base::Optional<base::string16> opt_app_id16 = base::nullopt;
if (request.app_id) {
opt_app_id16 = base::UTF8ToUTF16(
base::StringPiece(reinterpret_cast<const char*>(request.app_id->data()),
request.app_id->size()));
}
// Note that entries in |allow_list_credentials| hold pointers into
// request.allow_list.
std::vector<WEBAUTHN_CREDENTIAL_EX> allow_list_credentials =
ToWinCredentialExVector(&request.allow_list);
std::vector<WEBAUTHN_CREDENTIAL_EX*> allow_list_ptrs;
std::transform(allow_list_credentials.begin(), allow_list_credentials.end(),
std::back_inserter(allow_list_ptrs),
[](auto& cred) { return &cred; });
WEBAUTHN_CREDENTIAL_LIST allow_credential_list{allow_list_ptrs.size(),
allow_list_ptrs.data()};
// Note that entries in |legacy_credentials| hold pointers into
// request.allow_list.
auto legacy_credentials = ToWinCredentialVector(&request.allow_list);
uint32_t authenticator_attachment;
if (request.is_u2f_only) {
authenticator_attachment =
WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM_U2F_V2;
} else if (request.is_incognito_mode) {
// Disable all platform authenticators in incognito mode. We are going to
// revisit this in crbug/908622.
authenticator_attachment = WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM;
} else {
authenticator_attachment = WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY;
}
static BOOL kUseAppIdTrue = TRUE; // const
static BOOL kUseAppIdFalse = FALSE; // const
WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS options{
WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_4,
kWinWebAuthnTimeoutMilliseconds,
// As of Nov 2018, the WebAuthNAuthenticatorGetAssertion method will
// fail to challenge credentials via CTAP1 if the allowList is passed
// in the extended form in WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS
// (i.e. pAllowCredentialList instead of CredentialList). The legacy
// CredentialList field works fine, but does not support setting
// transport restrictions on the credential descriptor.
//
// As a workaround, MS tells us to also set the CredentialList
// parameter with an accurate cCredentials count and some arbitrary
// pCredentials data.
WEBAUTHN_CREDENTIALS{legacy_credentials.size(),
legacy_credentials.data()},
WEBAUTHN_EXTENSIONS{0, nullptr},
authenticator_attachment,
ToWinUserVerificationRequirement(request.user_verification),
/*dwFlags=*/0,
opt_app_id16 ? base::as_wcstr(*opt_app_id16) : nullptr,
opt_app_id16 ? &kUseAppIdTrue : &kUseAppIdFalse,
&cancellation_id,
&allow_credential_list,
};
WEBAUTHN_ASSERTION* assertion = nullptr;
std::unique_ptr<WEBAUTHN_ASSERTION, std::function<void(PWEBAUTHN_ASSERTION)>>
assertion_deleter(assertion, [webauthn_api](PWEBAUTHN_ASSERTION ptr) {
webauthn_api->FreeAssertion(ptr);
});
FIDO_LOG(DEBUG) << "WebAuthNAuthenticatorGetAssertion("
<< "rp_id=\"" << rp_id16 << "\", client_data=" << client_data
<< ", options=" << options << ")";
HRESULT hresult = webauthn_api->AuthenticatorGetAssertion(
h_wnd, base::as_wcstr(rp_id16), &client_data, &options, &assertion);
if (hresult != S_OK) {
FIDO_LOG(DEBUG) << "WebAuthNAuthenticatorGetAssertion()="
<< webauthn_api->GetErrorName(hresult);
return {WinErrorNameToCtapDeviceResponseCode(
base::as_u16cstr(webauthn_api->GetErrorName(hresult))),
base::nullopt};
}
FIDO_LOG(DEBUG) << "WebAuthNAuthenticatorGetAssertion()=" << *assertion;
return {CtapDeviceResponseCode::kSuccess,
ToAuthenticatorGetAssertionResponse(*assertion)};
}
bool SupportsCredProtectExtension(WinWebAuthnApi* api) {
return api->IsAvailable() && api->Version() >= WEBAUTHN_API_VERSION_2;
}
} // namespace device