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