/* -*- 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/Base64.h" #include "mozilla/JSONStringWriteFuncs.h" #include "mozilla/JSONWriter.h" #include "mozilla/StaticPrefs_security.h" #include "mozilla/dom/PWindowGlobalParent.h" #include "mozilla/dom/WebAuthnTransactionParent.h" #include "mozilla/dom/WindowGlobalParent.h" #include "nsThreadUtils.h" #include "WebAuthnArgs.h" #include "WebAuthnUtil.h" namespace mozilla::dom { nsresult AssembleClientData(WindowGlobalParent* aManager, const nsACString& aType, const nsTArray& aChallenge, /* out */ nsACString& aJsonOut) { nsAutoCString challengeBase64; nsresult rv = Base64URLEncode(aChallenge.Length(), aChallenge.Elements(), Base64URLEncodePaddingPolicy::Omit, challengeBase64); if (NS_FAILED(rv)) { return NS_ERROR_FAILURE; } nsIPrincipal* principal = aManager->DocumentPrincipal(); nsIPrincipal* topPrincipal = aManager->TopWindowContext()->DocumentPrincipal(); nsCString origin; rv = principal->GetWebExposedOriginSerialization(origin); if (NS_FAILED(rv)) { return NS_ERROR_FAILURE; } bool crossOrigin = !principal->Equals(topPrincipal); // Serialize the collected client data using the algorithm from // https://www.w3.org/TR/webauthn-3/#clientdatajson-serialization. // Please update the definition of CollectedClientData in // dom/webidl/WebAuthentication.webidl when changes are made here. JSONStringRefWriteFunc f(aJsonOut); JSONWriter w(f, JSONWriter::CollectionStyle::SingleLineStyle); w.Start(); // Steps 2 and 3 w.StringProperty("type", aType); // Steps 4 and 5 w.StringProperty("challenge", challengeBase64); // Steps 6 and 7 w.StringProperty("origin", origin); // Steps 8 - 10 w.BoolProperty("crossOrigin", crossOrigin); // Step 11. The description of the algorithm says "If topOrigin is present", // but the definition of topOrigin says that topOrigin "is set only if [...] // crossOrigin is true." so we use the latter condition instead. if (crossOrigin) { nsCString topOrigin; rv = topPrincipal->GetWebExposedOriginSerialization(topOrigin); if (NS_FAILED(rv)) { return NS_ERROR_FAILURE; } w.StringProperty("topOrigin", topOrigin); } w.End(); return NS_OK; } bool GetAssertionRequestIncludesLargeBlobRead( const WebAuthnGetAssertionInfo& aInfo) { for (const WebAuthnExtension& ext : aInfo.Extensions()) { if (ext.type() == WebAuthnExtension::TWebAuthnExtensionLargeBlob) { if (ext.get_WebAuthnExtensionLargeBlob().flag().isSome()) { return ext.get_WebAuthnExtensionLargeBlob().flag().ref(); } } } return false; } bool MakeCredentialRequestIncludesPrfExtension( const WebAuthnMakeCredentialInfo& aInfo) { for (const WebAuthnExtension& ext : aInfo.Extensions()) { if (ext.type() == WebAuthnExtension::TWebAuthnExtensionPrf) { return true; } } return false; } void WebAuthnTransactionParent::CompleteTransaction() { if (mTransactionId.isSome()) { if (mRegisterPromiseRequest.Exists()) { mRegisterPromiseRequest.Complete(); } if (mSignPromiseRequest.Exists()) { mSignPromiseRequest.Complete(); } if (mWebAuthnService) { // We have to do this to work around Bug 1864526. mWebAuthnService->Cancel(mTransactionId.ref()); } mTransactionId.reset(); } } void WebAuthnTransactionParent::DisconnectTransaction() { mTransactionId.reset(); mRegisterPromiseRequest.DisconnectIfExists(); mSignPromiseRequest.DisconnectIfExists(); if (mWebAuthnService) { mWebAuthnService->Reset(); } } mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestRegister( const WebAuthnMakeCredentialInfo& aTransactionInfo, RequestRegisterResolver&& aResolver) { MOZ_ASSERT(NS_IsMainThread()); if (!mWebAuthnService) { mWebAuthnService = do_GetService("@mozilla.org/webauthn/service;1"); if (!mWebAuthnService) { aResolver(NS_ERROR_NOT_AVAILABLE); return IPC_OK(); } } // If there's an ongoing transaction, abort it. if (mTransactionId.isSome()) { DisconnectTransaction(); } uint64_t aTransactionId = NextId(); mTransactionId = Some(aTransactionId); WindowGlobalParent* manager = static_cast(Manager()); nsIPrincipal* principal = manager->DocumentPrincipal(); WindowGlobalParent* windowContext = manager; while (windowContext) { nsITransportSecurityInfo* securityInfo = windowContext->GetSecurityInfo(); if (securityInfo && !IsWebAuthnAllowedForTransportSecurityInfo(securityInfo)) { aResolver(NS_ERROR_DOM_SECURITY_ERR); return IPC_OK(); } windowContext = windowContext->GetParentWindowContext(); } if (!IsWebAuthnAllowedForPrincipal(principal)) { aResolver(NS_ERROR_DOM_SECURITY_ERR); return IPC_OK(); } if (!IsValidRpId(principal, aTransactionInfo.RpId())) { aResolver(NS_ERROR_DOM_SECURITY_ERR); return IPC_OK(); } nsCString origin; nsresult rv = principal->GetWebExposedOriginSerialization(origin); if (NS_FAILED(rv)) { aResolver(NS_ERROR_FAILURE); return IPC_OK(); } nsCString clientDataJSON; rv = AssembleClientData(manager, "webauthn.create"_ns, aTransactionInfo.Challenge(), clientDataJSON); if (NS_FAILED(rv)) { aResolver(NS_ERROR_FAILURE); return IPC_OK(); } bool requestIncludesPrfExtension = MakeCredentialRequestIncludesPrfExtension(aTransactionInfo); RefPtr promiseHolder = new WebAuthnRegisterPromiseHolder(GetCurrentSerialEventTarget()); RefPtr promise = promiseHolder->Ensure(); promise ->Then( GetCurrentSerialEventTarget(), __func__, [self = RefPtr{this}, inputClientData = clientDataJSON, requestIncludesPrfExtension, resolver = std::move(aResolver)]( const WebAuthnRegisterPromise::ResolveOrRejectValue& aValue) { self->CompleteTransaction(); if (aValue.IsReject()) { resolver(aValue.RejectValue()); return; } auto rejectWithNotAllowed = MakeScopeExit( [&]() { resolver(NS_ERROR_DOM_NOT_ALLOWED_ERR); }); RefPtr registerResult = aValue.ResolveValue(); nsCString clientData; nsresult rv = registerResult->GetClientDataJSON(clientData); if (rv == NS_ERROR_NOT_AVAILABLE) { clientData = inputClientData; } else if (NS_FAILED(rv)) { return; } nsTArray attObj; rv = registerResult->GetAttestationObject(attObj); if (NS_WARN_IF(NS_FAILED(rv))) { return; } nsTArray credentialId; rv = registerResult->GetCredentialId(credentialId); if (NS_WARN_IF(NS_FAILED(rv))) { return; } nsTArray transports; rv = registerResult->GetTransports(transports); if (NS_WARN_IF(NS_FAILED(rv))) { return; } Maybe authenticatorAttachment; nsString maybeAuthenticatorAttachment; rv = registerResult->GetAuthenticatorAttachment( maybeAuthenticatorAttachment); if (rv != NS_ERROR_NOT_AVAILABLE) { if (NS_WARN_IF(NS_FAILED(rv))) { return; } authenticatorAttachment = Some(maybeAuthenticatorAttachment); } nsTArray extensions; bool credPropsRk; rv = registerResult->GetCredPropsRk(&credPropsRk); if (rv != NS_ERROR_NOT_AVAILABLE) { if (NS_WARN_IF(NS_FAILED(rv))) { return; } extensions.AppendElement( WebAuthnExtensionResultCredProps(credPropsRk)); } bool hmacCreateSecret; rv = registerResult->GetHmacCreateSecret(&hmacCreateSecret); if (rv != NS_ERROR_NOT_AVAILABLE) { if (NS_WARN_IF(NS_FAILED(rv))) { return; } extensions.AppendElement( WebAuthnExtensionResultHmacSecret(hmacCreateSecret)); } bool largeBlobSupported; rv = registerResult->GetLargeBlobSupported(&largeBlobSupported); if (rv != NS_ERROR_NOT_AVAILABLE) { if (NS_FAILED(rv)) { return; } nsTArray blob; // unused extensions.AppendElement(WebAuthnExtensionResultLargeBlob( largeBlobSupported, blob, false)); } if (requestIncludesPrfExtension) { Maybe prfResults = Nothing(); bool prfEnabled = false; rv = registerResult->GetPrfEnabled(&prfEnabled); if (rv != NS_ERROR_NOT_AVAILABLE && NS_FAILED(rv)) { return; } nsTArray prfResultsFirst; rv = registerResult->GetPrfResultsFirst(prfResultsFirst); if (rv != NS_ERROR_NOT_AVAILABLE) { if (NS_WARN_IF(NS_FAILED(rv))) { return; } bool prfResultsSecondMaybe = false; nsTArray prfResultsSecond; rv = registerResult->GetPrfResultsSecond(prfResultsSecond); if (rv != NS_ERROR_NOT_AVAILABLE) { if (NS_WARN_IF(NS_FAILED(rv))) { return; } prfResultsSecondMaybe = true; } prfResults = Some(WebAuthnExtensionPrfValues( prfResultsFirst, prfResultsSecondMaybe, prfResultsSecond)); } extensions.AppendElement( WebAuthnExtensionResultPrf(Some(prfEnabled), prfResults)); } WebAuthnMakeCredentialResult result( clientData, attObj, credentialId, transports, extensions, authenticatorAttachment); rejectWithNotAllowed.release(); resolver(result); }) ->Track(mRegisterPromiseRequest); uint64_t browsingContextId = manager->GetBrowsingContext()->Top()->Id(); bool privateBrowsing = principal->GetIsInPrivateBrowsing(); auto args = MakeRefPtr( origin, clientDataJSON, privateBrowsing, aTransactionInfo); rv = mWebAuthnService->MakeCredential(aTransactionId, browsingContextId, args, promiseHolder); if (NS_FAILED(rv)) { promiseHolder->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR); } return IPC_OK(); } mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestSign( const WebAuthnGetAssertionInfo& aTransactionInfo, RequestSignResolver&& aResolver) { MOZ_ASSERT(NS_IsMainThread()); if (!mWebAuthnService) { mWebAuthnService = do_GetService("@mozilla.org/webauthn/service;1"); if (!mWebAuthnService) { aResolver(NS_ERROR_NOT_AVAILABLE); return IPC_OK(); } } if (mTransactionId.isSome()) { DisconnectTransaction(); } uint64_t transactionId = NextId(); mTransactionId = Some(transactionId); WindowGlobalParent* manager = static_cast(Manager()); nsIPrincipal* principal = manager->DocumentPrincipal(); WindowGlobalParent* windowContext = manager; while (windowContext) { nsITransportSecurityInfo* securityInfo = windowContext->GetSecurityInfo(); if (securityInfo && !IsWebAuthnAllowedForTransportSecurityInfo(securityInfo)) { aResolver(NS_ERROR_DOM_SECURITY_ERR); return IPC_OK(); } windowContext = windowContext->GetParentWindowContext(); } if (!IsWebAuthnAllowedForPrincipal(principal)) { aResolver(NS_ERROR_DOM_SECURITY_ERR); return IPC_OK(); } if (!IsValidRpId(principal, aTransactionInfo.RpId())) { aResolver(NS_ERROR_DOM_SECURITY_ERR); return IPC_OK(); } if (aTransactionInfo.AppId().isSome() && !IsValidAppId(principal, aTransactionInfo.AppId().ref())) { aResolver(NS_ERROR_DOM_SECURITY_ERR); return IPC_OK(); } nsCString origin; nsresult rv = principal->GetWebExposedOriginSerialization(origin); if (NS_FAILED(rv)) { aResolver(NS_ERROR_FAILURE); return IPC_OK(); } nsCString clientDataJSON; rv = AssembleClientData(manager, "webauthn.get"_ns, aTransactionInfo.Challenge(), clientDataJSON); if (NS_FAILED(rv)) { aResolver(NS_ERROR_FAILURE); return IPC_OK(); } bool requestIncludesAppId = aTransactionInfo.AppId().isSome(); bool requestIncludesLargeBlobRead = GetAssertionRequestIncludesLargeBlobRead(aTransactionInfo); RefPtr promiseHolder = new WebAuthnSignPromiseHolder(GetCurrentSerialEventTarget()); RefPtr promise = promiseHolder->Ensure(); promise ->Then( GetCurrentSerialEventTarget(), __func__, [self = RefPtr{this}, inputClientData = clientDataJSON, requestIncludesAppId, requestIncludesLargeBlobRead, resolver = std::move(aResolver)]( const WebAuthnSignPromise::ResolveOrRejectValue& aValue) { self->CompleteTransaction(); if (aValue.IsReject()) { resolver(aValue.RejectValue()); return; } auto rejectWithNotAllowed = MakeScopeExit( [&]() { resolver(NS_ERROR_DOM_NOT_ALLOWED_ERR); }); RefPtr signResult = aValue.ResolveValue(); nsCString clientData; nsresult rv = signResult->GetClientDataJSON(clientData); if (rv == NS_ERROR_NOT_AVAILABLE) { clientData = inputClientData; } else if (NS_FAILED(rv)) { return; } nsTArray credentialId; rv = signResult->GetCredentialId(credentialId); if (NS_WARN_IF(NS_FAILED(rv))) { return; } nsTArray signature; rv = signResult->GetSignature(signature); if (NS_WARN_IF(NS_FAILED(rv))) { return; } nsTArray authenticatorData; rv = signResult->GetAuthenticatorData(authenticatorData); if (NS_WARN_IF(NS_FAILED(rv))) { return; } nsTArray userHandle; Unused << signResult->GetUserHandle(userHandle); // optional Maybe authenticatorAttachment; nsString maybeAuthenticatorAttachment; rv = signResult->GetAuthenticatorAttachment( maybeAuthenticatorAttachment); if (rv != NS_ERROR_NOT_AVAILABLE) { if (NS_WARN_IF(NS_FAILED(rv))) { return; } authenticatorAttachment = Some(maybeAuthenticatorAttachment); } nsTArray extensions; if (requestIncludesAppId) { bool usedAppId = false; rv = signResult->GetUsedAppId(&usedAppId); if (rv != NS_ERROR_NOT_AVAILABLE && NS_FAILED(rv)) { return; } extensions.AppendElement(WebAuthnExtensionResultAppId(usedAppId)); } nsTArray largeBlobValue; rv = signResult->GetLargeBlobValue(largeBlobValue); if (rv != NS_ERROR_NOT_AVAILABLE) { if (NS_FAILED(rv)) { return; } extensions.AppendElement(WebAuthnExtensionResultLargeBlob( true, largeBlobValue, false)); } else if (requestIncludesLargeBlobRead) { // Signal a read error by setting both flags. extensions.AppendElement( WebAuthnExtensionResultLargeBlob(true, largeBlobValue, true)); } else { // Read and write operations are mutually exclusive, so we only // check for a write result if the read result is not available. bool largeBlobWritten; rv = signResult->GetLargeBlobWritten(&largeBlobWritten); if (rv != NS_ERROR_NOT_AVAILABLE) { if (NS_FAILED(rv)) { return; } extensions.AppendElement(WebAuthnExtensionResultLargeBlob( false, largeBlobValue, largeBlobWritten)); } } { Maybe prfResults; bool prfMaybe = false; rv = signResult->GetPrfMaybe(&prfMaybe); if (rv == NS_OK && prfMaybe) { nsTArray prfResultsFirst; rv = signResult->GetPrfResultsFirst(prfResultsFirst); if (rv != NS_ERROR_NOT_AVAILABLE) { if (NS_WARN_IF(NS_FAILED(rv))) { return; } bool prfResultsSecondMaybe = false; nsTArray prfResultsSecond; rv = signResult->GetPrfResultsSecond(prfResultsSecond); if (rv != NS_ERROR_NOT_AVAILABLE) { if (NS_WARN_IF(NS_FAILED(rv))) { return; } prfResultsSecondMaybe = true; } prfResults = Some(WebAuthnExtensionPrfValues( prfResultsFirst, prfResultsSecondMaybe, prfResultsSecond)); } else { prfResults = Nothing(); } extensions.AppendElement( WebAuthnExtensionResultPrf(Nothing(), prfResults)); } } WebAuthnGetAssertionResult result( clientData, credentialId, signature, authenticatorData, extensions, userHandle, authenticatorAttachment); rejectWithNotAllowed.release(); resolver(result); }) ->Track(mSignPromiseRequest); uint64_t browsingContextId = manager->GetBrowsingContext()->Top()->Id(); bool privateBrowsing = principal->GetIsInPrivateBrowsing(); auto args = MakeRefPtr(origin, clientDataJSON, privateBrowsing, aTransactionInfo); rv = mWebAuthnService->GetAssertion(transactionId, browsingContextId, args, promiseHolder); if (NS_FAILED(rv)) { promiseHolder->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR); } return IPC_OK(); } mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestCancel() { MOZ_ASSERT(NS_IsMainThread()); if (mTransactionId.isNothing()) { return IPC_OK(); } DisconnectTransaction(); return IPC_OK(); } mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestIsUVPAA( RequestIsUVPAAResolver&& aResolver) { MOZ_ASSERT(NS_IsMainThread()); #ifdef MOZ_WIDGET_ANDROID // Try the nsIWebAuthnService. If we're configured for tests we // will get a result. Otherwise we expect NS_ERROR_NOT_IMPLEMENTED. nsCOMPtr service( do_GetService("@mozilla.org/webauthn/service;1")); bool available; nsresult rv = service->GetIsUVPAA(&available); if (NS_SUCCEEDED(rv)) { aResolver(available); return IPC_OK(); } // Don't consult the platform API if resident key support is disabled. if (!StaticPrefs:: security_webauthn_webauthn_enable_android_fido2_residentkey()) { aResolver(false); return IPC_OK(); } // The GeckoView implementation of // isUserVerifiyingPlatformAuthenticatorAvailable dispatches the work to a // background thread and returns a MozPromise which we can ->Then to call // aResolver on the current thread. nsCOMPtr target = GetCurrentSerialEventTarget(); auto result = java::WebAuthnTokenManager:: WebAuthnIsUserVerifyingPlatformAuthenticatorAvailable(); auto geckoResult = java::GeckoResult::LocalRef(std::move(result)); MozPromise::FromGeckoResult(geckoResult) ->Then(target, __func__, [resolver = std::move(aResolver)]( const MozPromise::ResolveOrRejectValue& aValue) { if (aValue.IsResolve()) { resolver(aValue.ResolveValue()); } else { resolver(false); } }); return IPC_OK(); #else nsCOMPtr target = GetCurrentSerialEventTarget(); nsCOMPtr runnable(NS_NewRunnableFunction( __func__, [target, resolver = std::move(aResolver)]() { bool available; nsCOMPtr service( do_GetService("@mozilla.org/webauthn/service;1")); nsresult rv = service->GetIsUVPAA(&available); if (NS_FAILED(rv)) { available = false; } BoolPromise::CreateAndResolve(available, __func__) ->Then(target, __func__, [resolver](const BoolPromise::ResolveOrRejectValue& value) { if (value.IsResolve()) { resolver(value.ResolveValue()); } else { resolver(false); } }); })); NS_DispatchBackgroundTask(runnable.forget(), NS_DISPATCH_EVENT_MAY_BLOCK); return IPC_OK(); #endif } void WebAuthnTransactionParent::ActorDestroy(ActorDestroyReason aWhy) { // Called either by Send__delete__() in RecvDestroyMe() above, or when // the channel disconnects. Ensure the token manager forgets about us. MOZ_ASSERT(NS_IsMainThread()); if (mTransactionId.isSome()) { DisconnectTransaction(); } } } // namespace mozilla::dom