dom/u2f/U2F.cpp
author Cosmin Sabou <csabou@mozilla.com>
Sat, 02 Mar 2019 20:40:01 +0200
changeset 462132 bf3cbcc825276823afba2778333cf62cfa75039c
parent 461887 f41cee9bf14931b453838d1bbcbda528e3b064e8
child 462821 bfe72a7c57bde0d1825ba43cbd9afa34d03ed00d
permissions -rw-r--r--
Backed out 16 changesets (bug 827976) for causing bug 1532054. a=backout Backed out changeset 54c0b12443ed (bug 827976) Backed out changeset 92b45080d080 (bug 827976) Backed out changeset 443087a359f9 (bug 827976) Backed out changeset f976c2d4cebb (bug 827976) Backed out changeset 66cffb171024 (bug 827976) Backed out changeset 845e1d0b2402 (bug 827976) Backed out changeset fd94066a1d76 (bug 827976) Backed out changeset e253b264e7bd (bug 827976) Backed out changeset 07ef335770a8 (bug 827976) Backed out changeset 9d3805d77b99 (bug 827976) Backed out changeset eed600ceb606 (bug 827976) Backed out changeset 3b64368cff52 (bug 827976) Backed out changeset 69d0378e0c09 (bug 827976) Backed out changeset 9203871a5c6f (bug 827976) Backed out changeset 96a507c818e2 (bug 827976) Backed out changeset e254496ff95b (bug 827976)

/* -*- 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_END_INHERITING(WebAuthnManagerBase)

NS_IMPL_ADDREF_INHERITED(U2F, WebAuthnManagerBase)
NS_IMPL_RELEASE_INHERITED(U2F, WebAuthnManagerBase)

NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_INHERITED(U2F, WebAuthnManagerBase,
                                                mTransaction)

/***********************************************************************
 * 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 */
  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<Document> 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 U2F_Binding::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