gecko-dev/dom/u2f/U2F.cpp

560 строки
16 KiB
C++
Исходник Обычный вид История

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/dom/U2F.h"
#include "mozilla/dom/WebCryptoCommon.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/dom/WebAuthnTransactionChild.h"
#include "mozilla/dom/WebAuthnUtil.h"
#include "nsContentUtils.h"
#include "nsIEffectiveTLDService.h"
#include "nsNetUtil.h"
#include "nsURLParsers.h"
using namespace mozilla::ipc;
// Forward decl because of nsHTMLDocument.h's complex dependency on /layout/style
class nsHTMLDocument {
public:
bool IsRegistrableDomainSuffixOfOrEqualTo(const nsAString& aHostSuffixString,
const nsACString& aOrigHost);
};
namespace mozilla {
namespace dom {
static mozilla::LazyLogModule gU2FLog("u2fmanager");
NS_NAMED_LITERAL_STRING(kFinishEnrollment, "navigator.id.finishEnrollment");
NS_NAMED_LITERAL_STRING(kGetAssertion, "navigator.id.getAssertion");
// Bug #1436078 - Permit Google Accounts. Remove in Bug #1436085 in Jan 2023.
NS_NAMED_LITERAL_STRING(kGoogleAccountsAppId1,
"https://www.gstatic.com/securitykey/origins.json");
NS_NAMED_LITERAL_STRING(kGoogleAccountsAppId2,
"https://www.gstatic.com/securitykey/a/google.com/origins.json");
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(U2F)
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(U2F)
NS_IMPL_CYCLE_COLLECTING_RELEASE(U2F)
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(U2F, mParent)
/***********************************************************************
* Utility Functions
**********************************************************************/
static ErrorCode
ConvertNSResultToErrorCode(const nsresult& aError)
{
if (aError == NS_ERROR_DOM_TIMEOUT_ERR) {
return ErrorCode::TIMEOUT;
}
/* Emitted by U2F{Soft,HID}TokenManager when we really mean ineligible */
Bug 1460767 - Return device ineligible when appropriate for U2F r=ttaubert Summary: FIDO U2F's specification says that when the wrong security key responds to a signature, or when an already-registered key exists, that the UA should return error code 4, DEVICE_INELIGIBLE. We used to do that, but adjusted some things for WebAuthn and now we don't. This changes the soft token to return that at the appropriate times, and updates the expectations of U2F.cpp that it should use InvalidStateError as the signal to reutrn DEVICE_INELIGIBLE. Also, note that WebAuthn's specification says that if any authenticator returns "InvalidStateError" that it should be propagated, as it indicates that the authenticator obtained user consent and failed to complete its job [1]. This change to the Soft Token affects the WebAuthn tests, but in a good way. Reading the WebAuthn spec, we should not be returning NotAllowedError when there is consent from the user via the token (which the softtoken always deliveres). As such, this adjusts the affected WebAuthn tests, and adds a couple useful checks to test_webauthn_get_assertion.html for future purposes. [1] https://w3c.github.io/webauthn/#createCredential section 5.1.3 "Create a new credential", Step 20, Note 2: "If any authenticator returns an error status equivalent to "InvalidStateError"..." Test Plan: https://treeherder.mozilla.org/#/jobs?repo=try&revision=f2fc930f7fc8eea69b1ebc96748fe95e150a92a4 Reviewers: ttaubert Bug #: 1460767 Differential Revision: https://phabricator.services.mozilla.com/D1269 --HG-- extra : transplant_source : M%5B%93%81%29%7E%B2%E8%24%05%A6%96%8BUN%C9%FB%3E%B3h
2018-05-11 02:36:18 +03:00
if (aError == NS_ERROR_DOM_INVALID_STATE_ERR) {
return ErrorCode::DEVICE_INELIGIBLE;
}
return ErrorCode::OTHER_ERROR;
}
static uint32_t
AdjustedTimeoutMillis(const Optional<Nullable<int32_t>>& opt_aSeconds)
{
uint32_t adjustedTimeoutMillis = 30000u;
if (opt_aSeconds.WasPassed() && !opt_aSeconds.Value().IsNull()) {
adjustedTimeoutMillis = opt_aSeconds.Value().Value() * 1000u;
adjustedTimeoutMillis = std::max(15000u, adjustedTimeoutMillis);
adjustedTimeoutMillis = std::min(120000u, adjustedTimeoutMillis);
}
return adjustedTimeoutMillis;
}
static nsresult
AssembleClientData(const nsAString& aOrigin, const nsAString& aTyp,
const nsAString& aChallenge,
/* out */ nsString& aClientData)
{
MOZ_ASSERT(NS_IsMainThread());
U2FClientData clientDataObject;
clientDataObject.mTyp.Construct(aTyp); // "Typ" from the U2F specification
clientDataObject.mChallenge.Construct(aChallenge);
clientDataObject.mOrigin.Construct(aOrigin);
if (NS_WARN_IF(!clientDataObject.ToJSON(aClientData))) {
return NS_ERROR_FAILURE;
}
return NS_OK;
}
static void
RegisteredKeysToScopedCredentialList(const nsAString& aAppId,
const nsTArray<RegisteredKey>& aKeys,
nsTArray<WebAuthnScopedCredential>& aList)
{
for (const RegisteredKey& key : aKeys) {
// Check for required attributes
if (!key.mVersion.WasPassed() || !key.mKeyHandle.WasPassed() ||
key.mVersion.Value() != kRequiredU2FVersion) {
continue;
}
// If this key's mAppId doesn't match the invocation, we can't handle it.
if (key.mAppId.WasPassed() && !key.mAppId.Value().Equals(aAppId)) {
continue;
}
CryptoBuffer keyHandle;
nsresult rv = keyHandle.FromJwkBase64(key.mKeyHandle.Value());
if (NS_WARN_IF(NS_FAILED(rv))) {
continue;
}
WebAuthnScopedCredential c;
c.id() = keyHandle;
aList.AppendElement(c);
}
}
/***********************************************************************
* U2F JavaScript API Implementation
**********************************************************************/
U2F::~U2F()
{
MOZ_ASSERT(NS_IsMainThread());
if (mTransaction.isSome()) {
RejectTransaction(NS_ERROR_ABORT);
}
if (mChild) {
RefPtr<WebAuthnTransactionChild> c;
mChild.swap(c);
c->Disconnect();
}
}
void
U2F::Init(ErrorResult& aRv)
{
MOZ_ASSERT(mParent);
nsCOMPtr<nsIDocument> doc = mParent->GetDoc();
MOZ_ASSERT(doc);
if (!doc) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
nsIPrincipal* principal = doc->NodePrincipal();
aRv = nsContentUtils::GetUTFOrigin(principal, mOrigin);
if (NS_WARN_IF(aRv.Failed())) {
return;
}
if (NS_WARN_IF(mOrigin.IsEmpty())) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
}
/* virtual */ JSObject*
U2F::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
{
return U2FBinding::Wrap(aCx, this, aGivenProto);
}
template<typename T, typename C>
void
U2F::ExecuteCallback(T& aResp, nsMainThreadPtrHandle<C>& aCb)
{
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aCb);
// Assert that mTransaction was cleared before before we were called to allow
// reentrancy from microtask checkpoints.
MOZ_ASSERT(mTransaction.isNothing());
ErrorResult error;
aCb->Call(aResp, error);
NS_WARNING_ASSERTION(!error.Failed(), "dom::U2F::Promise callback failed");
error.SuppressException(); // Useful exceptions already emitted
}
void
U2F::Register(const nsAString& aAppId,
const Sequence<RegisterRequest>& aRegisterRequests,
const Sequence<RegisteredKey>& aRegisteredKeys,
U2FRegisterCallback& aCallback,
const Optional<Nullable<int32_t>>& opt_aTimeoutSeconds,
ErrorResult& aRv)
{
MOZ_ASSERT(NS_IsMainThread());
if (mTransaction.isSome()) {
CancelTransaction(NS_ERROR_ABORT);
}
nsMainThreadPtrHandle<U2FRegisterCallback> callback(
new nsMainThreadPtrHolder<U2FRegisterCallback>("U2F::Register::callback",
&aCallback));
// Ensure we have a callback.
if (NS_WARN_IF(!callback)) {
return;
}
// Evaluate the AppID
nsString adjustedAppId(aAppId);
if (!EvaluateAppID(mParent, mOrigin, U2FOperation::Register, adjustedAppId)) {
RegisterResponse response;
response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
ExecuteCallback(response, callback);
return;
}
nsAutoString clientDataJSON;
// Pick the first valid RegisterRequest; we can only work with one.
CryptoBuffer challenge;
for (const RegisterRequest& req : aRegisterRequests) {
if (!req.mChallenge.WasPassed() || !req.mVersion.WasPassed() ||
req.mVersion.Value() != kRequiredU2FVersion) {
continue;
}
if (!challenge.Assign(NS_ConvertUTF16toUTF8(req.mChallenge.Value()))) {
continue;
}
nsresult rv = AssembleClientData(mOrigin, kFinishEnrollment,
req.mChallenge.Value(), clientDataJSON);
if (NS_WARN_IF(NS_FAILED(rv))) {
continue;
}
}
// Did we not get a valid RegisterRequest? Abort.
if (clientDataJSON.IsEmpty()) {
RegisterResponse response;
response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
ExecuteCallback(response, callback);
return;
}
// Build the exclusion list, if any
nsTArray<WebAuthnScopedCredential> excludeList;
RegisteredKeysToScopedCredentialList(adjustedAppId, aRegisteredKeys,
excludeList);
if (!MaybeCreateBackgroundActor()) {
RegisterResponse response;
response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::OTHER_ERROR));
ExecuteCallback(response, callback);
return;
}
ListenForVisibilityEvents();
NS_ConvertUTF16toUTF8 clientData(clientDataJSON);
uint32_t adjustedTimeoutMillis = AdjustedTimeoutMillis(opt_aTimeoutSeconds);
WebAuthnMakeCredentialInfo info(mOrigin,
adjustedAppId,
challenge,
clientData,
adjustedTimeoutMillis,
excludeList,
null_t() /* no extra info for U2F */);
MOZ_ASSERT(mTransaction.isNothing());
mTransaction = Some(U2FTransaction(AsVariant(callback)));
mChild->SendRequestRegister(mTransaction.ref().mId, info);
}
void
U2F::FinishMakeCredential(const uint64_t& aTransactionId,
const WebAuthnMakeCredentialResult& aResult)
{
MOZ_ASSERT(NS_IsMainThread());
// Check for a valid transaction.
if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
return;
}
if (NS_WARN_IF(!mTransaction.ref().HasRegisterCallback())) {
RejectTransaction(NS_ERROR_ABORT);
return;
}
// A CTAP2 response.
if (aResult.RegistrationData().Length() == 0) {
RejectTransaction(NS_ERROR_ABORT);
return;
}
CryptoBuffer clientDataBuf;
if (NS_WARN_IF(!clientDataBuf.Assign(aResult.ClientDataJSON()))) {
RejectTransaction(NS_ERROR_ABORT);
return;
}
CryptoBuffer regBuf;
if (NS_WARN_IF(!regBuf.Assign(aResult.RegistrationData()))) {
RejectTransaction(NS_ERROR_ABORT);
return;
}
nsString clientDataBase64;
nsString registrationDataBase64;
nsresult rvClientData = clientDataBuf.ToJwkBase64(clientDataBase64);
nsresult rvRegistrationData = regBuf.ToJwkBase64(registrationDataBase64);
if (NS_WARN_IF(NS_FAILED(rvClientData)) ||
NS_WARN_IF(NS_FAILED(rvRegistrationData))) {
RejectTransaction(NS_ERROR_ABORT);
return;
}
// Assemble a response object to return
RegisterResponse response;
response.mVersion.Construct(kRequiredU2FVersion);
response.mClientData.Construct(clientDataBase64);
response.mRegistrationData.Construct(registrationDataBase64);
response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::OK));
// Keep the callback pointer alive.
nsMainThreadPtrHandle<U2FRegisterCallback> callback(
mTransaction.ref().GetRegisterCallback());
ClearTransaction();
ExecuteCallback(response, callback);
}
void
U2F::Sign(const nsAString& aAppId,
const nsAString& aChallenge,
const Sequence<RegisteredKey>& aRegisteredKeys,
U2FSignCallback& aCallback,
const Optional<Nullable<int32_t>>& opt_aTimeoutSeconds,
ErrorResult& aRv)
{
MOZ_ASSERT(NS_IsMainThread());
if (mTransaction.isSome()) {
CancelTransaction(NS_ERROR_ABORT);
}
nsMainThreadPtrHandle<U2FSignCallback> callback(
new nsMainThreadPtrHolder<U2FSignCallback>("U2F::Sign::callback",
&aCallback));
// Ensure we have a callback.
if (NS_WARN_IF(!callback)) {
return;
}
// Evaluate the AppID
nsString adjustedAppId(aAppId);
if (!EvaluateAppID(mParent, mOrigin, U2FOperation::Sign, adjustedAppId)) {
SignResponse response;
response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
ExecuteCallback(response, callback);
return;
}
// Produce the AppParam from the current AppID
nsCString cAppId = NS_ConvertUTF16toUTF8(adjustedAppId);
nsAutoString clientDataJSON;
nsresult rv = AssembleClientData(mOrigin, kGetAssertion, aChallenge,
clientDataJSON);
if (NS_WARN_IF(NS_FAILED(rv))) {
SignResponse response;
response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
ExecuteCallback(response, callback);
return;
}
CryptoBuffer challenge;
if (!challenge.Assign(NS_ConvertUTF16toUTF8(aChallenge))) {
SignResponse response;
response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::OTHER_ERROR));
ExecuteCallback(response, callback);
return;
}
// Build the key list, if any
nsTArray<WebAuthnScopedCredential> permittedList;
RegisteredKeysToScopedCredentialList(adjustedAppId, aRegisteredKeys,
permittedList);
if (!MaybeCreateBackgroundActor()) {
SignResponse response;
response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::OTHER_ERROR));
ExecuteCallback(response, callback);
return;
}
ListenForVisibilityEvents();
// Always blank for U2F
nsTArray<WebAuthnExtension> extensions;
NS_ConvertUTF16toUTF8 clientData(clientDataJSON);
uint32_t adjustedTimeoutMillis = AdjustedTimeoutMillis(opt_aTimeoutSeconds);
WebAuthnGetAssertionInfo info(mOrigin,
adjustedAppId,
challenge,
clientData,
adjustedTimeoutMillis,
permittedList,
null_t() /* no extra info for U2F */);
MOZ_ASSERT(mTransaction.isNothing());
mTransaction = Some(U2FTransaction(AsVariant(callback)));
mChild->SendRequestSign(mTransaction.ref().mId, info);
}
void
U2F::FinishGetAssertion(const uint64_t& aTransactionId,
const WebAuthnGetAssertionResult& aResult)
{
MOZ_ASSERT(NS_IsMainThread());
// Check for a valid transaction.
if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
return;
}
if (NS_WARN_IF(!mTransaction.ref().HasSignCallback())) {
RejectTransaction(NS_ERROR_ABORT);
return;
}
// A CTAP2 response.
if (aResult.SignatureData().Length() == 0) {
RejectTransaction(NS_ERROR_ABORT);
return;
}
CryptoBuffer clientDataBuf;
if (NS_WARN_IF(!clientDataBuf.Assign(aResult.ClientDataJSON()))) {
RejectTransaction(NS_ERROR_ABORT);
return;
}
CryptoBuffer credBuf;
if (NS_WARN_IF(!credBuf.Assign(aResult.KeyHandle()))) {
RejectTransaction(NS_ERROR_ABORT);
return;
}
CryptoBuffer sigBuf;
if (NS_WARN_IF(!sigBuf.Assign(aResult.SignatureData()))) {
RejectTransaction(NS_ERROR_ABORT);
return;
}
// Assemble a response object to return
nsString clientDataBase64;
nsString signatureDataBase64;
nsString keyHandleBase64;
nsresult rvClientData = clientDataBuf.ToJwkBase64(clientDataBase64);
nsresult rvSignatureData = sigBuf.ToJwkBase64(signatureDataBase64);
nsresult rvKeyHandle = credBuf.ToJwkBase64(keyHandleBase64);
if (NS_WARN_IF(NS_FAILED(rvClientData)) ||
NS_WARN_IF(NS_FAILED(rvSignatureData) ||
NS_WARN_IF(NS_FAILED(rvKeyHandle)))) {
RejectTransaction(NS_ERROR_ABORT);
return;
}
SignResponse response;
response.mKeyHandle.Construct(keyHandleBase64);
response.mClientData.Construct(clientDataBase64);
response.mSignatureData.Construct(signatureDataBase64);
response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::OK));
// Keep the callback pointer alive.
nsMainThreadPtrHandle<U2FSignCallback> callback(
mTransaction.ref().GetSignCallback());
ClearTransaction();
ExecuteCallback(response, callback);
}
void
U2F::ClearTransaction()
{
if (!NS_WARN_IF(mTransaction.isNothing())) {
StopListeningForVisibilityEvents();
}
mTransaction.reset();
}
void
U2F::RejectTransaction(const nsresult& aError)
{
if (NS_WARN_IF(mTransaction.isNothing())) {
return;
}
StopListeningForVisibilityEvents();
// Clear out mTransaction before calling ExecuteCallback() below to allow
// reentrancy from microtask checkpoints.
Maybe<U2FTransaction> maybeTransaction(std::move(mTransaction));
MOZ_ASSERT(mTransaction.isNothing() && maybeTransaction.isSome());
U2FTransaction& transaction = maybeTransaction.ref();
ErrorCode code = ConvertNSResultToErrorCode(aError);
if (transaction.HasRegisterCallback()) {
RegisterResponse response;
response.mErrorCode.Construct(static_cast<uint32_t>(code));
ExecuteCallback(response, transaction.GetRegisterCallback());
}
if (transaction.HasSignCallback()) {
SignResponse response;
response.mErrorCode.Construct(static_cast<uint32_t>(code));
ExecuteCallback(response, transaction.GetSignCallback());
}
}
void
U2F::CancelTransaction(const nsresult& aError)
{
if (!NS_WARN_IF(!mChild || mTransaction.isNothing())) {
mChild->SendRequestCancel(mTransaction.ref().mId);
}
RejectTransaction(aError);
}
void
U2F::RequestAborted(const uint64_t& aTransactionId, const nsresult& aError)
{
MOZ_ASSERT(NS_IsMainThread());
if (mTransaction.isSome() && mTransaction.ref().mId == aTransactionId) {
RejectTransaction(aError);
}
}
} // namespace dom
} // namespace mozilla