tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

WebAuthnHandler.cpp (36115B)


      1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 #include "mozilla/dom/WebAuthnHandler.h"
      8 
      9 #include "WebAuthnCoseIdentifiers.h"
     10 #include "WebAuthnEnumStrings.h"
     11 #include "WebAuthnTransportIdentifiers.h"
     12 #include "hasht.h"
     13 #include "mozilla/Base64.h"
     14 #include "mozilla/BasePrincipal.h"
     15 #include "mozilla/BounceTrackingProtection.h"
     16 #include "mozilla/dom/AuthenticatorAssertionResponse.h"
     17 #include "mozilla/dom/AuthenticatorAttestationResponse.h"
     18 #include "mozilla/dom/PWebAuthnTransaction.h"
     19 #include "mozilla/dom/PublicKeyCredential.h"
     20 #include "mozilla/dom/WebAuthnTransactionChild.h"
     21 #include "mozilla/dom/WebAuthnUtil.h"
     22 #include "mozilla/dom/WindowGlobalChild.h"
     23 #include "mozilla/glean/DomWebauthnMetrics.h"
     24 #include "nsHTMLDocument.h"
     25 #include "nsIURIMutator.h"
     26 #include "nsThreadUtils.h"
     27 
     28 #ifdef XP_WIN
     29 #  include "WinWebAuthnService.h"
     30 #endif
     31 
     32 using namespace mozilla::ipc;
     33 
     34 namespace mozilla::dom {
     35 
     36 /***********************************************************************
     37 * Statics
     38 **********************************************************************/
     39 
     40 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebAuthnHandler)
     41  NS_INTERFACE_MAP_ENTRY(nsISupports)
     42 NS_INTERFACE_MAP_END
     43 
     44 NS_IMPL_CYCLE_COLLECTION(WebAuthnHandler, mWindow, mTransaction)
     45 
     46 NS_IMPL_CYCLE_COLLECTING_ADDREF(WebAuthnHandler)
     47 NS_IMPL_CYCLE_COLLECTING_RELEASE(WebAuthnHandler)
     48 
     49 /***********************************************************************
     50 * Utility Functions
     51 **********************************************************************/
     52 
     53 static uint8_t SerializeTransports(
     54    const mozilla::dom::Sequence<nsString>& aTransports) {
     55  uint8_t transports = 0;
     56 
     57  // We ignore unknown transports for forward-compatibility, but this
     58  // needs to be reviewed if values are added to the
     59  // AuthenticatorTransport enum.
     60  static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 3);
     61  for (const nsAString& str : aTransports) {
     62    if (str.EqualsLiteral(MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_USB)) {
     63      transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_USB;
     64    } else if (str.EqualsLiteral(MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_NFC)) {
     65      transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_NFC;
     66    } else if (str.EqualsLiteral(MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_BLE)) {
     67      transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_BLE;
     68    } else if (str.EqualsLiteral(
     69                   MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_INTERNAL)) {
     70      transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_INTERNAL;
     71    } else if (str.EqualsLiteral(MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_HYBRID)) {
     72      transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_HYBRID;
     73    }
     74  }
     75  return transports;
     76 }
     77 
     78 /***********************************************************************
     79 * WebAuthnHandler Implementation
     80 **********************************************************************/
     81 
     82 WebAuthnHandler::~WebAuthnHandler() {
     83  MOZ_ASSERT(NS_IsMainThread());
     84  if (mActor) {
     85    if (mTransaction.isSome()) {
     86      CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
     87    }
     88    mActor->SetHandler(nullptr);
     89  }
     90 }
     91 
     92 bool WebAuthnHandler::MaybeCreateActor() {
     93  MOZ_ASSERT(NS_IsMainThread());
     94 
     95  if (mActor) {
     96    return true;
     97  }
     98 
     99  RefPtr<WebAuthnTransactionChild> actor = new WebAuthnTransactionChild();
    100 
    101  WindowGlobalChild* windowGlobalChild = mWindow->GetWindowGlobalChild();
    102  if (!windowGlobalChild ||
    103      !windowGlobalChild->SendPWebAuthnTransactionConstructor(actor)) {
    104    return false;
    105  }
    106 
    107  mActor = actor;
    108  mActor->SetHandler(this);
    109 
    110  return true;
    111 }
    112 
    113 void WebAuthnHandler::ActorDestroyed() {
    114  MOZ_ASSERT(NS_IsMainThread());
    115  mActor = nullptr;
    116 }
    117 
    118 already_AddRefed<Promise> WebAuthnHandler::MakeCredential(
    119    const PublicKeyCredentialCreationOptions& aOptions,
    120    const Optional<OwningNonNull<AbortSignal>>& aSignal, ErrorResult& aError) {
    121  MOZ_ASSERT(NS_IsMainThread());
    122 
    123  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mWindow);
    124 
    125  RefPtr<Promise> promise = Promise::Create(global, aError);
    126  if (aError.Failed()) {
    127    return nullptr;
    128  }
    129 
    130  if (mTransaction.isSome()) {
    131    // abort the old transaction and take over control from here.
    132    CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
    133  }
    134 
    135  if (!MaybeCreateActor()) {
    136    promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
    137    return promise.forget();
    138  }
    139 
    140  nsCOMPtr<Document> doc = mWindow->GetDoc();
    141  if (!IsWebAuthnAllowedInDocument(doc)) {
    142    promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    143    return promise.forget();
    144  }
    145 
    146  nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
    147 
    148  nsCString rpId;
    149  if (aOptions.mRp.mId.WasPassed()) {
    150    rpId = NS_ConvertUTF16toUTF8(aOptions.mRp.mId.Value());
    151  } else {
    152    nsresult rv = DefaultRpId(principal, rpId);
    153    if (NS_FAILED(rv)) {
    154      promise->MaybeReject(NS_ERROR_FAILURE);
    155      return promise.forget();
    156    }
    157  }
    158  if (!IsValidRpId(principal, rpId)) {
    159    promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    160    return promise.forget();
    161  }
    162 
    163  // Enforce 5.4.3 User Account Parameters for Credential Generation
    164  // When we add UX, we'll want to do more with this value, but for now
    165  // we just have to verify its correctness.
    166  CryptoBuffer userId;
    167  userId.Assign(aOptions.mUser.mId);
    168  if (userId.Length() > 64) {
    169    promise->MaybeRejectWithTypeError("user.id is too long");
    170    return promise.forget();
    171  }
    172 
    173  // If timeoutSeconds was specified, check if its value lies within a
    174  // reasonable range as defined by the platform and if not, correct it to the
    175  // closest value lying within that range.
    176 
    177  uint32_t adjustedTimeout = 30000;
    178  if (aOptions.mTimeout.WasPassed()) {
    179    adjustedTimeout = aOptions.mTimeout.Value();
    180    adjustedTimeout = std::max(15000u, adjustedTimeout);
    181    adjustedTimeout = std::min(120000u, adjustedTimeout);
    182  }
    183 
    184  // <https://w3c.github.io/webauthn/#sctn-appid-extension>
    185  if (aOptions.mExtensions.mAppid.WasPassed()) {
    186    promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
    187    return promise.forget();
    188  }
    189 
    190  // Process each element of mPubKeyCredParams using the following steps, to
    191  // produce a new sequence of coseAlgos.
    192  nsTArray<CoseAlg> coseAlgos;
    193  // If pubKeyCredParams is empty, append ES256 and RS256
    194  if (aOptions.mPubKeyCredParams.IsEmpty()) {
    195    coseAlgos.AppendElement(static_cast<long>(CoseAlgorithmIdentifier::ES256));
    196    coseAlgos.AppendElement(static_cast<long>(CoseAlgorithmIdentifier::RS256));
    197  } else {
    198    for (size_t a = 0; a < aOptions.mPubKeyCredParams.Length(); ++a) {
    199      // If current.type does not contain a PublicKeyCredentialType
    200      // supported by this implementation, then stop processing current and move
    201      // on to the next element in mPubKeyCredParams.
    202      if (!aOptions.mPubKeyCredParams[a].mType.EqualsLiteral(
    203              MOZ_WEBAUTHN_PUBLIC_KEY_CREDENTIAL_TYPE_PUBLIC_KEY)) {
    204        continue;
    205      }
    206 
    207      coseAlgos.AppendElement(aOptions.mPubKeyCredParams[a].mAlg);
    208    }
    209  }
    210 
    211  // If there are algorithms specified, but none are Public_key algorithms,
    212  // reject the promise.
    213  if (coseAlgos.IsEmpty() && !aOptions.mPubKeyCredParams.IsEmpty()) {
    214    promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
    215    return promise.forget();
    216  }
    217 
    218  // If excludeList is undefined, set it to the empty list.
    219  //
    220  // If extensions was specified, process any extensions supported by this
    221  // client platform, to produce the extension data that needs to be sent to the
    222  // authenticator. If an error is encountered while processing an extension,
    223  // skip that extension and do not produce any extension data for it. Call the
    224  // result of this processing clientExtensions.
    225  //
    226  // Currently no extensions are supported
    227 
    228  CryptoBuffer challenge;
    229  if (!challenge.Assign(aOptions.mChallenge)) {
    230    promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    231    return promise.forget();
    232  }
    233 
    234  nsTArray<WebAuthnScopedCredential> excludeList;
    235  for (const auto& s : aOptions.mExcludeCredentials) {
    236    WebAuthnScopedCredential c;
    237    CryptoBuffer cb;
    238    cb.Assign(s.mId);
    239    c.id() = cb;
    240    if (s.mTransports.WasPassed()) {
    241      c.transports() = SerializeTransports(s.mTransports.Value());
    242    }
    243    excludeList.AppendElement(c);
    244  }
    245 
    246  // TODO: Add extension list building
    247  nsTArray<WebAuthnExtension> extensions;
    248 
    249  // <https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#sctn-hmac-secret-extension>
    250  if (aOptions.mExtensions.mHmacCreateSecret.WasPassed()) {
    251    bool hmacCreateSecret = aOptions.mExtensions.mHmacCreateSecret.Value();
    252    if (hmacCreateSecret) {
    253      extensions.AppendElement(WebAuthnExtensionHmacSecret(hmacCreateSecret));
    254    }
    255  }
    256 
    257  if (aOptions.mExtensions.mCredentialProtectionPolicy.WasPassed()) {
    258    bool enforceCredProtect = false;
    259    if (aOptions.mExtensions.mEnforceCredentialProtectionPolicy.WasPassed()) {
    260      enforceCredProtect =
    261          aOptions.mExtensions.mEnforceCredentialProtectionPolicy.Value();
    262    }
    263    extensions.AppendElement(WebAuthnExtensionCredProtect(
    264        aOptions.mExtensions.mCredentialProtectionPolicy.Value(),
    265        enforceCredProtect));
    266  }
    267 
    268  if (aOptions.mExtensions.mCredProps.WasPassed()) {
    269    bool credProps = aOptions.mExtensions.mCredProps.Value();
    270    if (credProps) {
    271      extensions.AppendElement(WebAuthnExtensionCredProps(credProps));
    272    }
    273  }
    274 
    275  if (aOptions.mExtensions.mMinPinLength.WasPassed()) {
    276    bool minPinLength = aOptions.mExtensions.mMinPinLength.Value();
    277    if (minPinLength) {
    278      extensions.AppendElement(WebAuthnExtensionMinPinLength(minPinLength));
    279    }
    280  }
    281 
    282  // <https://w3c.github.io/webauthn/#sctn-large-blob-extension>
    283  if (aOptions.mExtensions.mLargeBlob.WasPassed()) {
    284    if (aOptions.mExtensions.mLargeBlob.Value().mRead.WasPassed() ||
    285        aOptions.mExtensions.mLargeBlob.Value().mWrite.WasPassed()) {
    286      promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
    287      return promise.forget();
    288    }
    289    Maybe<bool> supportRequired;
    290    const Optional<nsString>& largeBlobSupport =
    291        aOptions.mExtensions.mLargeBlob.Value().mSupport;
    292    if (largeBlobSupport.WasPassed()) {
    293      supportRequired.emplace(largeBlobSupport.Value().Equals(u"required"_ns));
    294    }
    295    nsTArray<uint8_t> write;  // unused
    296    extensions.AppendElement(
    297        WebAuthnExtensionLargeBlob(supportRequired, write));
    298  }
    299 
    300  // <https://w3c.github.io/webauthn/#prf-extension>
    301  if (aOptions.mExtensions.mPrf.WasPassed()) {
    302    const AuthenticationExtensionsPRFInputs& prf =
    303        aOptions.mExtensions.mPrf.Value();
    304 
    305    Maybe<WebAuthnExtensionPrfValues> eval = Nothing();
    306    if (prf.mEval.WasPassed()) {
    307      CryptoBuffer first;
    308      first.Assign(prf.mEval.Value().mFirst);
    309      const bool secondMaybe = prf.mEval.Value().mSecond.WasPassed();
    310      CryptoBuffer second;
    311      if (secondMaybe) {
    312        second.Assign(prf.mEval.Value().mSecond.Value());
    313      }
    314      eval = Some(WebAuthnExtensionPrfValues(first, secondMaybe, second));
    315    }
    316 
    317    const bool evalByCredentialMaybe = prf.mEvalByCredential.WasPassed();
    318    nsTArray<WebAuthnExtensionPrfEvalByCredentialEntry> evalByCredential;
    319    if (evalByCredentialMaybe) {
    320      // evalByCredential is only allowed in GetAssertion.
    321      // https://w3c.github.io/webauthn/#prf-extension
    322      promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
    323      return promise.forget();
    324    }
    325 
    326    extensions.AppendElement(
    327        WebAuthnExtensionPrf(eval, evalByCredentialMaybe, evalByCredential));
    328  }
    329 
    330  const auto& selection = aOptions.mAuthenticatorSelection;
    331  const auto& attachment = selection.mAuthenticatorAttachment;
    332  const nsString& attestation = aOptions.mAttestation;
    333 
    334  // Attachment
    335  Maybe<nsString> authenticatorAttachment;
    336  if (attachment.WasPassed()) {
    337    authenticatorAttachment.emplace(attachment.Value());
    338  }
    339 
    340  // The residentKey field was added in WebAuthn level 2. It takes precedent
    341  // over the requireResidentKey field if and only if it is present and it is a
    342  // member of the ResidentKeyRequirement enum.
    343  static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 3);
    344  bool useResidentKeyValue =
    345      selection.mResidentKey.WasPassed() &&
    346      (selection.mResidentKey.Value().EqualsLiteral(
    347           MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_REQUIRED) ||
    348       selection.mResidentKey.Value().EqualsLiteral(
    349           MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_PREFERRED) ||
    350       selection.mResidentKey.Value().EqualsLiteral(
    351           MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_DISCOURAGED));
    352 
    353  nsString residentKey;
    354  if (useResidentKeyValue) {
    355    residentKey = selection.mResidentKey.Value();
    356  } else {
    357    // "If no value is given then the effective value is required if
    358    // requireResidentKey is true or discouraged if it is false or absent."
    359    if (selection.mRequireResidentKey) {
    360      residentKey.AssignLiteral(MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_REQUIRED);
    361    } else {
    362      residentKey.AssignLiteral(
    363          MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_DISCOURAGED);
    364    }
    365  }
    366 
    367  // Create and forward authenticator selection criteria.
    368  WebAuthnAuthenticatorSelection authSelection(
    369      residentKey, selection.mUserVerification, authenticatorAttachment);
    370 
    371  WebAuthnMakeCredentialRpInfo rpInfo(aOptions.mRp.mName);
    372 
    373  WebAuthnMakeCredentialUserInfo userInfo(userId, aOptions.mUser.mName,
    374                                          aOptions.mUser.mDisplayName);
    375 
    376  // Abort the request if aborted flag is already set.
    377  if (aSignal.WasPassed() && aSignal.Value().Aborted()) {
    378    AutoJSAPI jsapi;
    379    if (!jsapi.Init(global)) {
    380      promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
    381      return promise.forget();
    382    }
    383    JSContext* cx = jsapi.cx();
    384    JS::Rooted<JS::Value> reason(cx);
    385    aSignal.Value().GetReason(cx, &reason);
    386    promise->MaybeReject(reason);
    387    return promise.forget();
    388  }
    389 
    390  WebAuthnMakeCredentialInfo info(rpId, challenge, adjustedTimeout, excludeList,
    391                                  rpInfo, userInfo, coseAlgos, extensions,
    392                                  authSelection, attestation, aOptions.mHints);
    393 
    394  // Set up the transaction state. Fallible operations should not be performed
    395  // below this line, as we must not leave the transaction state partially
    396  // initialized. Once the transaction state is initialized the only valid ways
    397  // to end the transaction are CancelTransaction, RejectTransaction, and
    398  // FinishMakeCredential.
    399  AbortSignal* signal = nullptr;
    400  if (aSignal.WasPassed()) {
    401    signal = &aSignal.Value();
    402    Follow(signal);
    403  }
    404 
    405  MOZ_ASSERT(mTransaction.isNothing());
    406  mTransaction =
    407      Some(WebAuthnTransaction(promise, WebAuthnTransactionType::Create));
    408  mActor->SendRequestRegister(info)
    409      ->Then(
    410          GetCurrentSerialEventTarget(), __func__,
    411          [self = RefPtr{this}](
    412              const PWebAuthnTransactionChild::RequestRegisterPromise::
    413                  ResolveOrRejectValue& aValue) {
    414            self->mTransaction.ref().mRegisterHolder.Complete();
    415            if (aValue.IsResolve() && aValue.ResolveValue().type() ==
    416                                          WebAuthnMakeCredentialResponse::Type::
    417                                              TWebAuthnMakeCredentialResult) {
    418              self->FinishMakeCredential(aValue.ResolveValue());
    419            } else if (aValue.IsResolve()) {
    420              self->RejectTransaction(aValue.ResolveValue());
    421            } else {
    422              self->RejectTransaction(NS_ERROR_DOM_NOT_ALLOWED_ERR);
    423            }
    424          })
    425      ->Track(mTransaction.ref().mRegisterHolder);
    426 
    427  return promise.forget();
    428 }
    429 
    430 const size_t MAX_ALLOWED_CREDENTIALS = 20;
    431 
    432 already_AddRefed<Promise> WebAuthnHandler::GetAssertion(
    433    const PublicKeyCredentialRequestOptions& aOptions,
    434    const bool aConditionallyMediated,
    435    const Optional<OwningNonNull<AbortSignal>>& aSignal, ErrorResult& aError) {
    436  MOZ_ASSERT(NS_IsMainThread());
    437 
    438  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mWindow);
    439 
    440  RefPtr<Promise> promise = Promise::Create(global, aError);
    441  if (aError.Failed()) {
    442    return nullptr;
    443  }
    444 
    445  if (mTransaction.isSome()) {
    446    // abort the old transaction and take over control from here.
    447    CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
    448  }
    449 
    450  if (!MaybeCreateActor()) {
    451    promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
    452    return promise.forget();
    453  }
    454 
    455  nsCOMPtr<Document> doc = mWindow->GetDoc();
    456  if (!IsWebAuthnAllowedInDocument(doc)) {
    457    promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    458    return promise.forget();
    459  }
    460 
    461  nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
    462 
    463  nsCString rpId;
    464  if (aOptions.mRpId.WasPassed()) {
    465    rpId = NS_ConvertUTF16toUTF8(aOptions.mRpId.Value());
    466  } else {
    467    nsresult rv = DefaultRpId(principal, rpId);
    468    if (NS_FAILED(rv)) {
    469      promise->MaybeReject(NS_ERROR_FAILURE);
    470      return promise.forget();
    471    }
    472  }
    473  if (!IsValidRpId(principal, rpId)) {
    474    promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    475    return promise.forget();
    476  }
    477 
    478  // If timeoutSeconds was specified, check if its value lies within a
    479  // reasonable range as defined by the platform and if not, correct it to the
    480  // closest value lying within that range.
    481 
    482  uint32_t adjustedTimeout = 30000;
    483  if (aOptions.mTimeout.WasPassed()) {
    484    adjustedTimeout = aOptions.mTimeout.Value();
    485    adjustedTimeout = std::max(15000u, adjustedTimeout);
    486    adjustedTimeout = std::min(120000u, adjustedTimeout);
    487  }
    488 
    489  // Abort the request if the allowCredentials set is too large
    490  if (aOptions.mAllowCredentials.Length() > MAX_ALLOWED_CREDENTIALS) {
    491    promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    492    return promise.forget();
    493  }
    494 
    495  CryptoBuffer challenge;
    496  if (!challenge.Assign(aOptions.mChallenge)) {
    497    promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    498    return promise.forget();
    499  }
    500 
    501  nsTArray<WebAuthnScopedCredential> allowList;
    502  for (const auto& s : aOptions.mAllowCredentials) {
    503    if (s.mType.EqualsLiteral(
    504            MOZ_WEBAUTHN_PUBLIC_KEY_CREDENTIAL_TYPE_PUBLIC_KEY)) {
    505      WebAuthnScopedCredential c;
    506      CryptoBuffer cb;
    507      cb.Assign(s.mId);
    508      c.id() = cb;
    509      if (s.mTransports.WasPassed()) {
    510        c.transports() = SerializeTransports(s.mTransports.Value());
    511      }
    512      allowList.AppendElement(c);
    513    }
    514  }
    515  if (allowList.Length() == 0 && aOptions.mAllowCredentials.Length() != 0) {
    516    promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
    517    return promise.forget();
    518  }
    519 
    520  // If extensions were specified, process any extensions supported by this
    521  // client platform, to produce the extension data that needs to be sent to the
    522  // authenticator. If an error is encountered while processing an extension,
    523  // skip that extension and do not produce any extension data for it. Call the
    524  // result of this processing clientExtensions.
    525  nsTArray<WebAuthnExtension> extensions;
    526 
    527  // credProps is only supported in MakeCredentials
    528  if (aOptions.mExtensions.mCredProps.WasPassed()) {
    529    promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
    530    return promise.forget();
    531  }
    532 
    533  // minPinLength is only supported in MakeCredentials
    534  if (aOptions.mExtensions.mMinPinLength.WasPassed()) {
    535    promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
    536    return promise.forget();
    537  }
    538 
    539  // <https://w3c.github.io/webauthn/#sctn-appid-extension>
    540  Maybe<nsCString> maybeAppId;
    541  if (aOptions.mExtensions.mAppid.WasPassed()) {
    542    nsCString appId(NS_ConvertUTF16toUTF8(aOptions.mExtensions.mAppid.Value()));
    543 
    544    // Step 2 of Algorithm 3.1.2 of
    545    // https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-appid-and-facets-v2.0-id-20180227.html#determining-if-a-caller-s-facetid-is-authorized-for-an-appid
    546    if (appId.IsEmpty() || appId.EqualsLiteral("null")) {
    547      auto* basePrin = BasePrincipal::Cast(principal);
    548      nsresult rv = basePrin->GetWebExposedOriginSerialization(appId);
    549      if (NS_FAILED(rv)) {
    550        promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    551        return promise.forget();
    552      }
    553    }
    554 
    555    maybeAppId.emplace(std::move(appId));
    556  }
    557 
    558  // <https://w3c.github.io/webauthn/#sctn-large-blob-extension>
    559  if (aOptions.mExtensions.mLargeBlob.WasPassed()) {
    560    const AuthenticationExtensionsLargeBlobInputs& extLargeBlob =
    561        aOptions.mExtensions.mLargeBlob.Value();
    562    if (extLargeBlob.mSupport.WasPassed() ||
    563        (extLargeBlob.mRead.WasPassed() && extLargeBlob.mWrite.WasPassed()) ||
    564        (extLargeBlob.mWrite.WasPassed() &&
    565         aOptions.mAllowCredentials.Length() != 1)) {
    566      promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
    567      return promise.forget();
    568    }
    569    Maybe<bool> read = Nothing();
    570    if (extLargeBlob.mRead.WasPassed() && extLargeBlob.mRead.Value()) {
    571      read.emplace(true);
    572    }
    573 
    574    CryptoBuffer write;
    575    if (extLargeBlob.mWrite.WasPassed()) {
    576      read.emplace(false);
    577      write.Assign(extLargeBlob.mWrite.Value());
    578    }
    579    extensions.AppendElement(WebAuthnExtensionLargeBlob(read, write));
    580  }
    581 
    582  // <https://w3c.github.io/webauthn/#prf-extension>
    583  if (aOptions.mExtensions.mPrf.WasPassed()) {
    584    const AuthenticationExtensionsPRFInputs& prf =
    585        aOptions.mExtensions.mPrf.Value();
    586 
    587    Maybe<WebAuthnExtensionPrfValues> eval = Nothing();
    588    if (prf.mEval.WasPassed()) {
    589      CryptoBuffer first;
    590      first.Assign(prf.mEval.Value().mFirst);
    591      const bool secondMaybe = prf.mEval.Value().mSecond.WasPassed();
    592      CryptoBuffer second;
    593      if (secondMaybe) {
    594        second.Assign(prf.mEval.Value().mSecond.Value());
    595      }
    596      eval = Some(WebAuthnExtensionPrfValues(first, secondMaybe, second));
    597    }
    598 
    599    const bool evalByCredentialMaybe = prf.mEvalByCredential.WasPassed();
    600    nsTArray<WebAuthnExtensionPrfEvalByCredentialEntry> evalByCredential;
    601    if (evalByCredentialMaybe) {
    602      if (allowList.Length() == 0) {
    603        promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
    604        return promise.forget();
    605      }
    606 
    607      for (const auto& entry : prf.mEvalByCredential.Value().Entries()) {
    608        FallibleTArray<uint8_t> evalByCredentialEntryId;
    609        nsresult rv = Base64URLDecode(NS_ConvertUTF16toUTF8(entry.mKey),
    610                                      Base64URLDecodePaddingPolicy::Ignore,
    611                                      evalByCredentialEntryId);
    612        if (NS_FAILED(rv)) {
    613          promise->MaybeReject(NS_ERROR_DOM_SYNTAX_ERR);
    614          return promise.forget();
    615        }
    616 
    617        bool foundMatchingAllowListEntry = false;
    618        for (const auto& cred : allowList) {
    619          if (evalByCredentialEntryId == cred.id()) {
    620            foundMatchingAllowListEntry = true;
    621          }
    622        }
    623        if (!foundMatchingAllowListEntry) {
    624          promise->MaybeReject(NS_ERROR_DOM_SYNTAX_ERR);
    625          return promise.forget();
    626        }
    627 
    628        CryptoBuffer first;
    629        first.Assign(entry.mValue.mFirst);
    630        const bool secondMaybe = entry.mValue.mSecond.WasPassed();
    631        CryptoBuffer second;
    632        if (secondMaybe) {
    633          second.Assign(entry.mValue.mSecond.Value());
    634        }
    635        evalByCredential.AppendElement(
    636            WebAuthnExtensionPrfEvalByCredentialEntry(
    637                evalByCredentialEntryId,
    638                WebAuthnExtensionPrfValues(first, secondMaybe, second)));
    639      }
    640    }
    641 
    642    extensions.AppendElement(
    643        WebAuthnExtensionPrf(eval, evalByCredentialMaybe, evalByCredential));
    644  }
    645 
    646  // Abort the request if aborted flag is already set.
    647  if (aSignal.WasPassed() && aSignal.Value().Aborted()) {
    648    AutoJSAPI jsapi;
    649    if (!jsapi.Init(global)) {
    650      promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
    651      return promise.forget();
    652    }
    653    JSContext* cx = jsapi.cx();
    654    JS::Rooted<JS::Value> reason(cx);
    655    aSignal.Value().GetReason(cx, &reason);
    656    promise->MaybeReject(reason);
    657    return promise.forget();
    658  }
    659 
    660  WebAuthnGetAssertionInfo info(
    661      rpId, maybeAppId, challenge, adjustedTimeout, allowList, extensions,
    662      aOptions.mUserVerification, aConditionallyMediated, aOptions.mHints);
    663 
    664  // Set up the transaction state. Fallible operations should not be performed
    665  // below this line, as we must not leave the transaction state partially
    666  // initialized. Once the transaction state is initialized the only valid ways
    667  // to end the transaction are CancelTransaction, RejectTransaction, and
    668  // FinishGetAssertion.
    669  AbortSignal* signal = nullptr;
    670  if (aSignal.WasPassed()) {
    671    signal = &aSignal.Value();
    672    Follow(signal);
    673  }
    674 
    675  MOZ_ASSERT(mTransaction.isNothing());
    676  mTransaction =
    677      Some(WebAuthnTransaction(promise, WebAuthnTransactionType::Get));
    678  mActor->SendRequestSign(info)
    679      ->Then(
    680          GetCurrentSerialEventTarget(), __func__,
    681          [self = RefPtr{this}](
    682              const PWebAuthnTransactionChild::RequestSignPromise::
    683                  ResolveOrRejectValue& aValue) {
    684            self->mTransaction.ref().mSignHolder.Complete();
    685            if (aValue.IsResolve() && aValue.ResolveValue().type() ==
    686                                          WebAuthnGetAssertionResponse::Type::
    687                                              TWebAuthnGetAssertionResult) {
    688              self->FinishGetAssertion(aValue.ResolveValue());
    689            } else if (aValue.IsResolve()) {
    690              self->RejectTransaction(aValue.ResolveValue());
    691            } else {
    692              self->RejectTransaction(NS_ERROR_DOM_NOT_ALLOWED_ERR);
    693            }
    694          })
    695      ->Track(mTransaction.ref().mSignHolder);
    696 
    697  return promise.forget();
    698 }
    699 
    700 already_AddRefed<Promise> WebAuthnHandler::Store(const Credential& aCredential,
    701                                                 ErrorResult& aError) {
    702  MOZ_ASSERT(NS_IsMainThread());
    703 
    704  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mWindow);
    705 
    706  RefPtr<Promise> promise = Promise::Create(global, aError);
    707  if (aError.Failed()) {
    708    return nullptr;
    709  }
    710 
    711  if (mTransaction.isSome()) {
    712    // abort the old transaction and take over control from here.
    713    CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
    714  }
    715 
    716  promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
    717  return promise.forget();
    718 }
    719 
    720 already_AddRefed<Promise> WebAuthnHandler::IsUVPAA(GlobalObject& aGlobal,
    721                                                   ErrorResult& aError) {
    722  RefPtr<Promise> promise =
    723      Promise::Create(xpc::CurrentNativeGlobal(aGlobal.Context()), aError);
    724  if (aError.Failed()) {
    725    return nullptr;
    726  }
    727 
    728  if (!MaybeCreateActor()) {
    729    promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
    730    return promise.forget();
    731  }
    732 
    733  mActor->SendRequestIsUVPAA()->Then(
    734      GetCurrentSerialEventTarget(), __func__,
    735      [promise](const PWebAuthnTransactionChild::RequestIsUVPAAPromise::
    736                    ResolveOrRejectValue& aValue) {
    737        if (aValue.IsResolve()) {
    738          promise->MaybeResolve(aValue.ResolveValue());
    739        } else {
    740          promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
    741        }
    742      });
    743  return promise.forget();
    744 }
    745 
    746 void WebAuthnHandler::FinishMakeCredential(
    747    const WebAuthnMakeCredentialResult& aResult) {
    748  MOZ_ASSERT(NS_IsMainThread());
    749  MOZ_ASSERT(mTransaction.isSome());
    750 
    751  nsAutoCString keyHandleBase64Url;
    752  nsresult rv = Base64URLEncode(
    753      aResult.KeyHandle().Length(), aResult.KeyHandle().Elements(),
    754      Base64URLEncodePaddingPolicy::Omit, keyHandleBase64Url);
    755  if (NS_WARN_IF(NS_FAILED(rv))) {
    756    RejectTransaction(rv);
    757    return;
    758  }
    759 
    760  // Create a new PublicKeyCredential object and populate its fields with the
    761  // values returned from the authenticator as well as the clientDataJSON
    762  // computed earlier.
    763  RefPtr<AuthenticatorAttestationResponse> attestation =
    764      new AuthenticatorAttestationResponse(mWindow);
    765  attestation->SetClientDataJSON(aResult.ClientDataJSON());
    766  attestation->SetAttestationObject(aResult.AttestationObject());
    767  attestation->SetTransports(aResult.Transports());
    768 
    769  RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mWindow);
    770  credential->SetId(NS_ConvertASCIItoUTF16(keyHandleBase64Url));
    771  credential->SetType(u"public-key"_ns);
    772  credential->SetRawId(aResult.KeyHandle());
    773  credential->SetAttestationResponse(attestation);
    774 
    775  if (aResult.AuthenticatorAttachment().isSome()) {
    776    credential->SetAuthenticatorAttachment(aResult.AuthenticatorAttachment());
    777 
    778    mozilla::glean::webauthn_create::authenticator_attachment
    779        .Get(NS_ConvertUTF16toUTF8(aResult.AuthenticatorAttachment().ref()))
    780        .Add(1);
    781  } else {
    782    mozilla::glean::webauthn_get::authenticator_attachment.Get("unknown"_ns)
    783        .Add(1);
    784  }
    785 
    786  // Forward client extension results.
    787  for (const auto& ext : aResult.Extensions()) {
    788    if (ext.type() ==
    789        WebAuthnExtensionResult::TWebAuthnExtensionResultCredProps) {
    790      bool credPropsRk = ext.get_WebAuthnExtensionResultCredProps().rk();
    791      credential->SetClientExtensionResultCredPropsRk(credPropsRk);
    792      if (credPropsRk) {
    793        mozilla::glean::webauthn_create::passkey.Add(1);
    794      }
    795    }
    796    if (ext.type() ==
    797        WebAuthnExtensionResult::TWebAuthnExtensionResultHmacSecret) {
    798      bool hmacCreateSecret =
    799          ext.get_WebAuthnExtensionResultHmacSecret().hmacCreateSecret();
    800      credential->SetClientExtensionResultHmacSecret(hmacCreateSecret);
    801    }
    802    if (ext.type() ==
    803        WebAuthnExtensionResult::TWebAuthnExtensionResultLargeBlob) {
    804      credential->InitClientExtensionResultLargeBlob();
    805      credential->SetClientExtensionResultLargeBlobSupported(
    806          ext.get_WebAuthnExtensionResultLargeBlob().flag());
    807    }
    808    if (ext.type() == WebAuthnExtensionResult::TWebAuthnExtensionResultPrf) {
    809      credential->InitClientExtensionResultPrf();
    810      const Maybe<bool> prfEnabled =
    811          ext.get_WebAuthnExtensionResultPrf().enabled();
    812      if (prfEnabled.isSome()) {
    813        credential->SetClientExtensionResultPrfEnabled(prfEnabled.value());
    814      }
    815      const Maybe<WebAuthnExtensionPrfValues> prfValues =
    816          ext.get_WebAuthnExtensionResultPrf().results();
    817      if (prfValues.isSome()) {
    818        credential->SetClientExtensionResultPrfResultsFirst(
    819            prfValues.value().first());
    820        if (prfValues.value().secondMaybe()) {
    821          credential->SetClientExtensionResultPrfResultsSecond(
    822              prfValues.value().second());
    823        }
    824      }
    825    }
    826  }
    827 
    828  ResolveTransaction(credential);
    829 }
    830 
    831 void WebAuthnHandler::FinishGetAssertion(
    832    const WebAuthnGetAssertionResult& aResult) {
    833  MOZ_ASSERT(NS_IsMainThread());
    834  MOZ_ASSERT(mTransaction.isSome());
    835 
    836  nsAutoCString keyHandleBase64Url;
    837  nsresult rv = Base64URLEncode(
    838      aResult.KeyHandle().Length(), aResult.KeyHandle().Elements(),
    839      Base64URLEncodePaddingPolicy::Omit, keyHandleBase64Url);
    840  if (NS_WARN_IF(NS_FAILED(rv))) {
    841    RejectTransaction(rv);
    842    return;
    843  }
    844 
    845  // Create a new PublicKeyCredential object named value and populate its fields
    846  // with the values returned from the authenticator as well as the
    847  // clientDataJSON computed earlier.
    848  RefPtr<AuthenticatorAssertionResponse> assertion =
    849      new AuthenticatorAssertionResponse(mWindow);
    850  assertion->SetClientDataJSON(aResult.ClientDataJSON());
    851  assertion->SetAuthenticatorData(aResult.AuthenticatorData());
    852  assertion->SetSignature(aResult.Signature());
    853  assertion->SetUserHandle(aResult.UserHandle());  // may be empty
    854 
    855  RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mWindow);
    856  credential->SetId(NS_ConvertASCIItoUTF16(keyHandleBase64Url));
    857  credential->SetType(u"public-key"_ns);
    858  credential->SetRawId(aResult.KeyHandle());
    859  credential->SetAssertionResponse(assertion);
    860 
    861  if (aResult.AuthenticatorAttachment().isSome()) {
    862    credential->SetAuthenticatorAttachment(aResult.AuthenticatorAttachment());
    863 
    864    mozilla::glean::webauthn_get::authenticator_attachment
    865        .Get(NS_ConvertUTF16toUTF8(aResult.AuthenticatorAttachment().ref()))
    866        .Add(1);
    867  } else {
    868    mozilla::glean::webauthn_get::authenticator_attachment.Get("unknown"_ns)
    869        .Add(1);
    870  }
    871 
    872  // Forward client extension results.
    873  for (const auto& ext : aResult.Extensions()) {
    874    if (ext.type() == WebAuthnExtensionResult::TWebAuthnExtensionResultAppId) {
    875      bool appid = ext.get_WebAuthnExtensionResultAppId().AppId();
    876      credential->SetClientExtensionResultAppId(appid);
    877    }
    878    if (ext.type() ==
    879        WebAuthnExtensionResult::TWebAuthnExtensionResultLargeBlob) {
    880      if (ext.get_WebAuthnExtensionResultLargeBlob().flag() &&
    881          ext.get_WebAuthnExtensionResultLargeBlob().written()) {
    882        // Signal a read failure by including an empty largeBlob extension.
    883        credential->InitClientExtensionResultLargeBlob();
    884      } else if (ext.get_WebAuthnExtensionResultLargeBlob().flag()) {
    885        const nsTArray<uint8_t>& largeBlobValue =
    886            ext.get_WebAuthnExtensionResultLargeBlob().blob();
    887        credential->InitClientExtensionResultLargeBlob();
    888        credential->SetClientExtensionResultLargeBlobValue(largeBlobValue);
    889      } else {
    890        bool largeBlobWritten =
    891            ext.get_WebAuthnExtensionResultLargeBlob().written();
    892        credential->InitClientExtensionResultLargeBlob();
    893        credential->SetClientExtensionResultLargeBlobWritten(largeBlobWritten);
    894      }
    895    }
    896    if (ext.type() == WebAuthnExtensionResult::TWebAuthnExtensionResultPrf) {
    897      credential->InitClientExtensionResultPrf();
    898      Maybe<WebAuthnExtensionPrfValues> prfResults =
    899          ext.get_WebAuthnExtensionResultPrf().results();
    900      if (prfResults.isSome()) {
    901        credential->SetClientExtensionResultPrfResultsFirst(
    902            prfResults.value().first());
    903        if (prfResults.value().secondMaybe()) {
    904          credential->SetClientExtensionResultPrfResultsSecond(
    905              prfResults.value().second());
    906        }
    907      }
    908    }
    909  }
    910 
    911  // Treat successful assertion as user activation for BounceTrackingProtection.
    912  nsIGlobalObject* global = mTransaction.ref().mPromise->GetGlobalObject();
    913  if (global) {
    914    nsPIDOMWindowInner* window = global->GetAsInnerWindow();
    915    if (window) {
    916      (void)BounceTrackingProtection::RecordUserActivation(
    917          window->GetWindowContext());
    918    }
    919  }
    920 
    921  ResolveTransaction(credential);
    922 }
    923 
    924 void WebAuthnHandler::RunAbortAlgorithm() {
    925  if (NS_WARN_IF(mTransaction.isNothing())) {
    926    return;
    927  }
    928 
    929  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mWindow);
    930 
    931  AutoJSAPI jsapi;
    932  if (!jsapi.Init(global)) {
    933    CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
    934    return;
    935  }
    936  JSContext* cx = jsapi.cx();
    937  JS::Rooted<JS::Value> reason(cx);
    938  Signal()->GetReason(cx, &reason);
    939  CancelTransaction(reason);
    940 }
    941 
    942 void WebAuthnHandler::ResolveTransaction(
    943    const RefPtr<PublicKeyCredential>& aCredential) {
    944  MOZ_ASSERT(mTransaction.isSome());
    945 
    946  switch (mTransaction.ref().mType) {
    947    case WebAuthnTransactionType::Create:
    948      mozilla::glean::webauthn_create::success.Add(1);
    949      break;
    950    case WebAuthnTransactionType::Get:
    951      mozilla::glean::webauthn_get::success.Add(1);
    952      break;
    953  }
    954 
    955  // Bug 1969341 - we need to reset the transaction before resolving the
    956  // promise. This lets us handle the case where resolving the promise initiates
    957  // a new WebAuthn request.
    958  RefPtr<Promise> promise = mTransaction.ref().mPromise;
    959  mTransaction.reset();
    960  Unfollow();
    961 
    962  promise->MaybeResolve(aCredential);
    963 }
    964 
    965 template <typename T>
    966 void WebAuthnHandler::RejectTransaction(const T& aReason) {
    967  MOZ_ASSERT(mTransaction.isSome());
    968 
    969  switch (mTransaction.ref().mType) {
    970    case WebAuthnTransactionType::Create:
    971      mozilla::glean::webauthn_create::failure.Add(1);
    972      break;
    973    case WebAuthnTransactionType::Get:
    974      mozilla::glean::webauthn_get::failure.Add(1);
    975      break;
    976  }
    977 
    978  // Bug 1969341 - we need to reset the transaction before rejecting the
    979  // promise. This lets us handle the case where rejecting the promise initiates
    980  // a new WebAuthn request.
    981  RefPtr<Promise> promise = mTransaction.ref().mPromise;
    982  mTransaction.reset();
    983  Unfollow();
    984 
    985  promise->MaybeReject(aReason);
    986 }
    987 
    988 }  // namespace mozilla::dom