/* -*- 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 "nsContentUtils.h" #include "nsIEffectiveTLDService.h" #include "nsNetCID.h" #include "nsNetUtil.h" #include "nsURLParsers.h" #include "U2FManager.h" // 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 { namespace dom { static mozilla::LazyLogModule gU2FLog("u2fmanager"); NS_NAMED_LITERAL_STRING(kFinishEnrollment, "navigator.id.finishEnrollment"); NS_NAMED_LITERAL_STRING(kGetAssertion, "navigator.id.getAssertion"); NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(U2F) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(U2F) NS_IMPL_CYCLE_COLLECTING_RELEASE(U2F) NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(U2F, mParent) static uint32_t AdjustedTimeoutMillis(const Optional>& 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& aKeys, nsTArray& 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; } WebAuthnScopedCredentialDescriptor c; c.id() = keyHandle; aList.AppendElement(c); } } static ErrorCode EvaluateAppID(nsPIDOMWindowInner* aParent, const nsString& aOrigin, /* in/out */ nsString& aAppId) { // Facet is the specification's way of referring to the web origin. nsAutoCString facetString = NS_ConvertUTF16toUTF8(aOrigin); nsCOMPtr facetUri; if (NS_FAILED(NS_NewURI(getter_AddRefs(facetUri), facetString))) { return ErrorCode::BAD_REQUEST; } // If the facetId (origin) is not HTTPS, reject bool facetIsHttps = false; if (NS_FAILED(facetUri->SchemeIs("https", &facetIsHttps)) || !facetIsHttps) { return ErrorCode::BAD_REQUEST; } // If the appId is empty or null, overwrite it with the facetId and accept if (aAppId.IsEmpty() || aAppId.EqualsLiteral("null")) { aAppId.Assign(aOrigin); return ErrorCode::OK; } // AppID is user-supplied. It's quite possible for this parse to fail. nsAutoCString appIdString = NS_ConvertUTF16toUTF8(aAppId); nsCOMPtr appIdUri; if (NS_FAILED(NS_NewURI(getter_AddRefs(appIdUri), appIdString))) { return ErrorCode::BAD_REQUEST; } // if the appId URL is not HTTPS, reject. bool appIdIsHttps = false; if (NS_FAILED(appIdUri->SchemeIs("https", &appIdIsHttps)) || !appIdIsHttps) { return ErrorCode::BAD_REQUEST; } // Run the HTML5 algorithm to relax the same-origin policy, copied from W3C // Web Authentication. See Bug 1244959 comment #8 for context on why we are // doing this instead of implementing the external-fetch FacetID logic. nsCOMPtr document = aParent->GetDoc(); if (!document || !document->IsHTMLDocument()) { return ErrorCode::BAD_REQUEST; } nsHTMLDocument* html = document->AsHTMLDocument(); if (NS_WARN_IF(!html)) { return ErrorCode::BAD_REQUEST; } // Use the base domain as the facet for evaluation. This lets this algorithm // relax the whole eTLD+1. nsCOMPtr tldService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); if (!tldService) { return ErrorCode::BAD_REQUEST; } nsAutoCString lowestFacetHost; if (NS_FAILED(tldService->GetBaseDomain(facetUri, 0, lowestFacetHost))) { return ErrorCode::BAD_REQUEST; } nsAutoCString appIdHost; if (NS_FAILED(appIdUri->GetAsciiHost(appIdHost))) { return ErrorCode::BAD_REQUEST; } MOZ_LOG(gU2FLog, LogLevel::Debug, ("AppId %s Facet %s", appIdHost.get(), lowestFacetHost.get())); if (html->IsRegistrableDomainSuffixOfOrEqualTo(NS_ConvertUTF8toUTF16(lowestFacetHost), appIdHost)) { return ErrorCode::OK; } return ErrorCode::BAD_REQUEST; } template static void ExecuteCallback(T& aResp, Maybe>& aCb) { MOZ_ASSERT(NS_IsMainThread()); if (aCb.isNothing()) { return; } // reset callback earlier to allow reentry from callback. nsMainThreadPtrHandle callback(aCb.ref()); aCb.reset(); MOZ_ASSERT(aCb.isNothing()); MOZ_ASSERT(!!callback); ErrorResult error; callback->Call(aResp, error); NS_WARNING_ASSERTION(!error.Failed(), "dom::U2F::Promise callback failed"); error.SuppressException(); // Useful exceptions already emitted } U2F::U2F(nsPIDOMWindowInner* aParent) : mParent(aParent) { } U2F::~U2F() { mPromiseHolder.DisconnectIfExists(); mRegisterCallback.reset(); mSignCallback.reset(); } void U2F::Init(ErrorResult& aRv) { MOZ_ASSERT(mParent); MOZ_ASSERT(!mEventTarget); nsCOMPtr 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; } mEventTarget = doc->EventTargetFor(TaskCategory::Other); MOZ_ASSERT(mEventTarget); } /* virtual */ JSObject* U2F::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return U2FBinding::Wrap(aCx, this, aGivenProto); } void U2F::Register(const nsAString& aAppId, const Sequence& aRegisterRequests, const Sequence& aRegisteredKeys, U2FRegisterCallback& aCallback, const Optional>& opt_aTimeoutSeconds, ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); Cancel(); MOZ_ASSERT(mRegisterCallback.isNothing()); mRegisterCallback = Some(nsMainThreadPtrHandle( new nsMainThreadPtrHolder( "U2F::Register::callback", &aCallback))); uint32_t adjustedTimeoutMillis = AdjustedTimeoutMillis(opt_aTimeoutSeconds); // Evaluate the AppID nsString adjustedAppId; adjustedAppId.Assign(aAppId); ErrorCode appIdResult = EvaluateAppID(mParent, mOrigin, adjustedAppId); if (appIdResult != ErrorCode::OK) { RegisterResponse response; response.mErrorCode.Construct(static_cast(appIdResult)); ExecuteCallback(response, mRegisterCallback); return; } // Produce the AppParam from the current AppID nsCString cAppId = NS_ConvertUTF16toUTF8(adjustedAppId); nsAutoString clientDataJSON; // Pick the first valid RegisterRequest; we can only work with one. for (const RegisterRequest& req : aRegisterRequests) { if (!req.mChallenge.WasPassed() || !req.mVersion.WasPassed() || req.mVersion.Value() != kRequiredU2FVersion) { 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(ErrorCode::BAD_REQUEST)); ExecuteCallback(response, mRegisterCallback); return; } // Build the exclusion list, if any nsTArray excludeList; RegisteredKeysToScopedCredentialList(adjustedAppId, aRegisteredKeys, excludeList); auto& localReqHolder = mPromiseHolder; auto& localCb = mRegisterCallback; RefPtr mgr = U2FManager::GetOrCreate(); RefPtr p = mgr->Register(mParent, cAppId, NS_ConvertUTF16toUTF8(clientDataJSON), adjustedTimeoutMillis, excludeList); p->Then(mEventTarget, "dom::U2F::Register::Promise::Resolve", [&localCb, &localReqHolder](nsString aResponse) { MOZ_LOG(gU2FLog, LogLevel::Debug, ("dom::U2F::Register::Promise::Resolve, response was %s", NS_ConvertUTF16toUTF8(aResponse).get())); RegisterResponse response; response.Init(aResponse); // U2F could be reentered from microtask-checkpoint while calling // ExecuteCallback(), so we should mark Complete() earlier. localReqHolder.Complete(); ExecuteCallback(response, localCb); }, [&localCb, &localReqHolder](ErrorCode aErrorCode) { MOZ_LOG(gU2FLog, LogLevel::Debug, ("dom::U2F::Register::Promise::Reject, response was %d", static_cast(aErrorCode))); RegisterResponse response; response.mErrorCode.Construct(static_cast(aErrorCode)); // U2F could be reentered from microtask-checkpoint while calling // ExecuteCallback(), so we should mark Complete() earlier. localReqHolder.Complete(); ExecuteCallback(response, localCb); }) ->Track(mPromiseHolder); } void U2F::Sign(const nsAString& aAppId, const nsAString& aChallenge, const Sequence& aRegisteredKeys, U2FSignCallback& aCallback, const Optional>& opt_aTimeoutSeconds, ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); Cancel(); MOZ_ASSERT(mSignCallback.isNothing()); mSignCallback = Some(nsMainThreadPtrHandle( new nsMainThreadPtrHolder( "U2F::Sign::callback", &aCallback))); uint32_t adjustedTimeoutMillis = AdjustedTimeoutMillis(opt_aTimeoutSeconds); // Evaluate the AppID nsString adjustedAppId; adjustedAppId.Assign(aAppId); ErrorCode appIdResult = EvaluateAppID(mParent, mOrigin, adjustedAppId); if (appIdResult != ErrorCode::OK) { SignResponse response; response.mErrorCode.Construct(static_cast(appIdResult)); ExecuteCallback(response, mSignCallback); 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(ErrorCode::BAD_REQUEST)); ExecuteCallback(response, mSignCallback); return; } // Build the key list, if any nsTArray permittedList; RegisteredKeysToScopedCredentialList(adjustedAppId, aRegisteredKeys, permittedList); auto& localReqHolder = mPromiseHolder; auto& localCb = mSignCallback; RefPtr mgr = U2FManager::GetOrCreate(); RefPtr p = mgr->Sign(mParent, cAppId, NS_ConvertUTF16toUTF8(clientDataJSON), adjustedTimeoutMillis, permittedList); p->Then(mEventTarget, "dom::U2F::Sign::Promise::Resolve", [&localCb, &localReqHolder](nsString aResponse) { MOZ_LOG(gU2FLog, LogLevel::Debug, ("dom::U2F::Sign::Promise::Resolve, response was %s", NS_ConvertUTF16toUTF8(aResponse).get())); SignResponse response; response.Init(aResponse); // U2F could be reentered from microtask-checkpoint while calling // ExecuteCallback(), so we should mark Complete() earlier. localReqHolder.Complete(); ExecuteCallback(response, localCb); }, [&localCb, &localReqHolder](ErrorCode aErrorCode) { MOZ_LOG(gU2FLog, LogLevel::Debug, ("dom::U2F::Sign::Promise::Reject, response was %d", static_cast(aErrorCode))); SignResponse response; response.mErrorCode.Construct(static_cast(aErrorCode)); // U2F could be reentered from microtask-checkpoint while calling // ExecuteCallback(), so we should mark Complete() earlier. localReqHolder.Complete(); ExecuteCallback(response, localCb); }) ->Track(mPromiseHolder); } void U2F::Cancel() { MOZ_ASSERT(NS_IsMainThread()); const ErrorCode errorCode = ErrorCode::OTHER_ERROR; if (mRegisterCallback.isSome()) { RegisterResponse response; response.mErrorCode.Construct(static_cast(errorCode)); ExecuteCallback(response, mRegisterCallback); } if (mSignCallback.isSome()) { SignResponse response; response.mErrorCode.Construct(static_cast(errorCode)); ExecuteCallback(response, mSignCallback); } RefPtr mgr = U2FManager::Get(); if (mgr) { mgr->MaybeCancelTransaction(NS_ERROR_DOM_OPERATION_ERR); } mPromiseHolder.DisconnectIfExists(); } } // namespace dom } // namespace mozilla