diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /dom/u2f/U2F.cpp | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/u2f/U2F.cpp')
-rw-r--r-- | dom/u2f/U2F.cpp | 643 |
1 files changed, 643 insertions, 0 deletions
diff --git a/dom/u2f/U2F.cpp b/dom/u2f/U2F.cpp new file mode 100644 index 0000000000..c5ee8c41c5 --- /dev/null +++ b/dom/u2f/U2F.cpp @@ -0,0 +1,643 @@ +/* -*- 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/dom/U2F.h" +#include "mozilla/dom/WebCryptoCommon.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/WebAuthnTransactionChild.h" +#include "mozilla/dom/WebAuthnUtil.h" +#include "nsContentUtils.h" +#include "nsNetUtil.h" +#include "nsURLParsers.h" + +#ifdef OS_WIN +# include "WinWebAuthnManager.h" +#endif + +using namespace mozilla::ipc; + +class JSJitInfo; + +// Forward decl because of nsHTMLDocument.h's complex dependency on +// /layout/style +class nsHTMLDocument { + public: + bool IsRegistrableDomainSuffixOfOrEqualTo(const nsAString& aHostSuffixString, + const nsACString& aOrigHost); +}; + +namespace mozilla::dom { + +constexpr auto kFinishEnrollment = u"navigator.id.finishEnrollment"_ns; +constexpr auto kGetAssertion = u"navigator.id.getAssertion"_ns; + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(U2F) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY +NS_INTERFACE_MAP_END_INHERITING(WebAuthnManagerBase) + +NS_IMPL_ADDREF_INHERITED(U2F, WebAuthnManagerBase) +NS_IMPL_RELEASE_INHERITED(U2F, WebAuthnManagerBase) + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(U2F) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(U2F, WebAuthnManagerBase) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTransaction) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + tmp->mTransaction.reset(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(U2F, WebAuthnManagerBase) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransaction) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +/*********************************************************************** + * Utility Functions + **********************************************************************/ + +static ErrorCode ConvertNSResultToErrorCode(const nsresult& aError) { + if (aError == NS_ERROR_DOM_TIMEOUT_ERR) { + return ErrorCode::TIMEOUT; + } + /* Emitted by U2F{Soft,HID}TokenManager when we really mean ineligible */ + if (aError == NS_ERROR_DOM_INVALID_STATE_ERR) { + return ErrorCode::DEVICE_INELIGIBLE; + } + return ErrorCode::OTHER_ERROR; +} + +static uint32_t AdjustedTimeoutMillis( + const Optional<Nullable<int32_t>>& opt_aSeconds) { + uint32_t adjustedTimeoutMillis = 30000u; + if (opt_aSeconds.WasPassed() && !opt_aSeconds.Value().IsNull()) { + adjustedTimeoutMillis = opt_aSeconds.Value().Value() * 1000u; + adjustedTimeoutMillis = std::max(15000u, adjustedTimeoutMillis); + adjustedTimeoutMillis = std::min(120000u, adjustedTimeoutMillis); + } + return adjustedTimeoutMillis; +} + +static nsresult AssembleClientData(const nsAString& aOrigin, + const nsAString& aTyp, + const nsAString& aChallenge, + /* out */ nsString& aClientData) { + MOZ_ASSERT(NS_IsMainThread()); + U2FClientData clientDataObject; + clientDataObject.mTyp.Construct(aTyp); // "Typ" from the U2F specification + clientDataObject.mChallenge.Construct(aChallenge); + clientDataObject.mOrigin.Construct(aOrigin); + + if (NS_WARN_IF(!clientDataObject.ToJSON(aClientData))) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +static void RegisteredKeysToScopedCredentialList( + const nsAString& aAppId, const nsTArray<RegisteredKey>& aKeys, + nsTArray<WebAuthnScopedCredential>& aList) { + for (const RegisteredKey& key : aKeys) { + // Check for required attributes + if (!key.mVersion.WasPassed() || !key.mKeyHandle.WasPassed() || + key.mVersion.Value() != kRequiredU2FVersion) { + continue; + } + + // If this key's mAppId doesn't match the invocation, we can't handle it. + if (key.mAppId.WasPassed() && !key.mAppId.Value().Equals(aAppId)) { + continue; + } + + CryptoBuffer keyHandle; + nsresult rv = keyHandle.FromJwkBase64(key.mKeyHandle.Value()); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + WebAuthnScopedCredential c; + c.id() = keyHandle; + aList.AppendElement(c); + } +} + +/*********************************************************************** + * U2F JavaScript API Implementation + **********************************************************************/ + +U2F::~U2F() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mTransaction.isSome()) { + ClearTransaction(); + } + + if (mChild) { + RefPtr<WebAuthnTransactionChild> c; + mChild.swap(c); + c->Disconnect(); + } +} + +void U2F::Init(ErrorResult& aRv) { + MOZ_ASSERT(mParent); + + nsCOMPtr<Document> doc = mParent->GetDoc(); + MOZ_ASSERT(doc); + if (!doc) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + nsIPrincipal* principal = doc->NodePrincipal(); + aRv = nsContentUtils::GetUTFOrigin(principal, mOrigin); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + if (NS_WARN_IF(mOrigin.IsEmpty())) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } +} + +/* virtual */ +JSObject* U2F::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return U2F_Binding::Wrap(aCx, this, aGivenProto); +} + +template <typename T, typename C> +void U2F::ExecuteCallback(T& aResp, nsMainThreadPtrHandle<C>& aCb) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aCb); + + ErrorResult error; + RefPtr<C> temp = aCb.get(); // Make sure it stays alive + temp->Call(aResp, error); + NS_WARNING_ASSERTION(!error.Failed(), "dom::U2F::Promise callback failed"); + error.SuppressException(); // Useful exceptions already emitted +} + +void U2F::Register(const nsAString& aAppId, + const Sequence<RegisterRequest>& aRegisterRequests, + const Sequence<RegisteredKey>& aRegisteredKeys, + U2FRegisterCallback& aCallback, + const Optional<Nullable<int32_t>>& opt_aTimeoutSeconds, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + + nsMainThreadPtrHandle<U2FRegisterCallback> callback( + new nsMainThreadPtrHolder<U2FRegisterCallback>("U2F::Register::callback", + &aCallback)); + + // Ensure we have a callback. + if (NS_WARN_IF(!callback)) { + return; + } + + if (mTransaction.isSome()) { + // If there hasn't been a visibility change during the current + // transaction, then let's let that one complete rather than + // cancelling it on a subsequent call. + if (!mTransaction.ref().mVisibilityChanged) { + RegisterResponse response; + response.mErrorCode.Construct( + static_cast<uint32_t>(ErrorCode::OTHER_ERROR)); + ExecuteCallback(response, callback); + return; + } + + // Otherwise, the user may well have clicked away, so let's + // abort the old transaction and take over control from here. + CancelTransaction(NS_ERROR_ABORT); + } + + // Evaluate the AppID + nsString adjustedAppId(aAppId); + if (!EvaluateAppID(mParent, mOrigin, adjustedAppId)) { + RegisterResponse response; + response.mErrorCode.Construct( + static_cast<uint32_t>(ErrorCode::BAD_REQUEST)); + ExecuteCallback(response, callback); + return; + } + + nsAutoString clientDataJSON; + + // Pick the first valid RegisterRequest; we can only work with one. + CryptoBuffer challenge; + for (const RegisterRequest& req : aRegisterRequests) { + if (!req.mChallenge.WasPassed() || !req.mVersion.WasPassed() || + req.mVersion.Value() != kRequiredU2FVersion) { + continue; + } + if (!challenge.Assign(NS_ConvertUTF16toUTF8(req.mChallenge.Value()))) { + continue; + } + + nsresult rv = AssembleClientData(mOrigin, kFinishEnrollment, + req.mChallenge.Value(), clientDataJSON); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + } + + // Did we not get a valid RegisterRequest? Abort. + if (clientDataJSON.IsEmpty()) { + RegisterResponse response; + response.mErrorCode.Construct( + static_cast<uint32_t>(ErrorCode::BAD_REQUEST)); + ExecuteCallback(response, callback); + return; + } + + // Build the exclusion list, if any + nsTArray<WebAuthnScopedCredential> excludeList; + RegisteredKeysToScopedCredentialList(adjustedAppId, aRegisteredKeys, + excludeList); + + if (!MaybeCreateBackgroundActor()) { + RegisterResponse response; + response.mErrorCode.Construct( + static_cast<uint32_t>(ErrorCode::OTHER_ERROR)); + ExecuteCallback(response, callback); + return; + } + +#ifdef OS_WIN + if (!WinWebAuthnManager::AreWebAuthNApisAvailable()) { + ListenForVisibilityEvents(); + } +#else + ListenForVisibilityEvents(); +#endif + + NS_ConvertUTF16toUTF8 clientData(clientDataJSON); + uint32_t adjustedTimeoutMillis = AdjustedTimeoutMillis(opt_aTimeoutSeconds); + + BrowsingContext* context = mParent->GetBrowsingContext(); + if (!context) { + RegisterResponse response; + response.mErrorCode.Construct( + static_cast<uint32_t>(ErrorCode::OTHER_ERROR)); + ExecuteCallback(response, callback); + return; + } + + WebAuthnMakeCredentialInfo info(mOrigin, adjustedAppId, challenge, clientData, + adjustedTimeoutMillis, excludeList, + Nothing(), /* no extra info for U2F */ + context->Id()); + + MOZ_ASSERT(mTransaction.isNothing()); + mTransaction = Some(U2FTransaction(AsVariant(callback))); + mChild->SendRequestRegister(mTransaction.ref().mId, info); +} + +using binding_detail::GenericMethod; +using binding_detail::NormalThisPolicy; +using binding_detail::ThrowExceptions; + +// register_impl_methodinfo is generated by bindings. +namespace U2F_Binding { +extern const JSJitInfo register_impl_methodinfo; +} // namespace U2F_Binding + +// We have 4 non-optional args. +static const JSFunctionSpec register_spec = JS_FNSPEC( + "register", (GenericMethod<NormalThisPolicy, ThrowExceptions>), + &U2F_Binding::register_impl_methodinfo, 4, JSPROP_ENUMERATE, nullptr); + +void U2F::GetRegister(JSContext* aCx, + JS::MutableHandle<JSObject*> aRegisterFunc, + ErrorResult& aRv) { + JSFunction* fun = JS::NewFunctionFromSpec(aCx, ®ister_spec); + if (!fun) { + aRv.NoteJSContextException(aCx); + return; + } + + aRegisterFunc.set(JS_GetFunctionObject(fun)); +} + +void U2F::FinishMakeCredential(const uint64_t& aTransactionId, + const WebAuthnMakeCredentialResult& aResult) { + MOZ_ASSERT(NS_IsMainThread()); + + // Check for a valid transaction. + if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) { + return; + } + + if (NS_WARN_IF(!mTransaction.ref().HasRegisterCallback())) { + RejectTransaction(NS_ERROR_ABORT); + return; + } + + // A CTAP2 response. + if (aResult.RegistrationData().Length() == 0) { + RejectTransaction(NS_ERROR_ABORT); + return; + } + + CryptoBuffer clientDataBuf; + if (NS_WARN_IF(!clientDataBuf.Assign(aResult.ClientDataJSON()))) { + RejectTransaction(NS_ERROR_ABORT); + return; + } + + CryptoBuffer regBuf; + if (NS_WARN_IF(!regBuf.Assign(aResult.RegistrationData()))) { + RejectTransaction(NS_ERROR_ABORT); + return; + } + + nsString clientDataBase64; + nsString registrationDataBase64; + nsresult rvClientData = clientDataBuf.ToJwkBase64(clientDataBase64); + nsresult rvRegistrationData = regBuf.ToJwkBase64(registrationDataBase64); + + if (NS_WARN_IF(NS_FAILED(rvClientData)) || + NS_WARN_IF(NS_FAILED(rvRegistrationData))) { + RejectTransaction(NS_ERROR_ABORT); + return; + } + + // Assemble a response object to return + RegisterResponse response; + response.mVersion.Construct(kRequiredU2FVersion); + response.mClientData.Construct(clientDataBase64); + response.mRegistrationData.Construct(registrationDataBase64); + response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::OK)); + + // Keep the callback pointer alive. + nsMainThreadPtrHandle<U2FRegisterCallback> callback( + mTransaction.ref().GetRegisterCallback()); + + ClearTransaction(); + ExecuteCallback(response, callback); +} + +void U2F::Sign(const nsAString& aAppId, const nsAString& aChallenge, + const Sequence<RegisteredKey>& aRegisteredKeys, + U2FSignCallback& aCallback, + const Optional<Nullable<int32_t>>& opt_aTimeoutSeconds, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + + nsMainThreadPtrHandle<U2FSignCallback> callback( + new nsMainThreadPtrHolder<U2FSignCallback>("U2F::Sign::callback", + &aCallback)); + + // Ensure we have a callback. + if (NS_WARN_IF(!callback)) { + return; + } + + if (mTransaction.isSome()) { + // If there hasn't been a visibility change during the current + // transaction, then let's let that one complete rather than + // cancelling it on a subsequent call. + if (!mTransaction.ref().mVisibilityChanged) { + SignResponse response; + response.mErrorCode.Construct( + static_cast<uint32_t>(ErrorCode::OTHER_ERROR)); + ExecuteCallback(response, callback); + return; + } + + // Otherwise, the user may well have clicked away, so let's + // abort the old transaction and take over control from here. + CancelTransaction(NS_ERROR_ABORT); + } + + // Evaluate the AppID + nsString adjustedAppId(aAppId); + if (!EvaluateAppID(mParent, mOrigin, adjustedAppId)) { + SignResponse response; + response.mErrorCode.Construct( + static_cast<uint32_t>(ErrorCode::BAD_REQUEST)); + ExecuteCallback(response, callback); + return; + } + + // Produce the AppParam from the current AppID + nsCString cAppId = NS_ConvertUTF16toUTF8(adjustedAppId); + + nsAutoString clientDataJSON; + nsresult rv = + AssembleClientData(mOrigin, kGetAssertion, aChallenge, clientDataJSON); + if (NS_WARN_IF(NS_FAILED(rv))) { + SignResponse response; + response.mErrorCode.Construct( + static_cast<uint32_t>(ErrorCode::BAD_REQUEST)); + ExecuteCallback(response, callback); + return; + } + + CryptoBuffer challenge; + if (!challenge.Assign(NS_ConvertUTF16toUTF8(aChallenge))) { + SignResponse response; + response.mErrorCode.Construct( + static_cast<uint32_t>(ErrorCode::OTHER_ERROR)); + ExecuteCallback(response, callback); + return; + } + + // Build the key list, if any + nsTArray<WebAuthnScopedCredential> permittedList; + RegisteredKeysToScopedCredentialList(adjustedAppId, aRegisteredKeys, + permittedList); + + if (!MaybeCreateBackgroundActor()) { + SignResponse response; + response.mErrorCode.Construct( + static_cast<uint32_t>(ErrorCode::OTHER_ERROR)); + ExecuteCallback(response, callback); + return; + } + +#ifdef OS_WIN + if (!WinWebAuthnManager::AreWebAuthNApisAvailable()) { + ListenForVisibilityEvents(); + } +#else + ListenForVisibilityEvents(); +#endif + + // Always blank for U2F + nsTArray<WebAuthnExtension> extensions; + + NS_ConvertUTF16toUTF8 clientData(clientDataJSON); + uint32_t adjustedTimeoutMillis = AdjustedTimeoutMillis(opt_aTimeoutSeconds); + + BrowsingContext* context = mParent->GetBrowsingContext(); + if (!context) { + SignResponse response; + response.mErrorCode.Construct( + static_cast<uint32_t>(ErrorCode::OTHER_ERROR)); + ExecuteCallback(response, callback); + return; + } + + WebAuthnGetAssertionInfo info(mOrigin, adjustedAppId, challenge, clientData, + adjustedTimeoutMillis, permittedList, + Nothing(), /* no extra info for U2F */ + context->Id()); + + MOZ_ASSERT(mTransaction.isNothing()); + mTransaction = Some(U2FTransaction(AsVariant(callback))); + mChild->SendRequestSign(mTransaction.ref().mId, info); +} + +// sign_impl_methodinfo is generated by bindings. +namespace U2F_Binding { +extern const JSJitInfo sign_impl_methodinfo; +} // namespace U2F_Binding + +// We have 4 non-optional args. +static const JSFunctionSpec sign_spec = + JS_FNSPEC("sign", (GenericMethod<NormalThisPolicy, ThrowExceptions>), + &U2F_Binding::sign_impl_methodinfo, 4, JSPROP_ENUMERATE, nullptr); + +void U2F::GetSign(JSContext* aCx, JS::MutableHandle<JSObject*> aSignFunc, + ErrorResult& aRv) { + JSFunction* fun = JS::NewFunctionFromSpec(aCx, &sign_spec); + if (!fun) { + aRv.NoteJSContextException(aCx); + return; + } + + aSignFunc.set(JS_GetFunctionObject(fun)); +} + +void U2F::FinishGetAssertion(const uint64_t& aTransactionId, + const WebAuthnGetAssertionResult& aResult) { + MOZ_ASSERT(NS_IsMainThread()); + + // Check for a valid transaction. + if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) { + return; + } + + if (NS_WARN_IF(!mTransaction.ref().HasSignCallback())) { + RejectTransaction(NS_ERROR_ABORT); + return; + } + + // A CTAP2 response. + if (aResult.SignatureData().Length() == 0) { + RejectTransaction(NS_ERROR_ABORT); + return; + } + + CryptoBuffer clientDataBuf; + if (NS_WARN_IF(!clientDataBuf.Assign(aResult.ClientDataJSON()))) { + RejectTransaction(NS_ERROR_ABORT); + return; + } + + CryptoBuffer credBuf; + if (NS_WARN_IF(!credBuf.Assign(aResult.KeyHandle()))) { + RejectTransaction(NS_ERROR_ABORT); + return; + } + + CryptoBuffer sigBuf; + if (NS_WARN_IF(!sigBuf.Assign(aResult.SignatureData()))) { + RejectTransaction(NS_ERROR_ABORT); + return; + } + + // Assemble a response object to return + nsString clientDataBase64; + nsString signatureDataBase64; + nsString keyHandleBase64; + nsresult rvClientData = clientDataBuf.ToJwkBase64(clientDataBase64); + nsresult rvSignatureData = sigBuf.ToJwkBase64(signatureDataBase64); + nsresult rvKeyHandle = credBuf.ToJwkBase64(keyHandleBase64); + if (NS_WARN_IF(NS_FAILED(rvClientData)) || + NS_WARN_IF(NS_FAILED(rvSignatureData) || + NS_WARN_IF(NS_FAILED(rvKeyHandle)))) { + RejectTransaction(NS_ERROR_ABORT); + return; + } + + SignResponse response; + response.mKeyHandle.Construct(keyHandleBase64); + response.mClientData.Construct(clientDataBase64); + response.mSignatureData.Construct(signatureDataBase64); + response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::OK)); + + // Keep the callback pointer alive. + nsMainThreadPtrHandle<U2FSignCallback> callback( + mTransaction.ref().GetSignCallback()); + + ClearTransaction(); + ExecuteCallback(response, callback); +} + +void U2F::ClearTransaction() { + if (!mTransaction.isNothing()) { + StopListeningForVisibilityEvents(); + } + + mTransaction.reset(); +} + +void U2F::RejectTransaction(const nsresult& aError) { + if (NS_WARN_IF(mTransaction.isNothing())) { + return; + } + + StopListeningForVisibilityEvents(); + + // Clear out mTransaction before calling ExecuteCallback() below to allow + // reentrancy from microtask checkpoints. + Maybe<U2FTransaction> maybeTransaction(std::move(mTransaction)); + MOZ_ASSERT(mTransaction.isNothing() && maybeTransaction.isSome()); + + U2FTransaction& transaction = maybeTransaction.ref(); + ErrorCode code = ConvertNSResultToErrorCode(aError); + + if (transaction.HasRegisterCallback()) { + RegisterResponse response; + response.mErrorCode.Construct(static_cast<uint32_t>(code)); + // MOZ_KnownLive because "transaction" lives on the stack. + ExecuteCallback(response, MOZ_KnownLive(transaction.GetRegisterCallback())); + } + + if (transaction.HasSignCallback()) { + SignResponse response; + response.mErrorCode.Construct(static_cast<uint32_t>(code)); + // MOZ_KnownLive because "transaction" lives on the stack. + ExecuteCallback(response, MOZ_KnownLive(transaction.GetSignCallback())); + } +} + +void U2F::CancelTransaction(const nsresult& aError) { + if (!NS_WARN_IF(!mChild || mTransaction.isNothing())) { + mChild->SendRequestCancel(mTransaction.ref().mId); + } + + RejectTransaction(aError); +} + +void U2F::RequestAborted(const uint64_t& aTransactionId, + const nsresult& aError) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mTransaction.isSome() && mTransaction.ref().mId == aTransactionId) { + RejectTransaction(aError); + } +} + +void U2F::HandleVisibilityChange() { + if (mTransaction.isSome()) { + mTransaction.ref().mVisibilityChanged = true; + } +} + +} // namespace mozilla::dom |