tor-browser

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

WebAuthnUtil.cpp (9222B)


      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/WebAuthnUtil.h"
      8 
      9 #include "hasht.h"
     10 #include "mozilla/BasePrincipal.h"
     11 #include "mozilla/StaticPrefs_security.h"
     12 #include "mozilla/dom/WindowGlobalParent.h"
     13 #include "mozpkix/pkixutil.h"
     14 #include "nsComponentManagerUtils.h"
     15 #include "nsHTMLDocument.h"
     16 #include "nsICryptoHash.h"
     17 #include "nsIEffectiveTLDService.h"
     18 #include "nsIURIMutator.h"
     19 #include "nsNetUtil.h"
     20 
     21 namespace mozilla::dom {
     22 
     23 bool IsValidAppId(const nsCOMPtr<nsIPrincipal>& aPrincipal,
     24                  const nsCString& aAppId) {
     25  // An AppID is a substitute for the RP ID that allows the caller to assert
     26  // credentials that were created using the legacy U2F protocol. While an RP ID
     27  // is the caller origin's effective domain, or a registrable suffix thereof,
     28  // an AppID is a URL (with a scheme and a possibly non-empty path) that is
     29  // same-site with the caller's origin.
     30  //
     31  // The U2F protocol nominally uses Algorithm 3.1.2 of [1] to validate AppIDs.
     32  // However, the WebAuthn spec [2] notes that it is not necessary to "implement
     33  // steps four and onward of" Algorithm 3.1.2. Instead, in step three, "the
     34  // comparison on the host is relaxed to accept hosts on the same site." Step
     35  // two is best seen as providing a default value for the AppId when one is not
     36  // provided. That leaves step 1 and the same-site check, which is what we
     37  // implement here.
     38  //
     39  // [1]
     40  // 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
     41  // [2] https://w3c.github.io/webauthn/#sctn-appid-extension
     42 
     43  auto* principal = BasePrincipal::Cast(aPrincipal);
     44  nsCOMPtr<nsIURI> callerUri;
     45  nsresult rv = principal->GetURI(getter_AddRefs(callerUri));
     46  if (NS_FAILED(rv)) {
     47    return false;
     48  }
     49 
     50  nsCOMPtr<nsIURI> appIdUri;
     51  rv = NS_NewURI(getter_AddRefs(appIdUri), aAppId);
     52  if (NS_FAILED(rv)) {
     53    return false;
     54  }
     55 
     56  // Step 1 of Algorithm 3.1.2. "If the AppID is not an HTTPS URL, and matches
     57  // the FacetID of the caller, no additional processing is necessary and the
     58  // operation may proceed." In the web context, the "FacetID" is defined as
     59  // "the Web Origin [RFC6454] of the web page triggering the FIDO operation,
     60  // written as a URI with an empty path. Default ports are omitted and any path
     61  // component is ignored."
     62  if (!appIdUri->SchemeIs("https")) {
     63    nsCString facetId;
     64    rv = principal->GetWebExposedOriginSerialization(facetId);
     65    return NS_SUCCEEDED(rv) && facetId == aAppId;
     66  }
     67 
     68  // Same site check
     69  nsCOMPtr<nsIEffectiveTLDService> tldService =
     70      do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
     71  if (!tldService) {
     72    return false;
     73  }
     74 
     75  nsAutoCString baseDomainCaller;
     76  rv = tldService->GetBaseDomain(callerUri, 0, baseDomainCaller);
     77  if (NS_FAILED(rv)) {
     78    return false;
     79  }
     80 
     81  nsAutoCString baseDomainAppId;
     82  rv = tldService->GetBaseDomain(appIdUri, 0, baseDomainAppId);
     83  if (NS_FAILED(rv)) {
     84    return false;
     85  }
     86 
     87  if (baseDomainCaller == baseDomainAppId) {
     88    return true;
     89  }
     90 
     91  // Exceptions for Google Accounts from Bug 1436078. These were supposed to be
     92  // temporary, but users reported breakage when we tried to remove them (Bug
     93  // 1822703). We will need to keep them indefinitely.
     94  if (baseDomainCaller.EqualsLiteral("google.com") &&
     95      (aAppId.Equals("https://www.gstatic.com/securitykey/origins.json"_ns) ||
     96       aAppId.Equals(
     97           "https://www.gstatic.com/securitykey/a/google.com/origins.json"_ns))) {
     98    return true;
     99  }
    100 
    101  return false;
    102 }
    103 
    104 nsresult DefaultRpId(const nsCOMPtr<nsIPrincipal>& aPrincipal,
    105                     /* out */ nsACString& aRpId) {
    106  // [https://w3c.github.io/webauthn/#rp-id]
    107  // "By default, the RP ID for a WebAuthn operation is set to the caller's
    108  // origin's effective domain."
    109  auto* basePrin = BasePrincipal::Cast(aPrincipal);
    110  nsCOMPtr<nsIURI> uri;
    111  if (NS_FAILED(basePrin->GetURI(getter_AddRefs(uri)))) {
    112    return NS_ERROR_FAILURE;
    113  }
    114  return uri->GetAsciiHost(aRpId);
    115 }
    116 
    117 bool IsWebAuthnAllowedInDocument(const nsCOMPtr<Document>& aDoc) {
    118  MOZ_ASSERT(aDoc);
    119  return aDoc->IsHTMLOrXHTML();
    120 }
    121 
    122 bool IsWebAuthnAllowedInContext(WindowGlobalParent* aContext) {
    123  nsIPrincipal* principal = aContext->DocumentPrincipal();
    124  MOZ_ASSERT(principal);
    125 
    126  if (principal->GetIsNullPrincipal()) {
    127    return false;
    128  }
    129 
    130  if (principal->GetIsIpAddress()) {
    131    return false;
    132  }
    133  // This next test is not strictly necessary since CredentialsContainer is
    134  // [SecureContext] in our webidl.
    135  if (!principal->GetIsOriginPotentiallyTrustworthy()) {
    136    return false;
    137  }
    138 
    139  if (principal->GetIsLoopbackHost()) {
    140    return true;
    141  }
    142 
    143  if (StaticPrefs::security_webauthn_allow_with_certificate_override()) {
    144    return true;
    145  }
    146 
    147  WindowGlobalParent* windowContext = aContext;
    148  while (windowContext) {
    149    nsITransportSecurityInfo* securityInfo = windowContext->GetSecurityInfo();
    150    if (securityInfo &&
    151        !IsWebAuthnAllowedForTransportSecurityInfo(securityInfo)) {
    152      return false;
    153    }
    154    windowContext = windowContext->GetParentWindowContext();
    155  }
    156 
    157  return true;
    158 }
    159 
    160 bool IsWebAuthnAllowedForTransportSecurityInfo(
    161    nsITransportSecurityInfo* aSecurityInfo) {
    162  nsITransportSecurityInfo::OverridableErrorCategory overridableErrorCategory;
    163  if (!aSecurityInfo || NS_FAILED(aSecurityInfo->GetOverridableErrorCategory(
    164                            &overridableErrorCategory))) {
    165    return false;
    166  }
    167 
    168  switch (overridableErrorCategory) {
    169    case nsITransportSecurityInfo::OverridableErrorCategory::ERROR_UNSET:
    170      return true;
    171    case nsITransportSecurityInfo::OverridableErrorCategory::ERROR_TIME:
    172      return true;
    173    case nsITransportSecurityInfo::OverridableErrorCategory::ERROR_TRUST:
    174      return false;
    175    case nsITransportSecurityInfo::OverridableErrorCategory::ERROR_DOMAIN:
    176      return false;
    177    default:
    178      return false;
    179  }
    180 }
    181 
    182 bool IsValidRpId(const nsCOMPtr<nsIPrincipal>& aPrincipal,
    183                 const nsACString& aRpId) {
    184  // This checks two of the conditions defined in
    185  // https://w3c.github.io/webauthn/#rp-id, namely that the RP ID value is
    186  //  (1) "a valid domain string", and
    187  //  (2) "a registrable domain suffix of or is equal to the caller's origin's
    188  //      effective domain"
    189  //
    190  // We do not check that the condition that "origin's scheme is https [, or]
    191  // the origin's host is localhost and its scheme is http". These are special
    192  // cases of secure contexts (https://www.w3.org/TR/secure-contexts/). We
    193  // expose WebAuthn in all secure contexts, which is slightly more lenient
    194  // than the spec's condition.
    195 
    196  // Condition (1)
    197  nsCString normalizedRpId;
    198  nsresult rv = NS_DomainToASCII(aRpId, normalizedRpId);
    199  if (NS_FAILED(rv)) {
    200    return false;
    201  }
    202  if (normalizedRpId != aRpId) {
    203    return false;
    204  }
    205 
    206  // Condition (2)
    207  // The "is a registrable domain suffix of or is equal to" condition is defined
    208  // in https://html.spec.whatwg.org/multipage/browsers.html#dom-document-domain
    209  // as a subroutine of the document.domain setter, and it is exposed in XUL as
    210  // the Document::IsValidDomain function. This function takes URIs as inputs
    211  // rather than domain strings, so we construct a target URI using the current
    212  // document URI as a template.
    213  auto* basePrin = BasePrincipal::Cast(aPrincipal);
    214  nsCOMPtr<nsIURI> currentURI;
    215  if (NS_FAILED(basePrin->GetURI(getter_AddRefs(currentURI)))) {
    216    return false;
    217  }
    218  nsCOMPtr<nsIURI> targetURI;
    219  rv = NS_MutateURI(currentURI).SetHost(aRpId).Finalize(targetURI);
    220  if (NS_FAILED(rv)) {
    221    return false;
    222  }
    223  return Document::IsValidDomain(currentURI, targetURI);
    224 }
    225 
    226 static nsresult HashCString(nsICryptoHash* aHashService, const nsACString& aIn,
    227                            /* out */ nsTArray<uint8_t>& aOut) {
    228  MOZ_ASSERT(aHashService);
    229 
    230  nsresult rv = aHashService->Init(nsICryptoHash::SHA256);
    231  if (NS_WARN_IF(NS_FAILED(rv))) {
    232    return rv;
    233  }
    234 
    235  rv = aHashService->Update(
    236      reinterpret_cast<const uint8_t*>(aIn.BeginReading()), aIn.Length());
    237  if (NS_WARN_IF(NS_FAILED(rv))) {
    238    return rv;
    239  }
    240 
    241  nsAutoCString fullHash;
    242  // Passing false below means we will get a binary result rather than a
    243  // base64-encoded string.
    244  rv = aHashService->Finish(false, fullHash);
    245  if (NS_WARN_IF(NS_FAILED(rv))) {
    246    return rv;
    247  }
    248 
    249  aOut.Clear();
    250  aOut.AppendElements(reinterpret_cast<uint8_t const*>(fullHash.BeginReading()),
    251                      fullHash.Length());
    252 
    253  return NS_OK;
    254 }
    255 
    256 nsresult HashCString(const nsACString& aIn, /* out */ nsTArray<uint8_t>& aOut) {
    257  nsresult srv;
    258  nsCOMPtr<nsICryptoHash> hashService =
    259      do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &srv);
    260  if (NS_FAILED(srv)) {
    261    return srv;
    262  }
    263 
    264  srv = HashCString(hashService, aIn, aOut);
    265  if (NS_WARN_IF(NS_FAILED(srv))) {
    266    return NS_ERROR_FAILURE;
    267  }
    268 
    269  return NS_OK;
    270 }
    271 
    272 }  // namespace mozilla::dom