/* 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/WebAuthnUtil.h" #include "hasht.h" #include "js/Exception.h" #include "mozilla/Base64.h" #include "mozilla/BasePrincipal.h" #include "mozilla/Components.h" #include "mozilla/StaticPrefs_security.h" #include "mozilla/dom/WebAuthenticationBinding.h" #include "mozilla/dom/WindowGlobalParent.h" #include "mozilla/dom/nsMixedContentBlocker.h" #include "mozilla/extensions/MatchPattern.h" #include "mozilla/extensions/WebExtensionPolicy.h" #include "mozilla/net/DNS.h" #include "mozpkix/pkixutil.h" #include "nsComponentManagerUtils.h" #include "nsContentUtils.h" #include "nsHTMLDocument.h" #include "nsICryptoHash.h" #include "nsIEffectiveTLDService.h" #include "nsIURIMutator.h" #include "nsNetUtil.h" namespace mozilla::dom { bool IsValidAppId(const nsCOMPtr& aPrincipal, const nsCString& aAppId) { // An AppID is a substitute for the RP ID that allows the caller to assert // credentials that were created using the legacy U2F protocol. While an RP ID // is the caller origin's effective domain, or a registrable suffix thereof, // an AppID is a URL (with a scheme and a possibly non-empty path) that is // same-site with the caller's origin. // // The U2F protocol nominally uses Algorithm 3.1.2 of [1] to validate AppIDs. // However, the WebAuthn spec [2] notes that it is not necessary to "implement // steps four and onward of" Algorithm 3.1.2. Instead, in step three, "the // comparison on the host is relaxed to accept hosts on the same site." Step // two is best seen as providing a default value for the AppId when one is not // provided. That leaves step 1 and the same-site check, which is what we // implement here. // // [1] // 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 // [2] https://w3c.github.io/webauthn/#sctn-appid-extension auto* principal = BasePrincipal::Cast(aPrincipal); bool reqIsFromExtension = !!principal->AddonPolicy(); if (reqIsFromExtension) { // AppIDs are not allowed from extensions. return false; } nsCOMPtr callerUri; nsresult rv = principal->GetURI(getter_AddRefs(callerUri)); if (NS_FAILED(rv)) { return false; } nsCOMPtr appIdUri; rv = NS_NewURI(getter_AddRefs(appIdUri), aAppId); if (NS_FAILED(rv)) { return false; } // Step 1 of Algorithm 3.1.2. "If the AppID is not an HTTPS URL, and matches // the FacetID of the caller, no additional processing is necessary and the // operation may proceed." In the web context, the "FacetID" is defined as // "the Web Origin [RFC6454] of the web page triggering the FIDO operation, // written as a URI with an empty path. Default ports are omitted and any path // component is ignored." if (!appIdUri->SchemeIs("https")) { nsCString facetId; rv = principal->GetWebExposedOriginSerialization(facetId); return NS_SUCCEEDED(rv) && facetId == aAppId; } // Same site check nsCOMPtr tldService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); if (!tldService) { return false; } nsAutoCString baseDomainCaller; rv = tldService->GetBaseDomain(callerUri, 0, baseDomainCaller); if (NS_FAILED(rv)) { return false; } nsAutoCString baseDomainAppId; rv = tldService->GetBaseDomain(appIdUri, 0, baseDomainAppId); if (NS_FAILED(rv)) { return false; } if (baseDomainCaller == baseDomainAppId) { return true; } // Exceptions for Google Accounts from Bug 1436078. These were supposed to be // temporary, but users reported breakage when we tried to remove them (Bug // 1822703). We will need to keep them indefinitely. if (baseDomainCaller.EqualsLiteral("google.com") && (aAppId.Equals("https://www.gstatic.com/securitykey/origins.json"_ns) || aAppId.Equals( "https://www.gstatic.com/securitykey/a/google.com/origins.json"_ns))) { return true; } return false; } nsresult DefaultRpId(const nsCOMPtr& aPrincipal, /* out */ nsACString& aRpId) { // [https://w3c.github.io/webauthn/#rp-id] // "By default, the RP ID for a WebAuthn operation is set to the caller's // origin's effective domain." auto* basePrin = BasePrincipal::Cast(aPrincipal); nsCOMPtr uri; if (NS_FAILED(basePrin->GetURI(getter_AddRefs(uri)))) { return NS_ERROR_FAILURE; } return uri->GetAsciiHost(aRpId); } bool IsWebAuthnAllowedInDocument(const nsCOMPtr& aDoc) { MOZ_ASSERT(aDoc); return aDoc->IsHTMLOrXHTML(); } bool IsWebAuthnAllowedInContext(WindowGlobalParent* aContext) { nsIPrincipal* principal = aContext->DocumentPrincipal(); MOZ_ASSERT(principal); if (principal->GetIsNullPrincipal()) { return false; } if (principal->GetIsIpAddress()) { return false; } // This next test is not strictly necessary since CredentialsContainer is // [SecureContext] in our webidl. if (!principal->GetIsOriginPotentiallyTrustworthy()) { return false; } if (principal->GetIsLoopbackHost()) { return true; } if (StaticPrefs::security_webauthn_allow_with_certificate_override()) { return true; } WindowGlobalParent* windowContext = aContext; while (windowContext) { if (nsCOMPtr chan = windowContext->GetDocumentChannel()) { nsCOMPtr securityInfo; nsresult rv = chan->GetSecurityInfo(getter_AddRefs(securityInfo)); if (NS_SUCCEEDED(rv) && securityInfo && !IsWebAuthnAllowedForTransportSecurityInfo(securityInfo)) { return false; } } windowContext = windowContext->GetParentWindowContext(); } return true; } bool IsWebAuthnAllowedForTransportSecurityInfo( nsITransportSecurityInfo* aSecurityInfo) { nsITransportSecurityInfo::OverridableErrorCategory overridableErrorCategory; if (!aSecurityInfo || NS_FAILED(aSecurityInfo->GetOverridableErrorCategory( &overridableErrorCategory))) { return false; } switch (overridableErrorCategory) { case nsITransportSecurityInfo::OverridableErrorCategory::ERROR_UNSET: return true; case nsITransportSecurityInfo::OverridableErrorCategory::ERROR_TIME: return true; case nsITransportSecurityInfo::OverridableErrorCategory::ERROR_TRUST: return false; case nsITransportSecurityInfo::OverridableErrorCategory::ERROR_DOMAIN: return false; default: return false; } } // The "is a registrable domain suffix of or is equal to" condition is defined // in https://html.spec.whatwg.org/multipage/browsers.html#dom-document-domain // as a subroutine of the document.domain setter, and it is exposed in XUL as // the Document::IsValidDomain function. Unfortunately Document::IsValidDomain // takes URIs, and to support web extensions we need to compare directly with // ASCII host names. bool IsRegistrableDomainSuffixOfOrEqualTo(const nsACString& aQuery, const nsACString& aReference) { nsCOMPtr tldService = mozilla::components::EffectiveTLD::Service(); if (!tldService) { return false; } // exclude values that are themselves in the public suffix list. nsAutoCString queryPublicSuffix; nsresult rv = tldService->GetKnownPublicSuffixFromHost(aQuery, queryPublicSuffix); if (NS_FAILED(rv) || aQuery == queryPublicSuffix) { return false; } if (aQuery.Equals(aReference)) { return true; } if (aQuery.Length() > aReference.Length() && StringEndsWith(aQuery, aReference) && aQuery.CharAt(aQuery.Length() - aReference.Length() - 1) == '.') { // The query string ends with '.' followed by the reference string. It is a // registrable domain suffix of the reference string if and only if its base // domain is entirely contained in `aReference`. nsAutoCString queryBaseDomain; rv = tldService->GetBaseDomainFromHost(aQuery, 0, queryBaseDomain); if (NS_FAILED(rv)) { return false; } return aReference.Length() >= queryBaseDomain.Length(); } return false; } static bool OriginCanClaimRpId(const nsCOMPtr& aPrincipal, const nsACString& aRpId) { // This checks two of the conditions defined in // https://w3c.github.io/webauthn/#rp-id, namely that the RP ID value is // (1) "a valid domain string", and // (2) "a registrable domain suffix of or is equal to the caller's origin's // effective domain" // // The specification also requires // (3) Either "the origin's scheme is https" or "the origin's host is // localhost and its scheme is http". // We relax this third condition slightly and expose WebAuthn in all secure // contexts (https://www.w3.org/TR/secure-contexts/). // Condition (1) nsAutoCString normalizedRpId; nsresult rv = NS_DomainToASCII(aRpId, normalizedRpId); if (NS_FAILED(rv)) { return false; } if (normalizedRpId != aRpId) { return false; } // Condition (2) auto* basePrin = BasePrincipal::Cast(aPrincipal); nsAutoCString current; if (NS_FAILED(basePrin->GetAsciiHost(current))) { return false; } if (!IsRegistrableDomainSuffixOfOrEqualTo(current, aRpId)) { return false; } // Condition (3) if (!aPrincipal->GetIsOriginPotentiallyTrustworthy()) { return false; } return true; } static bool ExtensionCanClaimRpId(const nsCOMPtr& aPrincipal, const nsACString& aRpId) { // The conditions here are largely the same as in OriginCanClaimRpId. However, // rather than making direct comparisons with the caller's origin, we check // whether the extension has host permissions for a suitable origin via // WebExtensionPolicy::CanAccessURI (which checks the restricted URI list). // // The conditions that we enforce are: // (1) The RP ID must be a valid domain string. // (2) The RP ID must not be a single-label non-loopback hostname or a known // public suffix. // (3) The extension must have host permissions for either // - https://, or // - http://, if aRpId is a loopback hostname (per // mozilla::net::IsLoopbackHostname). // Condition (1) nsAutoCString normalizedRpId; nsresult rv = NS_DomainToASCII(aRpId, normalizedRpId); if (NS_FAILED(rv)) { return false; } if (normalizedRpId != aRpId) { return false; } // Condition (2) nsCOMPtr tldService = mozilla::components::EffectiveTLD::Service(); if (!tldService) { return false; } nsAutoCString rpIdPublicSuffix; if (NS_FAILED( tldService->GetKnownPublicSuffixFromHost(aRpId, rpIdPublicSuffix))) { return false; } if (aRpId == rpIdPublicSuffix) { return false; } // Exclude single-label non-loopback hostnames. int32_t firstDot = aRpId.FindChar('.'); if ((firstDot < 0 || firstDot == (int32_t)aRpId.Length() - 1) && !mozilla::net::IsLoopbackHostname(aRpId)) { return false; } // Condition (3) auto* basePrin = BasePrincipal::Cast(aPrincipal); MOZ_ASSERT(basePrin->AddonPolicy()); nsAutoCString httpsUriSpec("https://"_ns); httpsUriSpec.Append(aRpId); httpsUriSpec.AppendLiteral("/"); nsCOMPtr uri; rv = NS_NewURI(getter_AddRefs(uri), httpsUriSpec); if (NS_FAILED(rv)) { return false; } if (basePrin->AddonPolicy()->CanAccessURI(uri.get())) { return true; } if (mozilla::net::IsLoopbackHostname(aRpId)) { nsCOMPtr httpUri; rv = NS_MutateURI(uri).SetScheme("http"_ns).Finalize( getter_AddRefs(httpUri)); if (NS_SUCCEEDED(rv) && basePrin->AddonPolicy()->CanAccessURI(httpUri.get())) { return true; } } return false; } bool IsValidRpId(const nsCOMPtr& aPrincipal, const nsACString& aRpId) { auto* basePrincipal = BasePrincipal::Cast(aPrincipal); bool reqIsFromExtension = !!basePrincipal->AddonPolicy(); if (reqIsFromExtension) { return ExtensionCanClaimRpId(aPrincipal, aRpId); } return OriginCanClaimRpId(aPrincipal, aRpId); } nsresult GetWebAuthnClientDataOrigin(nsIPrincipal* aPrincipal, /* out */ nsACString& aOrigin) { auto* basePrincipal = BasePrincipal::Cast(aPrincipal); bool reqIsFromExtension = !!basePrincipal->AddonPolicy(); if (reqIsFromExtension) { nsAutoCString extensionId; basePrincipal->AddonPolicy()->Id()->ToUTF8String(extensionId); nsTArray hashedId; nsresult rv = HashCString(extensionId, hashedId); if (NS_FAILED(rv)) { return rv; } // The extension ID may not be a valid host string. So we use the sha256 // hash of the extension ID encoded in base 16 with the digits [a-p]. aOrigin.Assign("moz-extension://"); for (uint8_t byte : hashedId) { aOrigin.Append(char('a' + ((byte >> 4) & 0x0F))); aOrigin.Append(char('a' + (byte & 0x0F))); } return NS_OK; } return basePrincipal->GetWebExposedOriginSerialization(aOrigin); } static nsresult HashCString(nsICryptoHash* aHashService, const nsACString& aIn, /* out */ nsTArray& aOut) { MOZ_ASSERT(aHashService); nsresult rv = aHashService->Init(nsICryptoHash::SHA256); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aHashService->Update( reinterpret_cast(aIn.BeginReading()), aIn.Length()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsAutoCString fullHash; // Passing false below means we will get a binary result rather than a // base64-encoded string. rv = aHashService->Finish(false, fullHash); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } aOut.Clear(); aOut.AppendElements(reinterpret_cast(fullHash.BeginReading()), fullHash.Length()); return NS_OK; } nsresult HashCString(const nsACString& aIn, /* out */ nsTArray& aOut) { nsresult srv; nsCOMPtr hashService = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &srv); if (NS_FAILED(srv)) { return srv; } srv = HashCString(hashService, aIn, aOut); if (NS_WARN_IF(NS_FAILED(srv))) { return NS_ERROR_FAILURE; } return NS_OK; } uint32_t WebAuthnTimeout(const Optional& aTimeout) { uint32_t adjustedTimeout = 30000; if (aTimeout.WasPassed()) { adjustedTimeout = aTimeout.Value(); adjustedTimeout = std::max(15000u, adjustedTimeout); adjustedTimeout = std::min(120000u, adjustedTimeout); } return adjustedTimeout; } static nsresult SerializeWebAuthnData( const OwningArrayBufferViewOrArrayBuffer& aData, nsString& aOut) { return ProcessTypedArrays( aData, [&](const Span& aData, JS::AutoCheckCannotGC&&) { nsAutoCString result; nsresult rv = mozilla::Base64URLEncode( aData.Length(), aData.Elements(), Base64URLEncodePaddingPolicy::Omit, result); if (NS_SUCCEEDED(rv)) { aOut.Assign(NS_ConvertUTF8toUTF16(result)); } return rv; }); } nsresult SerializeWebAuthnCreationOptions( JSContext* aCx, const nsString& aRpId, const PublicKeyCredentialCreationOptions& aOptions, nsString& aOut) { nsresult rv; PublicKeyCredentialCreationOptionsJSON json; json.mRp.mId.Construct(aRpId); json.mRp.mName.Assign(aOptions.mRp.mName); json.mUser.mName.Assign(aOptions.mUser.mName); rv = SerializeWebAuthnData(aOptions.mUser.mId, json.mUser.mId); NS_ENSURE_SUCCESS(rv, rv); json.mUser.mDisplayName.Assign(aOptions.mUser.mDisplayName); rv = SerializeWebAuthnData(aOptions.mChallenge, json.mChallenge); NS_ENSURE_SUCCESS(rv, rv); json.mPubKeyCredParams = aOptions.mPubKeyCredParams; json.mTimeout.Construct(WebAuthnTimeout(aOptions.mTimeout)); for (const auto& excludeCredential : aOptions.mExcludeCredentials) { PublicKeyCredentialDescriptorJSON* excludeCredentialJSON = json.mExcludeCredentials.AppendElement(fallible); if (!excludeCredentialJSON) { return NS_ERROR_OUT_OF_MEMORY; } excludeCredentialJSON->mType = excludeCredential.mType; rv = SerializeWebAuthnData(excludeCredential.mId, excludeCredentialJSON->mId); NS_ENSURE_SUCCESS(rv, rv); if (excludeCredential.mTransports.WasPassed()) { excludeCredentialJSON->mTransports.Construct( excludeCredential.mTransports.Value()); } } json.mAuthenticatorSelection.Construct(aOptions.mAuthenticatorSelection); json.mHints = aOptions.mHints; json.mAttestation = aOptions.mAttestation; AuthenticationExtensionsClientInputsJSON& extensionsJSON = json.mExtensions.Construct(); if (aOptions.mExtensions.mAppid.WasPassed()) { extensionsJSON.mAppid.Construct(aOptions.mExtensions.mAppid.Value()); } if (aOptions.mExtensions.mCredentialProtectionPolicy.WasPassed()) { extensionsJSON.mCredentialProtectionPolicy.Construct( aOptions.mExtensions.mCredentialProtectionPolicy.Value()); } if (aOptions.mExtensions.mEnforceCredentialProtectionPolicy.WasPassed()) { extensionsJSON.mEnforceCredentialProtectionPolicy.Construct( aOptions.mExtensions.mEnforceCredentialProtectionPolicy.Value()); } if (aOptions.mExtensions.mCredProps.WasPassed()) { extensionsJSON.mCredProps.Construct( aOptions.mExtensions.mCredProps.Value()); } if (aOptions.mExtensions.mHmacCreateSecret.WasPassed()) { extensionsJSON.mHmacCreateSecret.Construct( aOptions.mExtensions.mHmacCreateSecret.Value()); } if (aOptions.mExtensions.mMinPinLength.WasPassed()) { extensionsJSON.mMinPinLength.Construct( aOptions.mExtensions.mMinPinLength.Value()); } if (aOptions.mExtensions.mLargeBlob.WasPassed()) { const AuthenticationExtensionsLargeBlobInputs& largeBlobInputs = aOptions.mExtensions.mLargeBlob.Value(); AuthenticationExtensionsLargeBlobInputsJSON& largeBlobInputsJSON = extensionsJSON.mLargeBlob.Construct(); if (largeBlobInputs.mSupport.WasPassed()) { largeBlobInputsJSON.mSupport.Construct(largeBlobInputs.mSupport.Value()); } if (largeBlobInputs.mRead.WasPassed()) { largeBlobInputsJSON.mRead.Construct(largeBlobInputs.mRead.Value()); } if (largeBlobInputs.mWrite.WasPassed()) { nsString write; rv = SerializeWebAuthnData(largeBlobInputs.mWrite.Value(), write); NS_ENSURE_SUCCESS(rv, rv); largeBlobInputsJSON.mWrite.Construct(write); } } if (aOptions.mExtensions.mPrf.WasPassed()) { const AuthenticationExtensionsPRFInputs& prfInputs = aOptions.mExtensions.mPrf.Value(); AuthenticationExtensionsPRFInputsJSON& prfInputsJSON = extensionsJSON.mPrf.Construct(); if (prfInputs.mEval.WasPassed()) { AuthenticationExtensionsPRFValuesJSON& evalJSON = prfInputsJSON.mEval.Construct(); rv = SerializeWebAuthnData(prfInputs.mEval.Value().mFirst, evalJSON.mFirst); NS_ENSURE_SUCCESS(rv, rv); if (prfInputs.mEval.Value().mSecond.WasPassed()) { nsString second; rv = SerializeWebAuthnData(prfInputs.mEval.Value().mSecond.Value(), second); NS_ENSURE_SUCCESS(rv, rv); evalJSON.mSecond.Construct(second); } } if (prfInputs.mEvalByCredential.WasPassed()) { auto& evalByCredentialJSON = prfInputsJSON.mEvalByCredential.Construct(); for (const auto& entry : prfInputs.mEvalByCredential.Value().Entries()) { auto* jsonEntry = evalByCredentialJSON.Entries().AppendElement(fallible); if (!jsonEntry) { return NS_ERROR_OUT_OF_MEMORY; } jsonEntry->mKey = entry.mKey; AuthenticationExtensionsPRFValuesJSON& valuesJSON = jsonEntry->mValue; rv = SerializeWebAuthnData(entry.mValue.mFirst, valuesJSON.mFirst); NS_ENSURE_SUCCESS(rv, rv); if (entry.mValue.mSecond.WasPassed()) { nsString second; rv = SerializeWebAuthnData(entry.mValue.mSecond.Value(), second); NS_ENSURE_SUCCESS(rv, rv); valuesJSON.mSecond.Construct(second); } } } } JS::Rooted value(aCx); if (!ToJSValue(aCx, json, &value)) { // Callers reject the promise with a fixed error code and have no way to // forward a pending exception, so we must not leave one set. JS_ClearPendingException(aCx); return NS_ERROR_FAILURE; } nsAutoString jsonString; if (!nsContentUtils::StringifyJSON(aCx, value, jsonString, UndefinedIsNullStringLiteral)) { JS_ClearPendingException(aCx); return NS_ERROR_FAILURE; } aOut = std::move(jsonString); return NS_OK; } nsresult SerializeWebAuthnRequestOptions( JSContext* aCx, const nsString& aRpId, const PublicKeyCredentialRequestOptions& aOptions, nsString& aOut) { nsresult rv; PublicKeyCredentialRequestOptionsJSON json; rv = SerializeWebAuthnData(aOptions.mChallenge, json.mChallenge); NS_ENSURE_SUCCESS(rv, rv); json.mTimeout.Construct(WebAuthnTimeout(aOptions.mTimeout)); json.mRpId.Construct(aRpId); for (const auto& allowCredential : aOptions.mAllowCredentials) { PublicKeyCredentialDescriptorJSON* allowCredentialJSON = json.mAllowCredentials.AppendElement(fallible); if (!allowCredentialJSON) { return NS_ERROR_OUT_OF_MEMORY; } allowCredentialJSON->mType = allowCredential.mType; rv = SerializeWebAuthnData(allowCredential.mId, allowCredentialJSON->mId); NS_ENSURE_SUCCESS(rv, rv); if (allowCredential.mTransports.WasPassed()) { allowCredentialJSON->mTransports.Construct( allowCredential.mTransports.Value()); } } json.mUserVerification = aOptions.mUserVerification; json.mHints = aOptions.mHints; AuthenticationExtensionsClientInputsJSON& extensionsJSON = json.mExtensions.Construct(); if (aOptions.mExtensions.mAppid.WasPassed()) { extensionsJSON.mAppid.Construct(aOptions.mExtensions.mAppid.Value()); } if (aOptions.mExtensions.mCredProps.WasPassed()) { extensionsJSON.mCredProps.Construct( aOptions.mExtensions.mCredProps.Value()); } if (aOptions.mExtensions.mHmacCreateSecret.WasPassed()) { extensionsJSON.mHmacCreateSecret.Construct( aOptions.mExtensions.mHmacCreateSecret.Value()); } if (aOptions.mExtensions.mMinPinLength.WasPassed()) { extensionsJSON.mMinPinLength.Construct( aOptions.mExtensions.mMinPinLength.Value()); } if (aOptions.mExtensions.mLargeBlob.WasPassed()) { const AuthenticationExtensionsLargeBlobInputs& largeBlobInputs = aOptions.mExtensions.mLargeBlob.Value(); AuthenticationExtensionsLargeBlobInputsJSON& largeBlobInputsJSON = extensionsJSON.mLargeBlob.Construct(); if (largeBlobInputs.mSupport.WasPassed()) { largeBlobInputsJSON.mSupport.Construct(largeBlobInputs.mSupport.Value()); } if (largeBlobInputs.mRead.WasPassed()) { largeBlobInputsJSON.mRead.Construct(largeBlobInputs.mRead.Value()); } if (largeBlobInputs.mWrite.WasPassed()) { nsString write; rv = SerializeWebAuthnData(largeBlobInputs.mWrite.Value(), write); NS_ENSURE_SUCCESS(rv, rv); largeBlobInputsJSON.mWrite.Construct(write); } } if (aOptions.mExtensions.mPrf.WasPassed()) { const AuthenticationExtensionsPRFInputs& prfInputs = aOptions.mExtensions.mPrf.Value(); AuthenticationExtensionsPRFInputsJSON& prfInputsJSON = extensionsJSON.mPrf.Construct(); if (prfInputs.mEval.WasPassed()) { AuthenticationExtensionsPRFValuesJSON& evalJSON = prfInputsJSON.mEval.Construct(); rv = SerializeWebAuthnData(prfInputs.mEval.Value().mFirst, evalJSON.mFirst); NS_ENSURE_SUCCESS(rv, rv); if (prfInputs.mEval.Value().mSecond.WasPassed()) { nsString second; rv = SerializeWebAuthnData(prfInputs.mEval.Value().mSecond.Value(), second); NS_ENSURE_SUCCESS(rv, rv); evalJSON.mSecond.Construct(second); } } if (prfInputs.mEvalByCredential.WasPassed()) { auto& evalByCredentialJSON = prfInputsJSON.mEvalByCredential.Construct(); for (const auto& entry : prfInputs.mEvalByCredential.Value().Entries()) { auto* jsonEntry = evalByCredentialJSON.Entries().AppendElement(fallible); if (!jsonEntry) { return NS_ERROR_OUT_OF_MEMORY; } jsonEntry->mKey = entry.mKey; AuthenticationExtensionsPRFValuesJSON& valuesJSON = jsonEntry->mValue; rv = SerializeWebAuthnData(entry.mValue.mFirst, valuesJSON.mFirst); NS_ENSURE_SUCCESS(rv, rv); if (entry.mValue.mSecond.WasPassed()) { nsString second; rv = SerializeWebAuthnData(entry.mValue.mSecond.Value(), second); NS_ENSURE_SUCCESS(rv, rv); valuesJSON.mSecond.Construct(second); } } } } JS::Rooted value(aCx); if (!ToJSValue(aCx, json, &value)) { // Callers reject the promise with a fixed error code and have no way to // forward a pending exception, so we must not leave one set. JS_ClearPendingException(aCx); return NS_ERROR_FAILURE; } nsAutoString jsonString; if (!nsContentUtils::StringifyJSON(aCx, value, jsonString, UndefinedIsNullStringLiteral)) { JS_ClearPendingException(aCx); return NS_ERROR_FAILURE; } aOut = std::move(jsonString); return NS_OK; } } // namespace mozilla::dom