From 0a0369ff19f548fe7314c7e4d34f751020a0cc30 Mon Sep 17 00:00:00 2001 From: John Schanck Date: Thu, 16 Mar 2023 21:42:21 +0000 Subject: [PATCH] Bug 1813982 - XPCOM interface to authenticator-rs. r=keeler,geckoview-reviewers,m_kato Differential Revision: https://phabricator.services.mozilla.com/D171269 --- Cargo.lock | 16 +- browser/base/content/browser.js | 10 +- dom/webauthn/AndroidWebAuthnTokenManager.cpp | 6 +- dom/webauthn/AndroidWebAuthnTokenManager.h | 7 +- dom/webauthn/AuthrsTransport.cpp | 29 + dom/webauthn/AuthrsTransport.h | 17 + dom/webauthn/CTAPHIDTokenManager.cpp | 662 ------------ dom/webauthn/CTAPHIDTokenManager.h | 391 ------- dom/webauthn/CtapArgs.cpp | 302 ++++++ dom/webauthn/CtapArgs.h | 58 + dom/webauthn/CtapResults.cpp | 88 ++ dom/webauthn/CtapResults.h | 67 ++ dom/webauthn/U2FHIDTokenManager.cpp | 6 +- dom/webauthn/U2FHIDTokenManager.h | 7 +- dom/webauthn/U2FSoftTokenManager.cpp | 174 ++- dom/webauthn/U2FSoftTokenManager.h | 13 +- dom/webauthn/U2FSoftTokenTransport.cpp | 1018 ++++++++++++++++++ dom/webauthn/U2FSoftTokenTransport.h | 53 + dom/webauthn/U2FTokenManager.cpp | 293 +---- dom/webauthn/U2FTokenManager.h | 18 - dom/webauthn/U2FTokenTransport.h | 8 +- dom/webauthn/WebAuthnController.cpp | 745 +++++++++++++ dom/webauthn/WebAuthnController.h | 130 +++ dom/webauthn/WebAuthnManager.cpp | 9 +- dom/webauthn/WebAuthnTransactionParent.cpp | 79 +- dom/webauthn/WebAuthnTransportIdentifiers.h | 15 + dom/webauthn/WinWebAuthnManager.cpp | 17 +- dom/webauthn/authrs_bridge/Cargo.toml | 15 + dom/webauthn/authrs_bridge/src/lib.rs | 749 +++++++++++++ dom/webauthn/moz.build | 10 +- dom/webauthn/nsIU2FTokenManager.idl | 29 +- dom/webauthn/nsIWebAuthnController.idl | 219 ++++ layout/build/nsLayoutStatics.cpp | 2 + mobile/android/app/mobile.js | 3 - modules/libpref/init/StaticPrefList.yaml | 34 +- modules/libpref/init/all.js | 13 - toolkit/library/rust/shared/Cargo.toml | 2 +- toolkit/library/rust/shared/lib.rs | 2 +- 38 files changed, 3783 insertions(+), 1533 deletions(-) create mode 100644 dom/webauthn/AuthrsTransport.cpp create mode 100644 dom/webauthn/AuthrsTransport.h delete mode 100644 dom/webauthn/CTAPHIDTokenManager.cpp delete mode 100644 dom/webauthn/CTAPHIDTokenManager.h create mode 100644 dom/webauthn/CtapArgs.cpp create mode 100644 dom/webauthn/CtapArgs.h create mode 100644 dom/webauthn/CtapResults.cpp create mode 100644 dom/webauthn/CtapResults.h create mode 100644 dom/webauthn/U2FSoftTokenTransport.cpp create mode 100644 dom/webauthn/U2FSoftTokenTransport.h create mode 100644 dom/webauthn/WebAuthnController.cpp create mode 100644 dom/webauthn/WebAuthnController.h create mode 100644 dom/webauthn/WebAuthnTransportIdentifiers.h create mode 100644 dom/webauthn/authrs_bridge/Cargo.toml create mode 100644 dom/webauthn/authrs_bridge/src/lib.rs create mode 100644 dom/webauthn/nsIWebAuthnController.idl diff --git a/Cargo.lock b/Cargo.lock index 2af81dd205400..9d0979ae78e2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -405,6 +405,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "authrs_bridge" +version = "0.1.0" +dependencies = [ + "authenticator", + "log", + "moz_task", + "nserror", + "nsstring", + "serde_cbor", + "thin-vec", + "xpcom", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -2195,7 +2209,7 @@ dependencies = [ "audioipc-server", "audioipc2-client", "audioipc2-server", - "authenticator", + "authrs_bridge", "binary_http", "bitsdownload", "bookmark_sync", diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 07a61811b442e..1c25719233010 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -7550,7 +7550,6 @@ var WebAuthnPromptHelper = { }, observe(aSubject, aTopic, aData) { - let mgr = aSubject.QueryInterface(Ci.nsIU2FTokenManager); let data = JSON.parse(aData); // If we receive a cancel, it might be a WebAuthn prompt starting in another @@ -7558,6 +7557,7 @@ var WebAuthnPromptHelper = { // cancellations, so any cancel action we get should prompt us to cancel. if (data.action == "cancel") { this.cancel(data); + return; } if ( @@ -7567,6 +7567,10 @@ var WebAuthnPromptHelper = { return; } + let mgr = aSubject.QueryInterface( + data.is_ctap2 ? Ci.nsIWebAuthnController : Ci.nsIU2FTokenManager + ); + if (data.action == "register") { this.register(mgr, data); } else if (data.action == "register-direct") { @@ -7631,7 +7635,7 @@ var WebAuthnPromptHelper = { label: unescape(decodeURIComponent(usernames[i])), accessKey: i.toString(), callback(aState) { - mgr.resumeWithSelectedSignResult(tid, i); + mgr.signatureSelectionCallback(tid, i); }, }); } @@ -7657,7 +7661,7 @@ var WebAuthnPromptHelper = { aPassword ); if (res) { - mgr.pinCallback(aPassword.value); + mgr.pinCallback(tid, aPassword.value); } else { mgr.cancel(tid); } diff --git a/dom/webauthn/AndroidWebAuthnTokenManager.cpp b/dom/webauthn/AndroidWebAuthnTokenManager.cpp index 7c465104c26e9..43a9fcf63c0da 100644 --- a/dom/webauthn/AndroidWebAuthnTokenManager.cpp +++ b/dom/webauthn/AndroidWebAuthnTokenManager.cpp @@ -88,8 +88,7 @@ void AndroidWebAuthnTokenManager::Drop() { } RefPtr AndroidWebAuthnTokenManager::Register( - const WebAuthnMakeCredentialInfo& aInfo, bool aForceNoneAttestation, - void _status_callback(rust_ctap2_status_update_res*)) { + const WebAuthnMakeCredentialInfo& aInfo, bool aForceNoneAttestation) { AssertIsOnOwningThread(); ClearPromises(); @@ -276,8 +275,7 @@ void AndroidWebAuthnTokenManager::HandleRegisterResult( } RefPtr AndroidWebAuthnTokenManager::Sign( - const WebAuthnGetAssertionInfo& aInfo, - void _status_callback(rust_ctap2_status_update_res*)) { + const WebAuthnGetAssertionInfo& aInfo) { AssertIsOnOwningThread(); ClearPromises(); diff --git a/dom/webauthn/AndroidWebAuthnTokenManager.h b/dom/webauthn/AndroidWebAuthnTokenManager.h index f277a0c05bb1e..10620b1353f60 100644 --- a/dom/webauthn/AndroidWebAuthnTokenManager.h +++ b/dom/webauthn/AndroidWebAuthnTokenManager.h @@ -109,12 +109,11 @@ class AndroidWebAuthnTokenManager final : public U2FTokenTransport { ~AndroidWebAuthnTokenManager() {} virtual RefPtr Register( - const WebAuthnMakeCredentialInfo& aInfo, bool aForceNoneAttestation, - void status_callback(rust_ctap2_status_update_res*)) override; + const WebAuthnMakeCredentialInfo& aInfo, + bool aForceNoneAttestation) override; virtual RefPtr Sign( - const WebAuthnGetAssertionInfo& aInfo, - void status_callback(rust_ctap2_status_update_res*)) override; + const WebAuthnGetAssertionInfo& aInfo) override; void Cancel() override; diff --git a/dom/webauthn/AuthrsTransport.cpp b/dom/webauthn/AuthrsTransport.cpp new file mode 100644 index 0000000000000..699cd600eb964 --- /dev/null +++ b/dom/webauthn/AuthrsTransport.cpp @@ -0,0 +1,29 @@ +/* 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 "AuthrsTransport.h" +#include "nsIWebAuthnController.h" +#include "nsCOMPtr.h" + +namespace { +extern "C" { + +// Implemented in Rust +nsresult authrs_transport_constructor(nsIWebAuthnTransport** result); + +} // extern "C" +} // namespace + +namespace mozilla::dom { + +already_AddRefed NewAuthrsTransport() { + nsCOMPtr transport; + nsresult rv = authrs_transport_constructor(getter_AddRefs(transport)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + return transport.forget(); +} + +} // namespace mozilla::dom diff --git a/dom/webauthn/AuthrsTransport.h b/dom/webauthn/AuthrsTransport.h new file mode 100644 index 0000000000000..234fa2294c983 --- /dev/null +++ b/dom/webauthn/AuthrsTransport.h @@ -0,0 +1,17 @@ +/* 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/. */ + +#ifndef DOM_WEBAUTHN_AUTHRS_BRIDGE_H_ +#define DOM_WEBAUTHN_AUTHRS_BRIDGE_H_ + +#include "mozilla/AlreadyAddRefed.h" +#include "nsIWebAuthnController.h" + +namespace mozilla::dom { + +already_AddRefed NewAuthrsTransport(); + +} // namespace mozilla::dom + +#endif // DOM_WEBAUTHN_AUTHRS_BRIDGE_H_ diff --git a/dom/webauthn/CTAPHIDTokenManager.cpp b/dom/webauthn/CTAPHIDTokenManager.cpp deleted file mode 100644 index 10451622436a9..0000000000000 --- a/dom/webauthn/CTAPHIDTokenManager.cpp +++ /dev/null @@ -1,662 +0,0 @@ -/* -*- 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 "WebAuthnCoseIdentifiers.h" -#include "WebAuthnEnumStrings.h" -#include "mozilla/dom/CTAPHIDTokenManager.h" -#include "mozilla/dom/U2FHIDTokenManager.h" -#include "mozilla/dom/WebAuthnUtil.h" -#include "mozilla/dom/WebAuthnCBORUtil.h" -#include "mozilla/ipc/BackgroundParent.h" -#include "mozilla/StaticMutex.h" -#include -namespace mozilla::dom { - -static StaticMutex gCTAPMutex; -static CTAPHIDTokenManager* gCTAPInstance; -static nsIThread* gPCTAPBackgroundThread; - -static void ctap1_register_callback(uint64_t aTransactionId, - rust_u2f_result* aResult) { - UniquePtr rv = MakeUnique(aTransactionId, aResult); - - StaticMutexAutoLock lock(gCTAPMutex); - if (!gCTAPInstance || NS_WARN_IF(!gPCTAPBackgroundThread)) { - return; - } - - nsCOMPtr r(NewRunnableMethod&&>( - "CTAPHIDTokenManager::HandleRegisterResult", gCTAPInstance, - &CTAPHIDTokenManager::HandleRegisterResult, std::move(rv))); - - MOZ_ALWAYS_SUCCEEDS( - gPCTAPBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL)); -} - -static void ctap2_register_callback(uint64_t aTransactionId, - rust_ctap2_register_result* aResult) { - UniquePtr rv = MakeUnique(aTransactionId, aResult); - - StaticMutexAutoLock lock(gCTAPMutex); - if (!gCTAPInstance || NS_WARN_IF(!gPCTAPBackgroundThread)) { - return; - } - - nsCOMPtr r(NewRunnableMethod&&>( - "CTAPHIDTokenManager::HandleRegisterResult", gCTAPInstance, - &CTAPHIDTokenManager::HandleRegisterResult, std::move(rv))); - - MOZ_ALWAYS_SUCCEEDS( - gPCTAPBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL)); -} - -static void ctap1_sign_callback(uint64_t aTransactionId, - rust_u2f_result* aResult) { - UniquePtr rv = MakeUnique(aTransactionId, aResult); - - StaticMutexAutoLock lock(gCTAPMutex); - if (!gCTAPInstance || NS_WARN_IF(!gPCTAPBackgroundThread)) { - return; - } - - nsCOMPtr r(NewRunnableMethod&&>( - "CTAPHIDTokenManager::HandleSignResult", gCTAPInstance, - &CTAPHIDTokenManager::HandleSignResult, std::move(rv))); - - MOZ_ALWAYS_SUCCEEDS( - gPCTAPBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL)); -} - -static void ctap2_sign_callback(uint64_t aTransactionId, - rust_ctap2_sign_result* aResult) { - UniquePtr rv = MakeUnique(aTransactionId, aResult); - - StaticMutexAutoLock lock(gCTAPMutex); - if (!gCTAPInstance || NS_WARN_IF(!gPCTAPBackgroundThread)) { - return; - } - - nsCOMPtr r(NewRunnableMethod&&>( - "CTAPHIDTokenManager::HandleSignResult", gCTAPInstance, - &CTAPHIDTokenManager::HandleSignResult, std::move(rv))); - - MOZ_ALWAYS_SUCCEEDS( - gPCTAPBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL)); -} - -CTAPHIDTokenManager::CTAPHIDTokenManager() { - StaticMutexAutoLock lock(gCTAPMutex); - mozilla::ipc::AssertIsOnBackgroundThread(); - MOZ_ASSERT(XRE_IsParentProcess()); - MOZ_ASSERT(!gCTAPInstance); - - mCTAPManager = rust_ctap2_mgr_new(); - gPCTAPBackgroundThread = NS_GetCurrentThread(); - MOZ_ASSERT(gPCTAPBackgroundThread, "This should never be null!"); - gCTAPInstance = this; -} - -void CTAPHIDTokenManager::Drop() { - { - StaticMutexAutoLock lock(gCTAPMutex); - mozilla::ipc::AssertIsOnBackgroundThread(); - - mRegisterPromise.RejectIfExists(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - mSignPromise.RejectIfExists(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - - gCTAPInstance = nullptr; - } - - // Release gCTAPMutex before we call CTAPManager::drop(). It will wait - // for the work queue thread to join, and that requires the - // u2f_{register,sign}_callback to lock and return. - rust_ctap2_mgr_free(mCTAPManager); - mCTAPManager = nullptr; - - // Reset transaction ID so that queued runnables exit early. - mTransaction.reset(); -} - -// A CTAP Register operation causes a new key pair to be generated by the token. -// The token then returns the public key of the key pair, and a handle to the -// private key, which is a fancy way of saying "key wrapped private key", as -// well as the generated attestation certificate and a signature using that -// certificate's private key. -// Requests can be either CTAP1 or CTAP2, those will be packaged differently -// and handed over to the Rust lib. -RefPtr CTAPHIDTokenManager::Register( - const WebAuthnMakeCredentialInfo& aInfo, bool aForceNoneAttestation, - void status_callback(rust_ctap2_status_update_res*)) { - mozilla::ipc::AssertIsOnBackgroundThread(); - - uint64_t registerFlags = 0; - bool is_ctap2_request = false; - const uint8_t* user_id = nullptr; - size_t user_id_len = 0; - nsCString user_name; - - if (aInfo.Extra().isSome()) { - const auto& extra = aInfo.Extra().ref(); - const WebAuthnAuthenticatorSelection& sel = extra.AuthenticatorSelection(); - - bool requireUserVerification = - sel.userVerificationRequirement().EqualsLiteral( - MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED); - - bool requirePlatformAttachment = false; - if (sel.authenticatorAttachment().isSome()) { - const nsString& authenticatorAttachment = - sel.authenticatorAttachment().value(); - if (authenticatorAttachment.EqualsLiteral( - MOZ_WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM)) { - requirePlatformAttachment = true; - } - } - - // Set flags for credential creation. - if (sel.requireResidentKey()) { - registerFlags |= U2F_FLAG_REQUIRE_RESIDENT_KEY; - } - if (requireUserVerification) { - registerFlags |= U2F_FLAG_REQUIRE_USER_VERIFICATION; - } - if (requirePlatformAttachment) { - registerFlags |= U2F_FLAG_REQUIRE_PLATFORM_ATTACHMENT; - } - - nsTArray coseAlgos; - for (const auto& coseAlg : extra.coseAlgs()) { - switch (static_cast(coseAlg.alg())) { - case CoseAlgorithmIdentifier::ES256: - coseAlgos.AppendElement(coseAlg); - break; - default: - continue; - } - } - - // Only if no algorithms were specified, default to the only CTAP 1 / U2F - // protocol-supported algorithm. Ultimately this logic must move into - // u2f-hid-rs in a fashion that doesn't break the tests. - if (extra.coseAlgs().IsEmpty()) { - coseAlgos.AppendElement( - static_cast(CoseAlgorithmIdentifier::ES256)); - } - - // If there are no acceptable/supported algorithms, reject the promise. - if (coseAlgos.IsEmpty()) { - return U2FRegisterPromise::CreateAndReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR, - __func__); - } - - user_id_len = extra.User().Id().Length(); - user_id = extra.User().Id().Elements(); - user_name = NS_ConvertUTF16toUTF8(extra.User().DisplayName()); - is_ctap2_request = true; - } - - CryptoBuffer rpIdHash, clientDataHash; - NS_ConvertUTF16toUTF8 rpId(aInfo.RpId()); - nsresult rv = BuildTransactionHashes(rpId, aInfo.ClientDataJSON(), rpIdHash, - clientDataHash); - if (NS_WARN_IF(NS_FAILED(rv))) { - return U2FRegisterPromise::CreateAndReject(NS_ERROR_DOM_UNKNOWN_ERR, - __func__); - } - - ClearPromises(); - mTransaction.reset(); - - const int32_t pub_cred_params = (int32_t) - CoseAlgorithmIdentifier::ES256; // Currently the only supported one - uint64_t tid; - if (is_ctap2_request) { - AuthenticatorArgsUser user = {user_id, user_id_len, user_name.get()}; - AuthenticatorArgsPubCred pub_cred = {&pub_cred_params, 1}; - AuthenticatorArgsChallenge challenge = {aInfo.Challenge().Elements(), - aInfo.Challenge().Length()}; - AuthenticatorArgsOptions options = { - static_cast(registerFlags & U2F_FLAG_REQUIRE_RESIDENT_KEY), - static_cast(registerFlags & U2F_FLAG_REQUIRE_USER_VERIFICATION), - true, // user presence - aForceNoneAttestation}; - tid = rust_ctap2_mgr_register( - mCTAPManager, (uint64_t)aInfo.TimeoutMS(), ctap2_register_callback, - status_callback, challenge, rpId.get(), - NS_ConvertUTF16toUTF8(aInfo.Origin()).get(), user, pub_cred, - Ctap2PubKeyCredentialDescriptor(aInfo.ExcludeList()).Get(), options, - nullptr); - } else { - tid = rust_u2f_mgr_register( - mCTAPManager, registerFlags, (uint64_t)aInfo.TimeoutMS(), - ctap1_register_callback, clientDataHash.Elements(), - clientDataHash.Length(), rpIdHash.Elements(), rpIdHash.Length(), - U2FKeyHandles(aInfo.ExcludeList()).Get()); - } - if (tid == 0) { - return U2FRegisterPromise::CreateAndReject(NS_ERROR_DOM_UNKNOWN_ERR, - __func__); - } - - mTransaction = Some(Transaction( - tid, rpIdHash, Nothing(), aInfo.ClientDataJSON(), aForceNoneAttestation)); - - return mRegisterPromise.Ensure(__func__); -} - -// Signing into a webpage. Again, depending on if the request is CTAP1 or -// CTAP2, it will be packaged differently and passed to the Rust lib. -RefPtr CTAPHIDTokenManager::Sign( - const WebAuthnGetAssertionInfo& aInfo, - void status_callback(rust_ctap2_status_update_res*)) { - mozilla::ipc::AssertIsOnBackgroundThread(); - - bool is_ctap2_request = false; - CryptoBuffer rpIdHash, clientDataHash; - NS_ConvertUTF16toUTF8 rpId(aInfo.RpId()); - nsresult rv = BuildTransactionHashes(rpId, aInfo.ClientDataJSON(), rpIdHash, - clientDataHash); - if (NS_WARN_IF(NS_FAILED(rv))) { - return U2FSignPromise::CreateAndReject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - } - - uint64_t signFlags = 0; - nsTArray> appIds; - appIds.AppendElement(rpIdHash.InfallibleClone()); - - Maybe> appIdHashExt = Nothing(); - nsCString appId; - - if (aInfo.Extra().isSome()) { - const auto& extra = aInfo.Extra().ref(); - - // Set flags for credential requests. - if (extra.userVerificationRequirement().EqualsLiteral( - MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED)) { - signFlags |= U2F_FLAG_REQUIRE_USER_VERIFICATION; - } - - // Process extensions. - for (const WebAuthnExtension& ext : extra.Extensions()) { - if (ext.type() == WebAuthnExtension::TWebAuthnExtensionAppId) { - appId = NS_ConvertUTF16toUTF8( - ext.get_WebAuthnExtensionAppId().appIdentifier()); - appIdHashExt = Some(ext.get_WebAuthnExtensionAppId().AppId().Clone()); - appIds.AppendElement(appIdHashExt->Clone()); - } - } - - is_ctap2_request = true; - } - - ClearPromises(); - mTransaction.reset(); - uint64_t tid; - if (is_ctap2_request) { - AuthenticatorArgsChallenge challenge = {aInfo.Challenge().Elements(), - aInfo.Challenge().Length()}; - AuthenticatorArgsOptions options = { - false, // resident key, not used when signing - static_cast(signFlags & U2F_FLAG_REQUIRE_USER_VERIFICATION), - true, // user presence - }; - tid = rust_ctap2_mgr_sign( - mCTAPManager, (uint64_t)aInfo.TimeoutMS(), ctap2_sign_callback, - status_callback, challenge, rpId.get(), appId.get(), - NS_ConvertUTF16toUTF8(aInfo.Origin()).get(), - Ctap2PubKeyCredentialDescriptor(aInfo.AllowList()).Get(), options, - nullptr); - } else { - tid = rust_u2f_mgr_sign( - mCTAPManager, signFlags, (uint64_t)aInfo.TimeoutMS(), - ctap1_sign_callback, clientDataHash.Elements(), clientDataHash.Length(), - U2FAppIds(appIds).Get(), U2FKeyHandles(aInfo.AllowList()).Get()); - } - if (tid == 0) { - return U2FSignPromise::CreateAndReject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - } - - mTransaction = Some(Transaction(tid, std::move(rpIdHash), appIdHashExt, - aInfo.ClientDataJSON())); - - return mSignPromise.Ensure(__func__); -} - -void CTAPHIDTokenManager::Cancel() { - mozilla::ipc::AssertIsOnBackgroundThread(); - - ClearPromises(); - rust_u2f_mgr_cancel(mCTAPManager); - mTransaction.reset(); -} - -void CTAPHIDTokenManager::HandleRegisterResult( - UniquePtr&& aResult) { - mozilla::ipc::AssertIsOnBackgroundThread(); - - if (mTransaction.isNothing() || - aResult->GetTransactionId() != mTransaction.ref().mId) { - return; - } - - MOZ_ASSERT(!mRegisterPromise.IsEmpty()); - - if (aResult->IsError()) { - mRegisterPromise.Reject(aResult->GetError(), __func__); - return; - } - - if (aResult->IsCtap2()) { - HandleRegisterResultCtap2(std::move(aResult)); - } else { - HandleRegisterResultCtap1(std::move(aResult)); - } -} - -void CTAPHIDTokenManager::HandleRegisterResultCtap1( - UniquePtr&& aResult) { - CryptoBuffer regData; - CryptoBuffer pubKeyBuf; - CryptoBuffer keyHandle; - CryptoBuffer attestationCertBuf; - CryptoBuffer signatureBuf; - - nsTArray registration; - if (!aResult->CopyRegistration(registration)) { - mRegisterPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return; - } - // Decompose the U2F registration packet - - regData.Assign(registration); - - // Only handles attestation cert chains of length=1. - nsresult rv = U2FDecomposeRegistrationResponse( - regData, pubKeyBuf, keyHandle, attestationCertBuf, signatureBuf); - if (NS_WARN_IF(NS_FAILED(rv))) { - mRegisterPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return; - } - - CryptoBuffer rpIdHashBuf; - if (!rpIdHashBuf.Assign(mTransaction.ref().mRpIdHash)) { - mRegisterPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return; - } - - CryptoBuffer attObj; - rv = AssembleAttestationObject( - rpIdHashBuf, pubKeyBuf, keyHandle, attestationCertBuf, signatureBuf, - mTransaction.ref().mForceNoneAttestation, attObj); - if (NS_FAILED(rv)) { - mRegisterPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return; - } - - nsTArray extensions; - WebAuthnMakeCredentialResult result(mTransaction.ref().mClientDataJSON, - attObj, keyHandle, regData, extensions); - mRegisterPromise.Resolve(std::move(result), __func__); -} - -void CTAPHIDTokenManager::HandleRegisterResultCtap2( - UniquePtr&& aResult) { - CryptoBuffer attObj; - - nsTArray attestation; - if (!aResult->Ctap2CopyAttestation(attestation)) { - mRegisterPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return; - } - - if (!attObj.Assign(attestation)) { - mRegisterPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return; - } - - nsTArray credentialId; - if (!aResult->Ctap2CopyCredentialId(credentialId)) { - mRegisterPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return; - } - - CryptoBuffer keyHandle; - if (!keyHandle.Assign(credentialId)) { - mRegisterPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return; - } - - // We would have a copy of the client data stored inside mTransaction, - // but we need the one from authenticator-rs, as that data is part of - // the signed payload. If we reorder the JSON-values (e.g. by sorting the - // members alphabetically, as the codegen from IDL does, so we can't use - // that), that would break the signature and lead to a failed authentication - // on the server. So we make sure to take exactly the client data that - // authenticator-rs sent to the token. - nsCString clientData; - if (!aResult->CopyClientDataStr(clientData)) { - mRegisterPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return; - } - - // Dummy-values. Not used with CTAP2. - nsTArray extensions; - CryptoBuffer regData; - WebAuthnMakeCredentialResult result(clientData, attObj, keyHandle, regData, - extensions); - mRegisterPromise.Resolve(std::move(result), __func__); -} - -void CTAPHIDTokenManager::HandleSignResult(UniquePtr&& aResult) { - mozilla::ipc::AssertIsOnBackgroundThread(); - - if (mTransaction.isNothing() || - aResult->GetTransactionId() != mTransaction.ref().mId) { - return; - } - - MOZ_ASSERT(!mSignPromise.IsEmpty()); - - if (aResult->IsError()) { - mSignPromise.Reject(aResult->GetError(), __func__); - return; - } - - if (aResult->IsCtap2()) { - HandleSignResultCtap2(std::move(aResult)); - } else { - HandleSignResultCtap1(std::move(aResult)); - } -} - -void CTAPHIDTokenManager::HandleSignResultCtap1( - UniquePtr&& aResult) { - nsTArray hashChosenByAuthenticator; - if (!aResult->CopyAppId(hashChosenByAuthenticator)) { - mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return; - } - - nsTArray keyHandle; - if (!aResult->CopyKeyHandle(keyHandle)) { - mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return; - } - - nsTArray signature; - if (!aResult->CopySignature(signature)) { - mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return; - } - - CryptoBuffer rawSignatureBuf; - if (!rawSignatureBuf.Assign(signature)) { - mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return; - } - - nsTArray extensions; - - if (mTransaction.ref().mAppIdHash.isSome()) { - bool usedAppId = - (hashChosenByAuthenticator == mTransaction.ref().mAppIdHash.ref()); - extensions.AppendElement(WebAuthnExtensionResultAppId(usedAppId)); - } - - CryptoBuffer signatureBuf; - CryptoBuffer counterBuf; - uint8_t flags = 0; - nsresult rv = U2FDecomposeSignResponse(rawSignatureBuf, flags, counterBuf, - signatureBuf); - if (NS_WARN_IF(NS_FAILED(rv))) { - mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return; - } - - CryptoBuffer chosenAppIdBuf; - if (!chosenAppIdBuf.Assign(hashChosenByAuthenticator)) { - mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return; - } - - // Preserve the two LSBs of the flags byte, UP and RFU1. - // See - flags &= 0b11; - - CryptoBuffer emptyAttestationData; - CryptoBuffer authenticatorData; - rv = AssembleAuthenticatorData(chosenAppIdBuf, flags, counterBuf, - emptyAttestationData, authenticatorData); - if (NS_WARN_IF(NS_FAILED(rv))) { - mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return; - } - - nsTArray userHandle; - - WebAuthnGetAssertionResult result(mTransaction.ref().mClientDataJSON, - keyHandle, signatureBuf, authenticatorData, - extensions, rawSignatureBuf, userHandle); - nsTArray results = { - {result, mozilla::Nothing()}}; - mSignPromise.Resolve(std::move(results), __func__); -} - -void CTAPHIDTokenManager::HandleSignResultCtap2( - UniquePtr&& aResult) { - // Have choice here. For discoverable creds, the token - // can return multiple assertions. The user has to choose - // into which account we should sign in. We are getting - // all of them from auth-rs, let the user select one and send - // that back to the server - size_t num_of_results; - if (!aResult->Ctap2GetNumberOfSignAssertions(num_of_results)) { - mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return; - } - nsTArray results; - for (size_t idx = 0; idx < num_of_results; ++idx) { - auto assertion = HandleSelectedSignResultCtap2( - std::forward>(aResult), idx); - if (assertion.isNothing()) { - return; - } - results.AppendElement(assertion.extract()); - } - if (results.IsEmpty()) { - mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - } else { - mSignPromise.Resolve(std::move(results), __func__); - } -} - -mozilla::Maybe -CTAPHIDTokenManager::HandleSelectedSignResultCtap2( - UniquePtr&& aResult, size_t index) { - nsTArray signature; - if (!aResult->Ctap2CopySignature(signature, index)) { - mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return mozilla::Nothing(); - } - - CryptoBuffer signatureBuf; - if (!signatureBuf.Assign(signature)) { - mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return mozilla::Nothing(); - } - - nsTArray cred; - CryptoBuffer pubKeyCred; - if (aResult->Ctap2HasPubKeyCredential(index)) { - if (!aResult->Ctap2CopyPubKeyCredential(cred, index)) { - mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return mozilla::Nothing(); - } - if (!pubKeyCred.Assign(cred)) { - mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return mozilla::Nothing(); - } - } - - nsTArray auth; - if (!aResult->Ctap2CopyAuthData(auth, index)) { - mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return mozilla::Nothing(); - } - - CryptoBuffer authData; - if (!authData.Assign(auth)) { - mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return mozilla::Nothing(); - } - - nsTArray userID; - if (aResult->HasUserId(index)) { - if (!aResult->Ctap2CopyUserId(userID, - index)) { // Misusing AppId for User-handle - mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return mozilla::Nothing(); - } - } - - // We would have a copy of the client data stored inside mTransaction, - // but we need the one from authenticator-rs, as that data is part of - // the signed payload. If we reorder the JSON-values (e.g. by sorting the - // members alphabetically, as the codegen from IDL does, so we can't use - // that), that would break the signature and lead to a failed authentication - // on the server. So we make sure to take exactly the client data that - // authenticator-rs sent to the token. - nsCString clientData; - if (!aResult->CopyClientDataStr(clientData)) { - mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return mozilla::Nothing(); - } - - nsTArray effectiveRpIdHash; - if (!aResult->Ctap2CopyRpIdHash(effectiveRpIdHash, index)) { - mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - return mozilla::Nothing(); - } - - nsTArray extensions; - if (mTransaction.ref().mAppIdHash.isSome()) { - bool usedAppId = (effectiveRpIdHash == mTransaction.ref().mAppIdHash.ref()); - extensions.AppendElement(WebAuthnExtensionResultAppId(usedAppId)); - } - - WebAuthnGetAssertionResult assertion(clientData, pubKeyCred, signatureBuf, - authData, extensions, signature, userID); - mozilla::Maybe username; - nsCString name; - if (aResult->CopyUserName(name, index)) { - username = Some(name); - } - - WebAuthnGetAssertionResultWrapper result = {assertion, username}; - return mozilla::Some(result); -} - -} // namespace mozilla::dom diff --git a/dom/webauthn/CTAPHIDTokenManager.h b/dom/webauthn/CTAPHIDTokenManager.h deleted file mode 100644 index 90a1d2fb1076c..0000000000000 --- a/dom/webauthn/CTAPHIDTokenManager.h +++ /dev/null @@ -1,391 +0,0 @@ -/* -*- 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/. */ - -#ifndef mozilla_dom_CTAPHIDTokenManager_h -#define mozilla_dom_CTAPHIDTokenManager_h - -#include "mozilla/dom/U2FTokenTransport.h" -#include "authenticator/src/u2fhid-capi.h" -#include "authenticator/src/ctap2-capi.h" - -/* - * CTAPHIDTokenManager is a Rust implementation of a secure token manager - * for the CTAP2, U2F and WebAuthn APIs, talking to HIDs. - */ - -namespace mozilla::dom { - -class Ctap2PubKeyCredentialDescriptor { - public: - explicit Ctap2PubKeyCredentialDescriptor( - const nsTArray& aCredentials) { - cred_descriptors = rust_ctap2_pkcd_new(); - - for (auto& cred : aCredentials) { - rust_ctap2_pkcd_add(cred_descriptors, cred.id().Elements(), - cred.id().Length(), cred.transports()); - } - } - - rust_ctap2_pub_key_cred_descriptors* Get() { return cred_descriptors; } - - ~Ctap2PubKeyCredentialDescriptor() { rust_ctap2_pkcd_free(cred_descriptors); } - - private: - rust_ctap2_pub_key_cred_descriptors* cred_descriptors; -}; - -class CTAPResult { - public: - explicit CTAPResult(uint64_t aTransactionId, rust_u2f_result* aResult) - : mTransactionId(aTransactionId), mU2FResult(aResult) { - MOZ_ASSERT(mU2FResult); - } - - explicit CTAPResult(uint64_t aTransactionId, - rust_ctap2_register_result* aResult) - : mTransactionId(aTransactionId), mRegisterResult(aResult) { - MOZ_ASSERT(mRegisterResult); - } - - explicit CTAPResult(uint64_t aTransactionId, rust_ctap2_sign_result* aResult) - : mTransactionId(aTransactionId), mSignResult(aResult) { - MOZ_ASSERT(mSignResult); - } - - ~CTAPResult() { - // Rust-API can handle possible NULL-pointers - rust_u2f_res_free(mU2FResult); - rust_ctap2_register_res_free(mRegisterResult); - rust_ctap2_sign_res_free(mSignResult); - } - - uint64_t GetTransactionId() { return mTransactionId; } - - bool IsError() { return NS_FAILED(GetError()); } - - nsresult GetError() { - uint8_t res; - if (mU2FResult) { - res = rust_u2f_result_error(mU2FResult); - } else if (mRegisterResult) { - res = rust_ctap2_register_result_error(mRegisterResult); - } else if (mSignResult) { - res = rust_ctap2_sign_result_error(mSignResult); - } else { - return NS_ERROR_FAILURE; - } - - switch (res) { - case U2F_OK: - return NS_OK; - case U2F_ERROR_UKNOWN: - case U2F_ERROR_CONSTRAINT: - return NS_ERROR_DOM_UNKNOWN_ERR; - case U2F_ERROR_NOT_SUPPORTED: - return NS_ERROR_DOM_NOT_SUPPORTED_ERR; - case U2F_ERROR_INVALID_STATE: - return NS_ERROR_DOM_INVALID_STATE_ERR; - case U2F_ERROR_NOT_ALLOWED: - return NS_ERROR_DOM_NOT_ALLOWED_ERR; - case CTAP_ERROR_PIN_REQUIRED: - case CTAP_ERROR_PIN_INVALID: - case CTAP_ERROR_PIN_AUTH_BLOCKED: - case CTAP_ERROR_PIN_BLOCKED: - // This is not perfect, but we are reusing an existing error-code here. - // We need to differentiate only PIN-errors from non-PIN errors - // to know if the Popup-Dialog should be removed or stay - // after the operation errors out. We don't want to define - // new NS-errors at the moment, since it's all internal anyways. - return NS_ERROR_DOM_OPERATION_ERR; - default: - // Generic error - return NS_ERROR_FAILURE; - } - } - - bool CopyRegistration(nsTArray& aBuffer) { - return CopyBuffer(U2F_RESBUF_ID_REGISTRATION, aBuffer); - } - - bool CopyKeyHandle(nsTArray& aBuffer) { - return CopyBuffer(U2F_RESBUF_ID_KEYHANDLE, aBuffer); - } - - bool CopySignature(nsTArray& aBuffer) { - return CopyBuffer(U2F_RESBUF_ID_SIGNATURE, aBuffer); - } - - bool CopyAppId(nsTArray& aBuffer) { - return CopyBuffer(U2F_RESBUF_ID_APPID, aBuffer); - } - - bool CopyClientDataStr(nsCString& aBuffer) { - if (mU2FResult) { - return false; - } else if (mRegisterResult) { - size_t len; - if (!rust_ctap2_register_result_client_data_len(mRegisterResult, &len)) { - return false; - } - - if (!aBuffer.SetLength(len, fallible)) { - return false; - } - - return rust_ctap2_register_result_client_data_copy(mRegisterResult, - aBuffer.Data()); - } else if (mSignResult) { - size_t len; - if (!rust_ctap2_sign_result_client_data_len(mSignResult, &len)) { - return false; - } - - if (!aBuffer.SetLength(len, fallible)) { - return false; - } - - return rust_ctap2_sign_result_client_data_copy(mSignResult, - aBuffer.Data()); - } else { - return false; - } - } - - bool IsCtap2() { - // If it's not an U2F result, we already know its CTAP2 - return !mU2FResult; - } - - bool HasAppId() { return Contains(U2F_RESBUF_ID_APPID); } - - bool HasKeyHandle() { return Contains(U2F_RESBUF_ID_KEYHANDLE); } - - bool Ctap2GetNumberOfSignAssertions(size_t& len) { - return rust_ctap2_sign_result_assertions_len(mSignResult, &len); - } - - bool Ctap2CopyAttestation(nsTArray& aBuffer) { - if (!mRegisterResult) { - return false; - } - - size_t len; - if (!rust_ctap2_register_result_attestation_len(mRegisterResult, &len)) { - return false; - } - - if (!aBuffer.SetLength(len, fallible)) { - return false; - } - - return rust_ctap2_register_result_attestation_copy(mRegisterResult, - aBuffer.Elements()); - } - - bool Ctap2CopyCredentialId(nsTArray& aBuffer) { - if (!mRegisterResult) { - return false; - } - - size_t len; - if (!rust_ctap2_register_result_credential_id_len(mRegisterResult, &len)) { - return false; - } - - if (!aBuffer.SetLength(len, fallible)) { - return false; - } - - return rust_ctap2_register_result_credential_id_copy(mRegisterResult, - aBuffer.Elements()); - } - - bool Ctap2CopyPubKeyCredential(nsTArray& aBuffer, size_t index) { - return Ctap2SignResCopyBuffer(index, CTAP2_SIGN_RESULT_PUBKEY_CRED_ID, - aBuffer); - } - - bool Ctap2CopySignature(nsTArray& aBuffer, size_t index) { - return Ctap2SignResCopyBuffer(index, CTAP2_SIGN_RESULT_SIGNATURE, aBuffer); - } - - bool Ctap2CopyUserId(nsTArray& aBuffer, size_t index) { - return Ctap2SignResCopyBuffer(index, CTAP2_SIGN_RESULT_USER_ID, aBuffer); - } - - bool Ctap2CopyAuthData(nsTArray& aBuffer, size_t index) { - return Ctap2SignResCopyBuffer(index, CTAP2_SIGN_RESULT_AUTH_DATA, aBuffer); - } - - bool Ctap2CopyRpIdHash(nsTArray& aBuffer, size_t index) { - return Ctap2SignResCopyBuffer(index, CTAP2_SIGN_RESULT_RP_ID_HASH, aBuffer); - } - - bool Ctap2HasPubKeyCredential(size_t index) { - return Ctap2SignResContains(index, CTAP2_SIGN_RESULT_PUBKEY_CRED_ID); - } - - bool HasUserId(size_t index) { - return Ctap2SignResContains(index, CTAP2_SIGN_RESULT_USER_ID); - } - - bool HasUserName(size_t index) { - return Ctap2SignResContains(index, CTAP2_SIGN_RESULT_USER_NAME); - } - - bool CopyUserName(nsCString& aBuffer, size_t index) { - if (!mSignResult) { - return false; - } - - size_t len; - if (!rust_ctap2_sign_result_username_len(mSignResult, index, &len)) { - return false; - } - - if (!aBuffer.SetLength(len, fallible)) { - return false; - } - - return rust_ctap2_sign_result_username_copy(mSignResult, index, - aBuffer.Data()); - } - - private: - bool Contains(uint8_t aResBufId) { - if (mU2FResult) { - return rust_u2f_resbuf_contains(mU2FResult, aResBufId); - } - return false; - } - bool CopyBuffer(uint8_t aResBufID, nsTArray& aBuffer) { - if (!mU2FResult) { - return false; - } - - size_t len; - if (!rust_u2f_resbuf_length(mU2FResult, aResBufID, &len)) { - return false; - } - - if (!aBuffer.SetLength(len, fallible)) { - return false; - } - - return rust_u2f_resbuf_copy(mU2FResult, aResBufID, aBuffer.Elements()); - } - - bool Ctap2SignResContains(size_t assertion_idx, uint8_t item_idx) { - if (mSignResult) { - return rust_ctap2_sign_result_item_contains(mSignResult, assertion_idx, - item_idx); - } - return false; - } - bool Ctap2SignResCopyBuffer(size_t assertion_idx, uint8_t item_idx, - nsTArray& aBuffer) { - if (!mSignResult) { - return false; - } - - size_t len; - if (!rust_ctap2_sign_result_item_len(mSignResult, assertion_idx, item_idx, - &len)) { - return false; - } - - if (!aBuffer.SetLength(len, fallible)) { - return false; - } - - return rust_ctap2_sign_result_item_copy(mSignResult, assertion_idx, - item_idx, aBuffer.Elements()); - } - - uint64_t mTransactionId; - rust_u2f_result* mU2FResult = nullptr; - rust_ctap2_register_result* mRegisterResult = nullptr; - rust_ctap2_sign_result* mSignResult = nullptr; -}; - -class CTAPHIDTokenManager final : public U2FTokenTransport { - public: - explicit CTAPHIDTokenManager(); - - // TODO(MS): Once we completely switch over to CTAPHIDTokenManager and remove - // the old U2F version, this needs to be renamed to CTAPRegisterPromise. Same - // for Sign. - virtual RefPtr Register( - const WebAuthnMakeCredentialInfo& aInfo, bool aForceNoneAttestation, - void status_callback(rust_ctap2_status_update_res*)) override; - - virtual RefPtr Sign( - const WebAuthnGetAssertionInfo& aInfo, - void status_callback(rust_ctap2_status_update_res*)) override; - - void Cancel() override; - void Drop() override; - - void HandleRegisterResult(UniquePtr&& aResult); - void HandleSignResult(UniquePtr&& aResult); - - private: - ~CTAPHIDTokenManager() = default; - - void HandleRegisterResultCtap1(UniquePtr&& aResult); - void HandleRegisterResultCtap2(UniquePtr&& aResult); - void HandleSignResultCtap1(UniquePtr&& aResult); - void HandleSignResultCtap2(UniquePtr&& aResult); - mozilla::Maybe - HandleSelectedSignResultCtap2(UniquePtr&& aResult, size_t index); - void ClearPromises() { - mRegisterPromise.RejectIfExists(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - mSignPromise.RejectIfExists(NS_ERROR_DOM_UNKNOWN_ERR, __func__); - } - - class Transaction { - public: - Transaction(uint64_t aId, const nsTArray& aRpIdHash, - const Maybe>& aAppIdHash, - const nsCString& aClientDataJSON, - bool aForceNoneAttestation = false) - : mId(aId), - mRpIdHash(aRpIdHash.Clone()), - mClientDataJSON(aClientDataJSON), - mForceNoneAttestation(aForceNoneAttestation) { - if (aAppIdHash) { - mAppIdHash = Some(aAppIdHash->Clone()); - } else { - mAppIdHash = Nothing(); - } - } - - // The transaction ID. - uint64_t mId; - - // The RP ID hash. - nsTArray mRpIdHash; - - // The App ID hash, if the AppID extension was set - Maybe> mAppIdHash; - - // The clientData JSON. - nsCString mClientDataJSON; - - // Whether we'll force "none" attestation. - bool mForceNoneAttestation; - }; - - rust_ctap_manager* mCTAPManager; - Maybe mTransaction; - MozPromiseHolder mRegisterPromise; - MozPromiseHolder mSignPromise; -}; - -} // namespace mozilla::dom - -#endif // mozilla_dom_CTAPHIDTokenManager_h diff --git a/dom/webauthn/CtapArgs.cpp b/dom/webauthn/CtapArgs.cpp new file mode 100644 index 0000000000000..e6a497f730a5a --- /dev/null +++ b/dom/webauthn/CtapArgs.cpp @@ -0,0 +1,302 @@ +/* -*- 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 "CtapArgs.h" +#include "WebAuthnEnumStrings.h" +#include "mozilla/dom/PWebAuthnTransactionParent.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS(CtapRegisterArgs, nsICtapRegisterArgs) + +NS_IMETHODIMP +CtapRegisterArgs::GetOrigin(nsAString& aOrigin) { + mozilla::ipc::AssertIsOnBackgroundThread(); + aOrigin = mInfo.Origin(); + return NS_OK; +} + +NS_IMETHODIMP +CtapRegisterArgs::GetClientDataJSON(nsACString& aClientDataJSON) { + mozilla::ipc::AssertIsOnBackgroundThread(); + aClientDataJSON = mInfo.ClientDataJSON(); + return NS_OK; +} + +NS_IMETHODIMP +CtapRegisterArgs::GetChallenge(nsTArray& aChallenge) { + mozilla::ipc::AssertIsOnBackgroundThread(); + aChallenge.Clear(); + aChallenge.AppendElements(mInfo.Challenge()); + return NS_OK; +} + +NS_IMETHODIMP +CtapRegisterArgs::GetRpId(nsAString& aRpId) { + mozilla::ipc::AssertIsOnBackgroundThread(); + aRpId = mInfo.RpId(); + return NS_OK; +} + +NS_IMETHODIMP +CtapRegisterArgs::GetRpName(nsAString& aRpName) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mInfo.Extra().isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + aRpName = (*mInfo.Extra()).Rp().Name(); + return NS_OK; +} + +NS_IMETHODIMP +CtapRegisterArgs::GetUserId(nsTArray& aUserId) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mInfo.Extra().isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + aUserId.Clear(); + aUserId.AppendElements((*mInfo.Extra()).User().Id()); + return NS_OK; +} + +NS_IMETHODIMP +CtapRegisterArgs::GetUserName(nsAString& aUserName) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mInfo.Extra().isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + aUserName = (*mInfo.Extra()).User().Name(); + return NS_OK; +} + +NS_IMETHODIMP +CtapRegisterArgs::GetUserDisplayName(nsAString& aUserDisplayName) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mInfo.Extra().isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + aUserDisplayName = (*mInfo.Extra()).User().DisplayName(); + return NS_OK; +} + +NS_IMETHODIMP +CtapRegisterArgs::GetCoseAlgs(nsTArray& aCoseAlgs) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mInfo.Extra().isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + aCoseAlgs.Clear(); + for (const CoseAlg& coseAlg : (*mInfo.Extra()).coseAlgs()) { + aCoseAlgs.AppendElement(coseAlg.alg()); + } + return NS_OK; +} + +NS_IMETHODIMP +CtapRegisterArgs::GetExcludeList(nsTArray >& aExcludeList) { + mozilla::ipc::AssertIsOnBackgroundThread(); + aExcludeList.Clear(); + for (const WebAuthnScopedCredential& cred : mInfo.ExcludeList()) { + aExcludeList.AppendElement(cred.id().Clone()); + } + return NS_OK; +} + +NS_IMETHODIMP +CtapRegisterArgs::GetHmacCreateSecret(bool* aHmacCreateSecret) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mInfo.Extra().isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + + for (const WebAuthnExtension& ext : (*mInfo.Extra()).Extensions()) { + if (ext.type() == WebAuthnExtension::TWebAuthnExtensionHmacSecret) { + *aHmacCreateSecret = + ext.get_WebAuthnExtensionHmacSecret().hmacCreateSecret(); + return NS_OK; + } + } + + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +CtapRegisterArgs::GetRequireResidentKey(bool* aRequireResidentKey) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mInfo.Extra().isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + *aRequireResidentKey = + (*mInfo.Extra()).AuthenticatorSelection().requireResidentKey(); + return NS_OK; +} + +NS_IMETHODIMP +CtapRegisterArgs::GetUserVerification(nsAString& aUserVerificationRequirement) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mInfo.Extra().isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + aUserVerificationRequirement = + (*mInfo.Extra()).AuthenticatorSelection().userVerificationRequirement(); + return NS_OK; +} + +NS_IMETHODIMP +CtapRegisterArgs::GetAuthenticatorAttachment( + nsAString& aAuthenticatorAttachment) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mInfo.Extra().isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + if ((*mInfo.Extra()) + .AuthenticatorSelection() + .authenticatorAttachment() + .isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + aAuthenticatorAttachment = + *(*mInfo.Extra()).AuthenticatorSelection().authenticatorAttachment(); + return NS_OK; +} + +NS_IMETHODIMP +CtapRegisterArgs::GetTimeoutMS(uint32_t* aTimeoutMS) { + mozilla::ipc::AssertIsOnBackgroundThread(); + *aTimeoutMS = mInfo.TimeoutMS(); + return NS_OK; +} + +NS_IMETHODIMP +CtapRegisterArgs::GetAttestationConveyancePreference( + nsAString& aAttestationConveyancePreference) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mInfo.Extra().isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (mForceNoneAttestation) { + aAttestationConveyancePreference = NS_ConvertUTF8toUTF16( + MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE); + } else { + aAttestationConveyancePreference = + (*mInfo.Extra()).attestationConveyancePreference(); + } + return NS_OK; +} + +NS_IMPL_ISUPPORTS(CtapSignArgs, nsICtapSignArgs) + +NS_IMETHODIMP +CtapSignArgs::GetOrigin(nsAString& aOrigin) { + mozilla::ipc::AssertIsOnBackgroundThread(); + aOrigin = mInfo.Origin(); + return NS_OK; +} + +NS_IMETHODIMP +CtapSignArgs::GetRpId(nsAString& aRpId) { + mozilla::ipc::AssertIsOnBackgroundThread(); + aRpId = mInfo.RpId(); + return NS_OK; +} + +NS_IMETHODIMP +CtapSignArgs::GetClientDataJSON(nsACString& aClientDataJSON) { + mozilla::ipc::AssertIsOnBackgroundThread(); + aClientDataJSON = mInfo.ClientDataJSON(); + return NS_OK; +} + +NS_IMETHODIMP +CtapSignArgs::GetChallenge(nsTArray& aChallenge) { + mozilla::ipc::AssertIsOnBackgroundThread(); + aChallenge.Clear(); + aChallenge.AppendElements(mInfo.Challenge()); + return NS_OK; +} + +NS_IMETHODIMP +CtapSignArgs::GetAllowList(nsTArray >& aAllowList) { + mozilla::ipc::AssertIsOnBackgroundThread(); + aAllowList.Clear(); + for (const WebAuthnScopedCredential& cred : mInfo.AllowList()) { + aAllowList.AppendElement(cred.id().Clone()); + } + return NS_OK; +} + +NS_IMETHODIMP +CtapSignArgs::GetHmacCreateSecret(bool* aHmacCreateSecret) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mInfo.Extra().isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + + for (const WebAuthnExtension& ext : (*mInfo.Extra()).Extensions()) { + if (ext.type() == WebAuthnExtension::TWebAuthnExtensionHmacSecret) { + *aHmacCreateSecret = + ext.get_WebAuthnExtensionHmacSecret().hmacCreateSecret(); + return NS_OK; + } + } + + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +CtapSignArgs::GetAppId(nsAString& aAppId) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mInfo.Extra().isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + + for (const WebAuthnExtension& ext : (*mInfo.Extra()).Extensions()) { + if (ext.type() == WebAuthnExtension::TWebAuthnExtensionAppId) { + aAppId = ext.get_WebAuthnExtensionAppId().appIdentifier(); + return NS_OK; + } + } + + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +CtapSignArgs::GetAppIdHash(nsTArray& aAppIdHash) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mInfo.Extra().isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + + for (const WebAuthnExtension& ext : (*mInfo.Extra()).Extensions()) { + if (ext.type() == WebAuthnExtension::TWebAuthnExtensionAppId) { + aAppIdHash.Clear(); + aAppIdHash.AppendElements(ext.get_WebAuthnExtensionAppId().AppId()); + return NS_OK; + } + } + + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +CtapSignArgs::GetUserVerification(nsAString& aUserVerificationRequirement) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mInfo.Extra().isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + aUserVerificationRequirement = (*mInfo.Extra()).userVerificationRequirement(); + return NS_OK; +} + +NS_IMETHODIMP +CtapSignArgs::GetTimeoutMS(uint32_t* aTimeoutMS) { + mozilla::ipc::AssertIsOnBackgroundThread(); + *aTimeoutMS = mInfo.TimeoutMS(); + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/webauthn/CtapArgs.h b/dom/webauthn/CtapArgs.h new file mode 100644 index 0000000000000..406c24f4ba81b --- /dev/null +++ b/dom/webauthn/CtapArgs.h @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef CtapArgs_h +#define CtapArgs_h + +#include "mozilla/dom/WebAuthnTransactionChild.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "nsIWebAuthnController.h" + +namespace mozilla::dom { + +// These classes provide an FFI between C++ and Rust for the getters of IPC +// objects (WebAuthnMakeCredentialInfo and WebAuthnGetAssertionInfo). They hold +// non-owning references to IPC objects, and must only be used within the +// lifetime of the IPC transaction that created them. There are runtime +// assertions to ensure that these types are created and used on the IPC +// background thread, but that alone does not guarantee safety. + +class CtapRegisterArgs final : public nsICtapRegisterArgs { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICTAPREGISTERARGS + + explicit CtapRegisterArgs(const WebAuthnMakeCredentialInfo& aInfo, + bool aForceNoneAttestation) + : mInfo(aInfo), mForceNoneAttestation(aForceNoneAttestation) { + mozilla::ipc::AssertIsOnBackgroundThread(); + } + + private: + ~CtapRegisterArgs() = default; + + const WebAuthnMakeCredentialInfo& mInfo; + const bool mForceNoneAttestation; +}; + +class CtapSignArgs final : public nsICtapSignArgs { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICTAPSIGNARGS + + explicit CtapSignArgs(const WebAuthnGetAssertionInfo& aInfo) : mInfo(aInfo) { + mozilla::ipc::AssertIsOnBackgroundThread(); + } + + private: + ~CtapSignArgs() = default; + + const WebAuthnGetAssertionInfo& mInfo; +}; + +} // namespace mozilla::dom + +#endif // CtapArgs_h diff --git a/dom/webauthn/CtapResults.cpp b/dom/webauthn/CtapResults.cpp new file mode 100644 index 0000000000000..216b08c8ae0c0 --- /dev/null +++ b/dom/webauthn/CtapResults.cpp @@ -0,0 +1,88 @@ +/* -*- 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 "CtapResults.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS(CtapRegisterResult, nsICtapRegisterResult) + +NS_IMETHODIMP +CtapRegisterResult::GetStatus(nsresult* aStatus) { + *aStatus = mStatus; + return NS_OK; +} + +NS_IMETHODIMP +CtapRegisterResult::GetClientDataJSON(nsACString& aClientDataJSON) { + aClientDataJSON = mClientDataJSON; + return NS_OK; +} + +NS_IMETHODIMP +CtapRegisterResult::GetAttestationObject( + nsTArray& aAttestationObject) { + aAttestationObject.Clear(); + aAttestationObject.AppendElements(mAttestationObject); + return NS_OK; +} + +NS_IMETHODIMP +CtapRegisterResult::GetCredentialId(nsTArray& aCredentialId) { + aCredentialId.Clear(); + aCredentialId.AppendElements(mCredentialId); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(CtapSignResult, nsICtapSignResult) + +NS_IMETHODIMP +CtapSignResult::GetStatus(nsresult* aStatus) { + *aStatus = mStatus; + return NS_OK; +} + +NS_IMETHODIMP +CtapSignResult::GetCredentialId(nsTArray& aCredentialId) { + aCredentialId.Clear(); + aCredentialId.AppendElements(mCredentialId); + return NS_OK; +} + +NS_IMETHODIMP +CtapSignResult::GetAuthenticatorData(nsTArray& aAuthenticatorData) { + aAuthenticatorData.Clear(); + aAuthenticatorData.AppendElements(mAuthenticatorData); + return NS_OK; +} + +NS_IMETHODIMP +CtapSignResult::GetSignature(nsTArray& aSignature) { + aSignature.Clear(); + aSignature.AppendElements(mSignature); + return NS_OK; +} + +NS_IMETHODIMP +CtapSignResult::GetUserHandle(nsTArray& aUserHandle) { + aUserHandle.Clear(); + aUserHandle.AppendElements(mUserHandle); + return NS_OK; +} + +NS_IMETHODIMP +CtapSignResult::GetUserName(nsACString& aUserName) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +CtapSignResult::GetRpIdHash(nsTArray& aRpIdHash) { + aRpIdHash.Clear(); + aRpIdHash.AppendElements(mRpIdHash); + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/webauthn/CtapResults.h b/dom/webauthn/CtapResults.h new file mode 100644 index 0000000000000..6b4194c22104a --- /dev/null +++ b/dom/webauthn/CtapResults.h @@ -0,0 +1,67 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef CtapResults_h +#define CtapResults_h + +#include "mozilla/dom/WebAuthnTransactionChild.h" +#include "nsIWebAuthnController.h" + +namespace mozilla::dom { + +class CtapRegisterResult final : public nsICtapRegisterResult { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICTAPREGISTERRESULT + + explicit CtapRegisterResult(nsresult aStatus, nsCString&& aClientDataJSON, + nsTArray&& aAttestationObject, + nsTArray&& aCredentialId) + : mStatus(aStatus), + mClientDataJSON(std::move(aClientDataJSON)), + mAttestationObject(std::move(aAttestationObject)), + mCredentialId(std::move(aCredentialId)) {} + + private: + ~CtapRegisterResult() = default; + + nsresult mStatus; + nsCString mClientDataJSON; + nsTArray mAttestationObject; + nsTArray mCredentialId; +}; + +class CtapSignResult final : public nsICtapSignResult { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICTAPSIGNRESULT + + explicit CtapSignResult(nsresult aStatus, nsTArray&& aCredentialId, + nsTArray&& aSignature, + nsTArray&& aAuthenticatorData, + nsTArray&& aUserHandle, + nsTArray&& aRpIdHash) + : mStatus(aStatus), + mCredentialId(std::move(aCredentialId)), + mSignature(std::move(aSignature)), + mAuthenticatorData(std::move(aAuthenticatorData)), + mUserHandle(std::move(aUserHandle)), + mRpIdHash(std::move(aRpIdHash)) {} + + private: + ~CtapSignResult() = default; + + nsresult mStatus; + nsTArray mCredentialId; + nsTArray mSignature; + nsTArray mAuthenticatorData; + nsTArray mUserHandle; + nsTArray mRpIdHash; +}; + +} // namespace mozilla::dom + +#endif // CtapResult_h diff --git a/dom/webauthn/U2FHIDTokenManager.cpp b/dom/webauthn/U2FHIDTokenManager.cpp index 49d04e924c35d..fd0b26b1621ac 100644 --- a/dom/webauthn/U2FHIDTokenManager.cpp +++ b/dom/webauthn/U2FHIDTokenManager.cpp @@ -104,8 +104,7 @@ void U2FHIDTokenManager::Drop() { // * attestation signature // RefPtr U2FHIDTokenManager::Register( - const WebAuthnMakeCredentialInfo& aInfo, bool aForceNoneAttestation, - void _status_callback(rust_ctap2_status_update_res*)) { + const WebAuthnMakeCredentialInfo& aInfo, bool aForceNoneAttestation) { mozilla::ipc::AssertIsOnBackgroundThread(); uint64_t registerFlags = 0; @@ -207,8 +206,7 @@ RefPtr U2FHIDTokenManager::Register( // * Signature // RefPtr U2FHIDTokenManager::Sign( - const WebAuthnGetAssertionInfo& aInfo, - void _status_callback(rust_ctap2_status_update_res*)) { + const WebAuthnGetAssertionInfo& aInfo) { mozilla::ipc::AssertIsOnBackgroundThread(); CryptoBuffer rpIdHash, clientDataHash; diff --git a/dom/webauthn/U2FHIDTokenManager.h b/dom/webauthn/U2FHIDTokenManager.h index 4fe2495c31e25..87519e71a875c 100644 --- a/dom/webauthn/U2FHIDTokenManager.h +++ b/dom/webauthn/U2FHIDTokenManager.h @@ -123,12 +123,11 @@ class U2FHIDTokenManager final : public U2FTokenTransport { explicit U2FHIDTokenManager(); virtual RefPtr Register( - const WebAuthnMakeCredentialInfo& aInfo, bool aForceNoneAttestation, - void _status_callback(rust_ctap2_status_update_res*)) override; + const WebAuthnMakeCredentialInfo& aInfo, + bool aForceNoneAttestation) override; virtual RefPtr Sign( - const WebAuthnGetAssertionInfo& aInfo, - void _status_callback(rust_ctap2_status_update_res*)) override; + const WebAuthnGetAssertionInfo& aInfo) override; void Cancel() override; void Drop() override; diff --git a/dom/webauthn/U2FSoftTokenManager.cpp b/dom/webauthn/U2FSoftTokenManager.cpp index da30436ed73bb..d95deacd261d1 100644 --- a/dom/webauthn/U2FSoftTokenManager.cpp +++ b/dom/webauthn/U2FSoftTokenManager.cpp @@ -29,7 +29,7 @@ using mozilla::dom::CreateECParamsForCurve; const nsCString U2FSoftTokenManager::mSecretNickname = "U2F_NSSTOKEN"_ns; namespace { -constexpr auto kAttestCertSubjectName = "CN=Firefox U2F Soft Token"_ns; +constexpr auto kAttestCertSubjectNameOld = "CN=Firefox U2F Soft Token"_ns; // This U2F-compatible soft token uses FIDO U2F-compatible ECDSA keypairs // on the SEC_OID_SECG_EC_SECP256R1 curve. When asked to Register, it will @@ -42,25 +42,25 @@ constexpr auto kAttestCertSubjectName = "CN=Firefox U2F Soft Token"_ns; // ephemeral to counteract profiling. They have little use for a soft-token // at any rate, but are required by the specification. -const uint32_t kParamLen = 32; -const uint32_t kPublicKeyLen = 65; -const uint32_t kWrappedKeyBufLen = 256; -const uint32_t kWrappingKeyByteLen = 128 / 8; -const uint32_t kSaltByteLen = 64 / 8; -const uint32_t kVersion1KeyHandleLen = 162; -constexpr auto kEcAlgorithm = +const uint32_t kParamLenOld = 32; +const uint32_t kPublicKeyLenOld = 65; +const uint32_t kWrappedKeyBufLenOld = 256; +const uint32_t kWrappingKeyByteLenOld = 128 / 8; +const uint32_t kSaltByteLenOld = 64 / 8; +const uint32_t kVersion1KeyHandleLenOld = 162; +constexpr auto kEcAlgorithmOld = NS_LITERAL_STRING_FROM_CSTRING(WEBCRYPTO_NAMED_CURVE_P256); -const PRTime kOneDay = PRTime(PR_USEC_PER_SEC) * PRTime(60) // sec - * PRTime(60) // min - * PRTime(24); // hours -const PRTime kExpirationSlack = kOneDay; // Pre-date for clock skew -const PRTime kExpirationLife = kOneDay; +const PRTime kOneDayOld = PRTime(PR_USEC_PER_SEC) * PRTime(60) // sec + * PRTime(60) // min + * PRTime(24); // hours +const PRTime kExpirationSlackOld = kOneDayOld; // Pre-date for clock skew +const PRTime kExpirationLifeOld = kOneDayOld; -static mozilla::LazyLogModule gNSSTokenLog("webauth_u2f"); +static mozilla::LazyLogModule gNSSTokenLogOld("webauth_u2f"); -enum SoftTokenHandle { - Version1 = 0, +enum SoftTokenHandleOld { + Version1Old = 0, }; } // namespace @@ -77,21 +77,21 @@ U2FSoftTokenManager::U2FSoftTokenManager(uint32_t aCounter) * @param aNickname Nickname the key should have. * @return The first key found. nullptr if no key could be found. */ -static UniquePK11SymKey GetSymKeyByNickname(const UniquePK11SlotInfo& aSlot, - const nsCString& aNickname) { +static UniquePK11SymKey GetSymKeyByNicknameOld(const UniquePK11SlotInfo& aSlot, + const nsCString& aNickname) { MOZ_ASSERT(aSlot); if (NS_WARN_IF(!aSlot)) { return nullptr; } - MOZ_LOG(gNSSTokenLog, LogLevel::Debug, + MOZ_LOG(gNSSTokenLogOld, LogLevel::Debug, ("Searching for a symmetric key named %s", aNickname.get())); UniquePK11SymKey keyListHead( PK11_ListFixedKeysInSlot(aSlot.get(), const_cast(aNickname.get()), /* wincx */ nullptr)); if (NS_WARN_IF(!keyListHead)) { - MOZ_LOG(gNSSTokenLog, LogLevel::Debug, ("Symmetric key not found.")); + MOZ_LOG(gNSSTokenLogOld, LogLevel::Debug, ("Symmetric key not found.")); return nullptr; } @@ -99,7 +99,7 @@ static UniquePK11SymKey GetSymKeyByNickname(const UniquePK11SlotInfo& aSlot, // nickname. MOZ_ASSERT(aNickname == UniquePORTString(PK11_GetSymKeyNickname(keyListHead.get())).get()); - MOZ_LOG(gNSSTokenLog, LogLevel::Debug, ("Symmetric key found!")); + MOZ_LOG(gNSSTokenLogOld, LogLevel::Debug, ("Symmetric key found!")); // Free any remaining keys in the key list. UniquePK11SymKey freeKey(PK11_GetNextSymKey(keyListHead.get())); @@ -110,9 +110,9 @@ static UniquePK11SymKey GetSymKeyByNickname(const UniquePK11SlotInfo& aSlot, return keyListHead; } -static nsresult GenEcKeypair(const UniquePK11SlotInfo& aSlot, - /*out*/ UniqueSECKEYPrivateKey& aPrivKey, - /*out*/ UniqueSECKEYPublicKey& aPubKey) { +static nsresult GenEcKeypairOld(const UniquePK11SlotInfo& aSlot, + /*out*/ UniqueSECKEYPrivateKey& aPrivKey, + /*out*/ UniqueSECKEYPublicKey& aPubKey) { MOZ_ASSERT(aSlot); if (NS_WARN_IF(!aSlot)) { return NS_ERROR_INVALID_ARG; @@ -124,7 +124,7 @@ static nsresult GenEcKeypair(const UniquePK11SlotInfo& aSlot, } // Set the curve parameters; keyParams belongs to the arena memory space - SECItem* keyParams = CreateECParamsForCurve(kEcAlgorithm, arena.get()); + SECItem* keyParams = CreateECParamsForCurve(kEcAlgorithmOld, arena.get()); if (NS_WARN_IF(!keyParams)) { return NS_ERROR_OUT_OF_MEMORY; } @@ -144,7 +144,7 @@ static nsresult GenEcKeypair(const UniquePK11SlotInfo& aSlot, } // Check that the public key has the correct length - if (NS_WARN_IF(aPubKey->u.ec.publicValue.len != kPublicKeyLen)) { + if (NS_WARN_IF(aPubKey->u.ec.publicValue.len != kPublicKeyLenOld)) { return NS_ERROR_FAILURE; } @@ -160,28 +160,28 @@ nsresult U2FSoftTokenManager::GetOrCreateWrappingKey( // Search for an existing wrapping key. If we find it, // store it for later and mark ourselves initialized. - mWrappingKey = GetSymKeyByNickname(aSlot, mSecretNickname); + mWrappingKey = GetSymKeyByNicknameOld(aSlot, mSecretNickname); if (mWrappingKey) { - MOZ_LOG(gNSSTokenLog, LogLevel::Debug, ("U2F Soft Token Key found.")); + MOZ_LOG(gNSSTokenLogOld, LogLevel::Debug, ("U2F Soft Token Key found.")); mInitialized = true; return NS_OK; } - MOZ_LOG(gNSSTokenLog, LogLevel::Info, + MOZ_LOG(gNSSTokenLogOld, LogLevel::Info, ("No keys found. Generating new U2F Soft Token wrapping key.")); // We did not find an existing wrapping key, so we generate one in the // persistent database (e.g, Token). mWrappingKey = UniquePK11SymKey(PK11_TokenKeyGenWithFlags( aSlot.get(), CKM_AES_KEY_GEN, - /* default params */ nullptr, kWrappingKeyByteLen, + /* default params */ nullptr, kWrappingKeyByteLenOld, /* empty keyid */ nullptr, /* flags */ CKF_WRAP | CKF_UNWRAP, /* attributes */ PK11_ATTR_TOKEN | PK11_ATTR_PRIVATE, /* wincx */ nullptr)); if (NS_WARN_IF(!mWrappingKey)) { - MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + MOZ_LOG(gNSSTokenLogOld, LogLevel::Warning, ("Failed to store wrapping key, NSS error #%d", PORT_GetError())); return NS_ERROR_FAILURE; } @@ -189,12 +189,12 @@ nsresult U2FSoftTokenManager::GetOrCreateWrappingKey( SECStatus srv = PK11_SetSymKeyNickname(mWrappingKey.get(), mSecretNickname.get()); if (NS_WARN_IF(srv != SECSuccess)) { - MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + MOZ_LOG(gNSSTokenLogOld, LogLevel::Warning, ("Failed to set nickname, NSS error #%d", PORT_GetError())); return NS_ERROR_FAILURE; } - MOZ_LOG(gNSSTokenLog, LogLevel::Debug, + MOZ_LOG(gNSSTokenLogOld, LogLevel::Debug, ("Key stored, nickname set to %s.", mSecretNickname.get())); GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction( @@ -206,7 +206,7 @@ nsresult U2FSoftTokenManager::GetOrCreateWrappingKey( return NS_OK; } -static nsresult GetAttestationCertificate( +static nsresult GetAttestationCertificateOld( const UniquePK11SlotInfo& aSlot, /*out*/ UniqueSECKEYPrivateKey& aAttestPrivKey, /*out*/ UniqueCERTCertificate& aAttestCert) { @@ -218,17 +218,17 @@ static nsresult GetAttestationCertificate( UniqueSECKEYPublicKey pubKey; // Construct an ephemeral keypair for this Attestation Certificate - nsresult rv = GenEcKeypair(aSlot, aAttestPrivKey, pubKey); + nsresult rv = GenEcKeypairOld(aSlot, aAttestPrivKey, pubKey); if (NS_WARN_IF(NS_FAILED(rv) || !aAttestPrivKey || !pubKey)) { - MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + MOZ_LOG(gNSSTokenLogOld, LogLevel::Warning, ("Failed to gen keypair, NSS error #%d", PORT_GetError())); return NS_ERROR_FAILURE; } // Construct the Attestation Certificate itself - UniqueCERTName subjectName(CERT_AsciiToName(kAttestCertSubjectName.get())); + UniqueCERTName subjectName(CERT_AsciiToName(kAttestCertSubjectNameOld.get())); if (NS_WARN_IF(!subjectName)) { - MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + MOZ_LOG(gNSSTokenLogOld, LogLevel::Warning, ("Failed to set subject name, NSS error #%d", PORT_GetError())); return NS_ERROR_FAILURE; } @@ -236,7 +236,7 @@ static nsresult GetAttestationCertificate( UniqueCERTSubjectPublicKeyInfo spki( SECKEY_CreateSubjectPublicKeyInfo(pubKey.get())); if (NS_WARN_IF(!spki)) { - MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + MOZ_LOG(gNSSTokenLogOld, LogLevel::Warning, ("Failed to set SPKI, NSS error #%d", PORT_GetError())); return NS_ERROR_FAILURE; } @@ -244,18 +244,18 @@ static nsresult GetAttestationCertificate( UniqueCERTCertificateRequest certreq( CERT_CreateCertificateRequest(subjectName.get(), spki.get(), nullptr)); if (NS_WARN_IF(!certreq)) { - MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + MOZ_LOG(gNSSTokenLogOld, LogLevel::Warning, ("Failed to gen CSR, NSS error #%d", PORT_GetError())); return NS_ERROR_FAILURE; } PRTime now = PR_Now(); - PRTime notBefore = now - kExpirationSlack; - PRTime notAfter = now + kExpirationLife; + PRTime notBefore = now - kExpirationSlackOld; + PRTime notAfter = now + kExpirationLifeOld; UniqueCERTValidity validity(CERT_CreateValidity(notBefore, notAfter)); if (NS_WARN_IF(!validity)) { - MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + MOZ_LOG(gNSSTokenLogOld, LogLevel::Warning, ("Failed to gen validity, NSS error #%d", PORT_GetError())); return NS_ERROR_FAILURE; } @@ -266,7 +266,7 @@ static nsresult GetAttestationCertificate( SECStatus srv = PK11_GenerateRandomOnSlot(aSlot.get(), serialBytes, sizeof(serial)); if (NS_WARN_IF(srv != SECSuccess)) { - MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + MOZ_LOG(gNSSTokenLogOld, LogLevel::Warning, ("Failed to gen serial, NSS error #%d", PORT_GetError())); return NS_ERROR_FAILURE; } @@ -282,7 +282,7 @@ static nsresult GetAttestationCertificate( aAttestCert = UniqueCERTCertificate(CERT_CreateCertificate( serial, subjectName.get(), validity.get(), certreq.get())); if (NS_WARN_IF(!aAttestCert)) { - MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + MOZ_LOG(gNSSTokenLogOld, LogLevel::Warning, ("Failed to gen certificate, NSS error #%d", PORT_GetError())); return NS_ERROR_FAILURE; } @@ -319,7 +319,7 @@ static nsresult GetAttestationCertificate( } aAttestCert->derCert = *signedCert; - MOZ_LOG(gNSSTokenLog, LogLevel::Debug, + MOZ_LOG(gNSSTokenLogOld, LogLevel::Debug, ("U2F Soft Token attestation certificate generated.")); return NS_OK; } @@ -342,14 +342,14 @@ nsresult U2FSoftTokenManager::Init() { } mInitialized = true; - MOZ_LOG(gNSSTokenLog, LogLevel::Debug, ("U2F Soft Token initialized.")); + MOZ_LOG(gNSSTokenLogOld, LogLevel::Debug, ("U2F Soft Token initialized.")); return NS_OK; } // Convert a Private Key object into an opaque key handle, using AES Key Wrap // with the long-lived aPersistentKey mixed with aAppParam to convert aPrivKey. // The key handle's format is version || saltLen || salt || wrappedPrivateKey -static UniqueSECItem KeyHandleFromPrivateKey( +static UniqueSECItem KeyHandleFromPrivateKeyOld( const UniquePK11SlotInfo& aSlot, const UniquePK11SymKey& aPersistentKey, uint8_t* aAppParam, uint32_t aAppParamLen, const UniqueSECKEYPrivateKey& aPrivKey) { @@ -362,11 +362,11 @@ static UniqueSECItem KeyHandleFromPrivateKey( } // Generate a random salt - uint8_t saltParam[kSaltByteLen]; + uint8_t saltParam[kSaltByteLenOld]; SECStatus srv = PK11_GenerateRandomOnSlot(aSlot.get(), saltParam, sizeof(saltParam)); if (NS_WARN_IF(srv != SECSuccess)) { - MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + MOZ_LOG(gNSSTokenLogOld, LogLevel::Warning, ("Failed to generate a salt, NSS error #%d", PORT_GetError())); return nullptr; } @@ -382,19 +382,19 @@ static UniqueSECItem KeyHandleFromPrivateKey( // derived symmetric key and don't matter because we ignore them anyway. UniquePK11SymKey wrapKey( PK11_Derive(aPersistentKey.get(), CKM_NSS_HKDF_SHA256, &kdfParams, - CKM_AES_KEY_GEN, CKA_WRAP, kWrappingKeyByteLen)); + CKM_AES_KEY_GEN, CKA_WRAP, kWrappingKeyByteLenOld)); if (NS_WARN_IF(!wrapKey.get())) { MOZ_LOG( - gNSSTokenLog, LogLevel::Warning, + gNSSTokenLogOld, LogLevel::Warning, ("Failed to derive a wrapping key, NSS error #%d", PORT_GetError())); return nullptr; } UniqueSECItem wrappedKey(::SECITEM_AllocItem(/* default arena */ nullptr, /* no buffer */ nullptr, - kWrappedKeyBufLen)); + kWrappedKeyBufLenOld)); if (NS_WARN_IF(!wrappedKey)) { - MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Failed to allocate memory")); + MOZ_LOG(gNSSTokenLogOld, LogLevel::Warning, ("Failed to allocate memory")); return nullptr; } @@ -406,7 +406,7 @@ static UniqueSECItem KeyHandleFromPrivateKey( CKM_NSS_AES_KEY_WRAP_PAD, param.get(), wrappedKey.get(), /* wincx */ nullptr); if (NS_WARN_IF(srv != SECSuccess)) { - MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + MOZ_LOG(gNSSTokenLogOld, LogLevel::Warning, ("Failed to wrap U2F key, NSS error #%d", PORT_GetError())); return nullptr; } @@ -415,13 +415,13 @@ static UniqueSECItem KeyHandleFromPrivateKey( mozilla::dom::CryptoBuffer keyHandleBuf; if (NS_WARN_IF(!keyHandleBuf.SetCapacity( wrappedKey.get()->len + sizeof(saltParam) + 2, mozilla::fallible))) { - MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Failed to allocate memory")); + MOZ_LOG(gNSSTokenLogOld, LogLevel::Warning, ("Failed to allocate memory")); return nullptr; } // It's OK to ignore the return values here because we're writing into // pre-allocated space - (void)keyHandleBuf.AppendElement(SoftTokenHandle::Version1, + (void)keyHandleBuf.AppendElement(SoftTokenHandleOld::Version1Old, mozilla::fallible); (void)keyHandleBuf.AppendElement(sizeof(saltParam), mozilla::fallible); (void)keyHandleBuf.AppendElements(saltParam, sizeof(saltParam), @@ -430,13 +430,13 @@ static UniqueSECItem KeyHandleFromPrivateKey( UniqueSECItem keyHandle(::SECITEM_AllocItem(nullptr, nullptr, 0)); if (NS_WARN_IF(!keyHandle)) { - MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Failed to allocate memory")); + MOZ_LOG(gNSSTokenLogOld, LogLevel::Warning, ("Failed to allocate memory")); return nullptr; } if (NS_WARN_IF(!keyHandleBuf.ToSECItem(/* default arena */ nullptr, keyHandle.get()))) { - MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Failed to allocate memory")); + MOZ_LOG(gNSSTokenLogOld, LogLevel::Warning, ("Failed to allocate memory")); return nullptr; } return keyHandle; @@ -445,7 +445,7 @@ static UniqueSECItem KeyHandleFromPrivateKey( // Convert an opaque key handle aKeyHandle back into a Private Key object, using // the long-lived aPersistentKey mixed with aAppParam and the AES Key Wrap // algorithm. -static UniqueSECKEYPrivateKey PrivateKeyFromKeyHandle( +static UniqueSECKEYPrivateKey PrivateKeyFromKeyHandleOld( const UniquePK11SlotInfo& aSlot, const UniquePK11SymKey& aPersistentKey, uint8_t* aKeyHandle, uint32_t aKeyHandleLen, uint8_t* aAppParam, uint32_t aAppParamLen) { @@ -461,18 +461,18 @@ static UniqueSECKEYPrivateKey PrivateKeyFromKeyHandle( // As we only support one key format ourselves (right now), fail early if // we aren't that length - if (NS_WARN_IF(aKeyHandleLen != kVersion1KeyHandleLen)) { + if (NS_WARN_IF(aKeyHandleLen != kVersion1KeyHandleLenOld)) { return nullptr; } - if (NS_WARN_IF(aKeyHandle[0] != SoftTokenHandle::Version1)) { + if (NS_WARN_IF(aKeyHandle[0] != SoftTokenHandleOld::Version1Old)) { // Unrecognized version return nullptr; } uint8_t saltLen = aKeyHandle[1]; uint8_t* saltPtr = aKeyHandle + 2; - if (NS_WARN_IF(saltLen != kSaltByteLen)) { + if (NS_WARN_IF(saltLen != kSaltByteLenOld)) { return nullptr; } @@ -487,10 +487,10 @@ static UniqueSECKEYPrivateKey PrivateKeyFromKeyHandle( // derived symmetric key and don't matter because we ignore them anyway. UniquePK11SymKey wrapKey( PK11_Derive(aPersistentKey.get(), CKM_NSS_HKDF_SHA256, &kdfParams, - CKM_AES_KEY_GEN, CKA_WRAP, kWrappingKeyByteLen)); + CKM_AES_KEY_GEN, CKA_WRAP, kWrappingKeyByteLenOld)); if (NS_WARN_IF(!wrapKey.get())) { MOZ_LOG( - gNSSTokenLog, LogLevel::Warning, + gNSSTokenLogOld, LogLevel::Warning, ("Failed to derive a wrapping key, NSS error #%d", PORT_GetError())); return nullptr; } @@ -501,7 +501,7 @@ static UniqueSECKEYPrivateKey PrivateKeyFromKeyHandle( ScopedAutoSECItem wrappedKeyItem(wrappedLen); memcpy(wrappedKeyItem.data, wrappedPtr, wrappedKeyItem.len); - ScopedAutoSECItem pubKey(kPublicKeyLen); + ScopedAutoSECItem pubKey(kPublicKeyLenOld); UniqueSECItem param(PK11_ParamFromIV(CKM_NSS_AES_KEY_WRAP_PAD, /* default IV */ nullptr)); @@ -519,7 +519,7 @@ static UniqueSECKEYPrivateKey PrivateKeyFromKeyHandle( /* wincx */ nullptr)); if (NS_WARN_IF(!unwrappedKey)) { // Not our key. - MOZ_LOG(gNSSTokenLog, LogLevel::Debug, + MOZ_LOG(gNSSTokenLogOld, LogLevel::Debug, ("Could not unwrap key handle, NSS Error #%d", PORT_GetError())); return nullptr; } @@ -542,7 +542,7 @@ nsresult U2FSoftTokenManager::IsRegistered(const nsTArray& aKeyHandle, MOZ_ASSERT(slot.get()); // Decode the key handle - UniqueSECKEYPrivateKey privKey = PrivateKeyFromKeyHandle( + UniqueSECKEYPrivateKey privKey = PrivateKeyFromKeyHandleOld( slot, mWrappingKey, const_cast(aKeyHandle.Elements()), aKeyHandle.Length(), const_cast(aAppParam.Elements()), aAppParam.Length()); @@ -570,8 +570,7 @@ nsresult U2FSoftTokenManager::IsRegistered(const nsTArray& aKeyHandle, // * attestation signature // RefPtr U2FSoftTokenManager::Register( - const WebAuthnMakeCredentialInfo& aInfo, bool aForceNoneAttestation, - void _ctap2_status_callback(rust_ctap2_status_update_res*)) { + const WebAuthnMakeCredentialInfo& aInfo, bool aForceNoneAttestation) { if (!mInitialized) { nsresult rv = Init(); if (NS_WARN_IF(NS_FAILED(rv))) { @@ -661,7 +660,7 @@ RefPtr U2FSoftTokenManager::Register( // Construct a one-time-use Attestation Certificate UniqueSECKEYPrivateKey attestPrivKey; UniqueCERTCertificate attestCert; - rv = GetAttestationCertificate(slot, attestPrivKey, attestCert); + rv = GetAttestationCertificateOld(slot, attestPrivKey, attestCert); if (NS_WARN_IF(NS_FAILED(rv))) { return U2FRegisterPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); } @@ -671,13 +670,13 @@ RefPtr U2FSoftTokenManager::Register( // Generate a new keypair; the private will be wrapped into a Key Handle UniqueSECKEYPrivateKey privKey; UniqueSECKEYPublicKey pubKey; - rv = GenEcKeypair(slot, privKey, pubKey); + rv = GenEcKeypairOld(slot, privKey, pubKey); if (NS_WARN_IF(NS_FAILED(rv))) { return U2FRegisterPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); } // The key handle will be the result of keywrap(privKey, key=mWrappingKey) - UniqueSECItem keyHandleItem = KeyHandleFromPrivateKey( + UniqueSECItem keyHandleItem = KeyHandleFromPrivateKeyOld( slot, mWrappingKey, const_cast(rpIdHash.Elements()), rpIdHash.Length(), privKey); if (NS_WARN_IF(!keyHandleItem.get())) { @@ -688,7 +687,7 @@ RefPtr U2FSoftTokenManager::Register( mozilla::dom::CryptoBuffer signedDataBuf; if (NS_WARN_IF(!signedDataBuf.SetCapacity( 1 + rpIdHash.Length() + clientDataHash.Length() + keyHandleItem->len + - kPublicKeyLen, + kPublicKeyLenOld, mozilla::fallible))) { return U2FRegisterPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__); @@ -707,7 +706,7 @@ RefPtr U2FSoftTokenManager::Register( signedDataBuf.Length(), attestPrivKey.get(), SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE); if (NS_WARN_IF(srv != SECSuccess)) { - MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + MOZ_LOG(gNSSTokenLogOld, LogLevel::Warning, ("Signature failure: %d", PORT_GetError())); return U2FRegisterPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); } @@ -715,7 +714,7 @@ RefPtr U2FSoftTokenManager::Register( // Serialize the registration data mozilla::dom::CryptoBuffer registrationBuf; if (NS_WARN_IF(!registrationBuf.SetCapacity( - 1 + kPublicKeyLen + 1 + keyHandleItem->len + + 1 + kPublicKeyLenOld + 1 + keyHandleItem->len + attestCert.get()->derCert.len + signatureItem.len, mozilla::fallible))) { return U2FRegisterPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, @@ -800,8 +799,7 @@ bool U2FSoftTokenManager::FindRegisteredKeyHandle( // * Signature // RefPtr U2FSoftTokenManager::Sign( - const WebAuthnGetAssertionInfo& aInfo, - void _ctap2_status_callback(rust_ctap2_status_update_res*)) { + const WebAuthnGetAssertionInfo& aInfo) { if (!mInitialized) { nsresult rv = Init(); if (NS_WARN_IF(NS_FAILED(rv))) { @@ -856,23 +854,23 @@ RefPtr U2FSoftTokenManager::Sign( UniquePK11SlotInfo slot(PK11_GetInternalSlot()); MOZ_ASSERT(slot.get()); - if (NS_WARN_IF((clientDataHash.Length() != kParamLen) || - (chosenAppId.Length() != kParamLen))) { - MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + if (NS_WARN_IF((clientDataHash.Length() != kParamLenOld) || + (chosenAppId.Length() != kParamLenOld))) { + MOZ_LOG(gNSSTokenLogOld, LogLevel::Warning, ("Parameter lengths are wrong! challenge=%d app=%d expected=%d", (uint32_t)clientDataHash.Length(), (uint32_t)chosenAppId.Length(), - kParamLen)); + kParamLenOld)); return U2FSignPromise::CreateAndReject(NS_ERROR_ILLEGAL_VALUE, __func__); } // Decode the key handle - UniqueSECKEYPrivateKey privKey = PrivateKeyFromKeyHandle( + UniqueSECKEYPrivateKey privKey = PrivateKeyFromKeyHandleOld( slot, mWrappingKey, const_cast(keyHandle.Elements()), keyHandle.Length(), const_cast(chosenAppId.Elements()), chosenAppId.Length()); if (NS_WARN_IF(!privKey.get())) { - MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Couldn't get the priv key!")); + MOZ_LOG(gNSSTokenLogOld, LogLevel::Warning, ("Couldn't get the priv key!")); return U2FSignPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); } @@ -892,7 +890,7 @@ RefPtr U2FSoftTokenManager::Sign( // Compute the signature mozilla::dom::CryptoBuffer signedDataBuf; - if (NS_WARN_IF(!signedDataBuf.SetCapacity(1 + 4 + (2 * kParamLen), + if (NS_WARN_IF(!signedDataBuf.SetCapacity(1 + 4 + (2 * kParamLenOld), mozilla::fallible))) { return U2FSignPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__); } @@ -906,7 +904,7 @@ RefPtr U2FSoftTokenManager::Sign( (void)signedDataBuf.AppendElements( clientDataHash.Elements(), clientDataHash.Length(), mozilla::fallible); - if (MOZ_LOG_TEST(gNSSTokenLog, LogLevel::Debug)) { + if (MOZ_LOG_TEST(gNSSTokenLogOld, LogLevel::Debug)) { nsAutoCString base64; nsresult rv = Base64URLEncode(signedDataBuf.Length(), signedDataBuf.Elements(), @@ -915,7 +913,7 @@ RefPtr U2FSoftTokenManager::Sign( return U2FSignPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); } - MOZ_LOG(gNSSTokenLog, LogLevel::Debug, + MOZ_LOG(gNSSTokenLogOld, LogLevel::Debug, ("U2F Token signing bytes (base64): %s", base64.get())); } @@ -924,7 +922,7 @@ RefPtr U2FSoftTokenManager::Sign( signedDataBuf.Length(), privKey.get(), SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE); if (NS_WARN_IF(srv != SECSuccess)) { - MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + MOZ_LOG(gNSSTokenLogOld, LogLevel::Warning, ("Signature failure: %d", PORT_GetError())); return U2FSignPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); } diff --git a/dom/webauthn/U2FSoftTokenManager.h b/dom/webauthn/U2FSoftTokenManager.h index d56d9b6bf1037..2807e28f2c31d 100644 --- a/dom/webauthn/U2FSoftTokenManager.h +++ b/dom/webauthn/U2FSoftTokenManager.h @@ -21,15 +21,10 @@ class U2FSoftTokenManager final : public U2FTokenTransport { public: explicit U2FSoftTokenManager(uint32_t aCounter); - RefPtr Register( - const WebAuthnMakeCredentialInfo& aInfo, bool aForceNoneAttestation, - void _ctap2_status_callback( - rust_ctap2_status_update_res* status)) override; - - RefPtr Sign( - const WebAuthnGetAssertionInfo& aInfo, - void _ctap2_status_callback( - rust_ctap2_status_update_res* status)) override; + RefPtr Register(const WebAuthnMakeCredentialInfo& aInfo, + bool aForceNoneAttestation) override; + + RefPtr Sign(const WebAuthnGetAssertionInfo& aInfo) override; void Cancel() override; diff --git a/dom/webauthn/U2FSoftTokenTransport.cpp b/dom/webauthn/U2FSoftTokenTransport.cpp new file mode 100644 index 0000000000000..06526aac97e0a --- /dev/null +++ b/dom/webauthn/U2FSoftTokenTransport.cpp @@ -0,0 +1,1018 @@ +/* -*- 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 "CtapResults.h" +#include "WebAuthnCoseIdentifiers.h" +#include "WebAuthnEnumStrings.h" +#include "U2FSoftTokenTransport.h" +#include "mozilla/dom/WebAuthnUtil.h" +#include "CryptoBuffer.h" +#include "mozilla/Base64.h" +#include "mozilla/Casting.h" +#include "mozilla/Preferences.h" +#include "nsNSSComponent.h" +#include "nsThreadUtils.h" +#include "pk11pub.h" +#include "prerror.h" +#include "secerr.h" +#include "WebCryptoCommon.h" + +#define PREF_U2F_NSSTOKEN_COUNTER "security.webauth.softtoken_counter" + +namespace mozilla::dom { + +using namespace mozilla; +using mozilla::dom::CreateECParamsForCurve; + +const nsCString U2FSoftTokenTransport::mSecretNickname = "U2F_NSSTOKEN"_ns; + +namespace { +constexpr auto kAttestCertSubjectName = "CN=Firefox U2F Soft Token"_ns; + +// This U2F-compatible soft token uses FIDO U2F-compatible ECDSA keypairs +// on the SEC_OID_SECG_EC_SECP256R1 curve. When asked to Register, it will +// generate and return a new keypair KP, where the private component is wrapped +// using AES-KW with the 128-bit mWrappingKey to make an opaque "key handle". +// In other words, Register yields { KP_pub, AES-KW(KP_priv, key=mWrappingKey) } +// +// The value mWrappingKey is long-lived; it is persisted as part of the NSS DB +// for the current profile. The attestation certificates that are produced are +// ephemeral to counteract profiling. They have little use for a soft-token +// at any rate, but are required by the specification. + +const uint32_t kParamLen = 32; +const uint32_t kPublicKeyLen = 65; +const uint32_t kWrappedKeyBufLen = 256; +const uint32_t kWrappingKeyByteLen = 128 / 8; +const uint32_t kSaltByteLen = 64 / 8; +const uint32_t kVersion1KeyHandleLen = 162; +constexpr auto kEcAlgorithm = + NS_LITERAL_STRING_FROM_CSTRING(WEBCRYPTO_NAMED_CURVE_P256); + +const PRTime kOneDay = PRTime(PR_USEC_PER_SEC) * PRTime(60) // sec + * PRTime(60) // min + * PRTime(24); // hours +const PRTime kExpirationSlack = kOneDay; // Pre-date for clock skew +const PRTime kExpirationLife = kOneDay; + +static mozilla::LazyLogModule gNSSTokenLog("webauthn_softtoken"); + +enum SoftTokenHandle { + Version1 = 0, +}; + +} // namespace + +NS_IMPL_ISUPPORTS(U2FSoftTokenTransport, nsIWebAuthnTransport) + +U2FSoftTokenTransport::U2FSoftTokenTransport(uint32_t aCounter) + : mInitialized(false), mCounter(aCounter), mController(nullptr) {} + +NS_IMETHODIMP +U2FSoftTokenTransport::GetController(nsIWebAuthnController** aController) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +U2FSoftTokenTransport::SetController(nsIWebAuthnController* aController) { + mController = aController; + return NS_OK; +} + +/** + * Gets the first key with the given nickname from the given slot. Any other + * keys found are not returned. + * PK11_GetNextSymKey() should not be called on the returned key. + * + * @param aSlot Slot to search. + * @param aNickname Nickname the key should have. + * @return The first key found. nullptr if no key could be found. + */ +static UniquePK11SymKey GetSymKeyByNickname(const UniquePK11SlotInfo& aSlot, + const nsCString& aNickname) { + MOZ_ASSERT(aSlot); + if (NS_WARN_IF(!aSlot)) { + return nullptr; + } + + MOZ_LOG(gNSSTokenLog, LogLevel::Debug, + ("Searching for a symmetric key named %s", aNickname.get())); + + UniquePK11SymKey keyListHead( + PK11_ListFixedKeysInSlot(aSlot.get(), const_cast(aNickname.get()), + /* wincx */ nullptr)); + if (NS_WARN_IF(!keyListHead)) { + MOZ_LOG(gNSSTokenLog, LogLevel::Debug, ("Symmetric key not found.")); + return nullptr; + } + + // Sanity check PK11_ListFixedKeysInSlot() only returns keys with the correct + // nickname. + MOZ_ASSERT(aNickname == + UniquePORTString(PK11_GetSymKeyNickname(keyListHead.get())).get()); + MOZ_LOG(gNSSTokenLog, LogLevel::Debug, ("Symmetric key found!")); + + // Free any remaining keys in the key list. + UniquePK11SymKey freeKey(PK11_GetNextSymKey(keyListHead.get())); + while (freeKey) { + freeKey = UniquePK11SymKey(PK11_GetNextSymKey(freeKey.get())); + } + + return keyListHead; +} + +static nsresult GenEcKeypair(const UniquePK11SlotInfo& aSlot, + /*out*/ UniqueSECKEYPrivateKey& aPrivKey, + /*out*/ UniqueSECKEYPublicKey& aPubKey) { + MOZ_ASSERT(aSlot); + if (NS_WARN_IF(!aSlot)) { + return NS_ERROR_INVALID_ARG; + } + + UniquePLArenaPool arena(PORT_NewArena(DER_DEFAULT_CHUNKSIZE)); + if (NS_WARN_IF(!arena)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Set the curve parameters; keyParams belongs to the arena memory space + SECItem* keyParams = CreateECParamsForCurve(kEcAlgorithm, arena.get()); + if (NS_WARN_IF(!keyParams)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Generate a key pair + CK_MECHANISM_TYPE mechanism = CKM_EC_KEY_PAIR_GEN; + + SECKEYPublicKey* pubKeyRaw; + aPrivKey = UniqueSECKEYPrivateKey( + PK11_GenerateKeyPair(aSlot.get(), mechanism, keyParams, &pubKeyRaw, + /* ephemeral */ false, false, + /* wincx */ nullptr)); + aPubKey = UniqueSECKEYPublicKey(pubKeyRaw); + pubKeyRaw = nullptr; + if (NS_WARN_IF(!aPrivKey.get() || !aPubKey.get())) { + return NS_ERROR_FAILURE; + } + + // Check that the public key has the correct length + if (NS_WARN_IF(aPubKey->u.ec.publicValue.len != kPublicKeyLen)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult U2FSoftTokenTransport::GetOrCreateWrappingKey( + const UniquePK11SlotInfo& aSlot) { + MOZ_ASSERT(aSlot); + if (NS_WARN_IF(!aSlot)) { + return NS_ERROR_INVALID_ARG; + } + + // Search for an existing wrapping key. If we find it, + // store it for later and mark ourselves initialized. + mWrappingKey = GetSymKeyByNickname(aSlot, mSecretNickname); + if (mWrappingKey) { + MOZ_LOG(gNSSTokenLog, LogLevel::Debug, ("U2F Soft Token Key found.")); + mInitialized = true; + return NS_OK; + } + + MOZ_LOG(gNSSTokenLog, LogLevel::Info, + ("No keys found. Generating new U2F Soft Token wrapping key.")); + + // We did not find an existing wrapping key, so we generate one in the + // persistent database (e.g, Token). + mWrappingKey = UniquePK11SymKey(PK11_TokenKeyGenWithFlags( + aSlot.get(), CKM_AES_KEY_GEN, + /* default params */ nullptr, kWrappingKeyByteLen, + /* empty keyid */ nullptr, + /* flags */ CKF_WRAP | CKF_UNWRAP, + /* attributes */ PK11_ATTR_TOKEN | PK11_ATTR_PRIVATE, + /* wincx */ nullptr)); + + if (NS_WARN_IF(!mWrappingKey)) { + MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + ("Failed to store wrapping key, NSS error #%d", PORT_GetError())); + return NS_ERROR_FAILURE; + } + + SECStatus srv = + PK11_SetSymKeyNickname(mWrappingKey.get(), mSecretNickname.get()); + if (NS_WARN_IF(srv != SECSuccess)) { + MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + ("Failed to set nickname, NSS error #%d", PORT_GetError())); + return NS_ERROR_FAILURE; + } + + MOZ_LOG(gNSSTokenLog, LogLevel::Debug, + ("Key stored, nickname set to %s.", mSecretNickname.get())); + + GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction( + "dom::U2FSoftTokenTransport::GetOrCreateWrappingKey", []() { + MOZ_ASSERT(NS_IsMainThread()); + Preferences::SetUint(PREF_U2F_NSSTOKEN_COUNTER, 0); + })); + + return NS_OK; +} + +static nsresult GetAttestationCertificate( + const UniquePK11SlotInfo& aSlot, + /*out*/ UniqueSECKEYPrivateKey& aAttestPrivKey, + /*out*/ UniqueCERTCertificate& aAttestCert) { + MOZ_ASSERT(aSlot); + if (NS_WARN_IF(!aSlot)) { + return NS_ERROR_INVALID_ARG; + } + + UniqueSECKEYPublicKey pubKey; + + // Construct an ephemeral keypair for this Attestation Certificate + nsresult rv = GenEcKeypair(aSlot, aAttestPrivKey, pubKey); + if (NS_WARN_IF(NS_FAILED(rv) || !aAttestPrivKey || !pubKey)) { + MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + ("Failed to gen keypair, NSS error #%d", PORT_GetError())); + return NS_ERROR_FAILURE; + } + + // Construct the Attestation Certificate itself + UniqueCERTName subjectName(CERT_AsciiToName(kAttestCertSubjectName.get())); + if (NS_WARN_IF(!subjectName)) { + MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + ("Failed to set subject name, NSS error #%d", PORT_GetError())); + return NS_ERROR_FAILURE; + } + + UniqueCERTSubjectPublicKeyInfo spki( + SECKEY_CreateSubjectPublicKeyInfo(pubKey.get())); + if (NS_WARN_IF(!spki)) { + MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + ("Failed to set SPKI, NSS error #%d", PORT_GetError())); + return NS_ERROR_FAILURE; + } + + UniqueCERTCertificateRequest certreq( + CERT_CreateCertificateRequest(subjectName.get(), spki.get(), nullptr)); + if (NS_WARN_IF(!certreq)) { + MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + ("Failed to gen CSR, NSS error #%d", PORT_GetError())); + return NS_ERROR_FAILURE; + } + + PRTime now = PR_Now(); + PRTime notBefore = now - kExpirationSlack; + PRTime notAfter = now + kExpirationLife; + + UniqueCERTValidity validity(CERT_CreateValidity(notBefore, notAfter)); + if (NS_WARN_IF(!validity)) { + MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + ("Failed to gen validity, NSS error #%d", PORT_GetError())); + return NS_ERROR_FAILURE; + } + + unsigned long serial; + unsigned char* serialBytes = + mozilla::BitwiseCast(&serial); + SECStatus srv = + PK11_GenerateRandomOnSlot(aSlot.get(), serialBytes, sizeof(serial)); + if (NS_WARN_IF(srv != SECSuccess)) { + MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + ("Failed to gen serial, NSS error #%d", PORT_GetError())); + return NS_ERROR_FAILURE; + } + // Ensure that the most significant bit isn't set (which would + // indicate a negative number, which isn't valid for serial + // numbers). + serialBytes[0] &= 0x7f; + // Also ensure that the least significant bit on the most + // significant byte is set (to prevent a leading zero byte, + // which also wouldn't be valid). + serialBytes[0] |= 0x01; + + aAttestCert = UniqueCERTCertificate(CERT_CreateCertificate( + serial, subjectName.get(), validity.get(), certreq.get())); + if (NS_WARN_IF(!aAttestCert)) { + MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + ("Failed to gen certificate, NSS error #%d", PORT_GetError())); + return NS_ERROR_FAILURE; + } + + PLArenaPool* arena = aAttestCert->arena; + + srv = SECOID_SetAlgorithmID(arena, &aAttestCert->signature, + SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE, + /* wincx */ nullptr); + if (NS_WARN_IF(srv != SECSuccess)) { + return NS_ERROR_FAILURE; + } + + // Set version to X509v3. + *(aAttestCert->version.data) = SEC_CERTIFICATE_VERSION_3; + aAttestCert->version.len = 1; + + SECItem innerDER = {siBuffer, nullptr, 0}; + if (NS_WARN_IF(!SEC_ASN1EncodeItem(arena, &innerDER, aAttestCert.get(), + SEC_ASN1_GET(CERT_CertificateTemplate)))) { + return NS_ERROR_FAILURE; + } + + SECItem* signedCert = PORT_ArenaZNew(arena, SECItem); + if (NS_WARN_IF(!signedCert)) { + return NS_ERROR_FAILURE; + } + + srv = SEC_DerSignData(arena, signedCert, innerDER.data, innerDER.len, + aAttestPrivKey.get(), + SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE); + if (NS_WARN_IF(srv != SECSuccess)) { + return NS_ERROR_FAILURE; + } + aAttestCert->derCert = *signedCert; + + MOZ_LOG(gNSSTokenLog, LogLevel::Debug, + ("U2F Soft Token attestation certificate generated.")); + return NS_OK; +} + +// Set up the context for the soft U2F Token. This is called by NSS +// initialization. +nsresult U2FSoftTokenTransport::Init() { + // If we've already initialized, just return. + if (mInitialized) { + return NS_OK; + } + + UniquePK11SlotInfo slot(PK11_GetInternalKeySlot()); + MOZ_ASSERT(slot.get()); + + // Search for an existing wrapping key, or create one. + nsresult rv = GetOrCreateWrappingKey(slot); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mInitialized = true; + MOZ_LOG(gNSSTokenLog, LogLevel::Debug, ("U2F Soft Token initialized.")); + return NS_OK; +} + +// Convert a Private Key object into an opaque key handle, using AES Key Wrap +// with the long-lived aPersistentKey mixed with aAppParam to convert aPrivKey. +// The key handle's format is version || saltLen || salt || wrappedPrivateKey +static UniqueSECItem KeyHandleFromPrivateKey( + const UniquePK11SlotInfo& aSlot, const UniquePK11SymKey& aPersistentKey, + uint8_t* aAppParam, uint32_t aAppParamLen, + const UniqueSECKEYPrivateKey& aPrivKey) { + MOZ_ASSERT(aSlot); + MOZ_ASSERT(aPersistentKey); + MOZ_ASSERT(aAppParam); + MOZ_ASSERT(aPrivKey); + if (NS_WARN_IF(!aSlot || !aPersistentKey || !aPrivKey || !aAppParam)) { + return nullptr; + } + + // Generate a random salt + uint8_t saltParam[kSaltByteLen]; + SECStatus srv = + PK11_GenerateRandomOnSlot(aSlot.get(), saltParam, sizeof(saltParam)); + if (NS_WARN_IF(srv != SECSuccess)) { + MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + ("Failed to generate a salt, NSS error #%d", PORT_GetError())); + return nullptr; + } + + // Prepare the HKDF (https://tools.ietf.org/html/rfc5869) + CK_NSS_HKDFParams hkdfParams = {true, saltParam, sizeof(saltParam), + true, aAppParam, aAppParamLen}; + SECItem kdfParams = {siBuffer, (unsigned char*)&hkdfParams, + sizeof(hkdfParams)}; + + // Derive a wrapping key from aPersistentKey, the salt, and the aAppParam. + // CKM_AES_KEY_GEN and CKA_WRAP are key type and usage attributes of the + // derived symmetric key and don't matter because we ignore them anyway. + UniquePK11SymKey wrapKey( + PK11_Derive(aPersistentKey.get(), CKM_NSS_HKDF_SHA256, &kdfParams, + CKM_AES_KEY_GEN, CKA_WRAP, kWrappingKeyByteLen)); + if (NS_WARN_IF(!wrapKey.get())) { + MOZ_LOG( + gNSSTokenLog, LogLevel::Warning, + ("Failed to derive a wrapping key, NSS error #%d", PORT_GetError())); + return nullptr; + } + + UniqueSECItem wrappedKey(::SECITEM_AllocItem(/* default arena */ nullptr, + /* no buffer */ nullptr, + kWrappedKeyBufLen)); + if (NS_WARN_IF(!wrappedKey)) { + MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Failed to allocate memory")); + return nullptr; + } + + UniqueSECItem param(PK11_ParamFromIV(CKM_NSS_AES_KEY_WRAP_PAD, + /* default IV */ nullptr)); + + srv = + PK11_WrapPrivKey(aSlot.get(), wrapKey.get(), aPrivKey.get(), + CKM_NSS_AES_KEY_WRAP_PAD, param.get(), wrappedKey.get(), + /* wincx */ nullptr); + if (NS_WARN_IF(srv != SECSuccess)) { + MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + ("Failed to wrap U2F key, NSS error #%d", PORT_GetError())); + return nullptr; + } + + // Concatenate the salt and the wrapped Private Key together + mozilla::dom::CryptoBuffer keyHandleBuf; + if (NS_WARN_IF(!keyHandleBuf.SetCapacity( + wrappedKey.get()->len + sizeof(saltParam) + 2, mozilla::fallible))) { + MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Failed to allocate memory")); + return nullptr; + } + + // It's OK to ignore the return values here because we're writing into + // pre-allocated space + (void)keyHandleBuf.AppendElement(SoftTokenHandle::Version1, + mozilla::fallible); + (void)keyHandleBuf.AppendElement(sizeof(saltParam), mozilla::fallible); + (void)keyHandleBuf.AppendElements(saltParam, sizeof(saltParam), + mozilla::fallible); + keyHandleBuf.AppendSECItem(wrappedKey.get()); + + UniqueSECItem keyHandle(::SECITEM_AllocItem(nullptr, nullptr, 0)); + if (NS_WARN_IF(!keyHandle)) { + MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Failed to allocate memory")); + return nullptr; + } + + if (NS_WARN_IF(!keyHandleBuf.ToSECItem(/* default arena */ nullptr, + keyHandle.get()))) { + MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Failed to allocate memory")); + return nullptr; + } + return keyHandle; +} + +// Convert an opaque key handle aKeyHandle back into a Private Key object, using +// the long-lived aPersistentKey mixed with aAppParam and the AES Key Wrap +// algorithm. +static UniqueSECKEYPrivateKey PrivateKeyFromKeyHandle( + const UniquePK11SlotInfo& aSlot, const UniquePK11SymKey& aPersistentKey, + uint8_t* aKeyHandle, uint32_t aKeyHandleLen, uint8_t* aAppParam, + uint32_t aAppParamLen) { + MOZ_ASSERT(aSlot); + MOZ_ASSERT(aPersistentKey); + MOZ_ASSERT(aKeyHandle); + MOZ_ASSERT(aAppParam); + MOZ_ASSERT(aAppParamLen == SHA256_LENGTH); + if (NS_WARN_IF(!aSlot || !aPersistentKey || !aKeyHandle || !aAppParam || + aAppParamLen != SHA256_LENGTH)) { + return nullptr; + } + + // As we only support one key format ourselves (right now), fail early if + // we aren't that length + if (NS_WARN_IF(aKeyHandleLen != kVersion1KeyHandleLen)) { + return nullptr; + } + + if (NS_WARN_IF(aKeyHandle[0] != SoftTokenHandle::Version1)) { + // Unrecognized version + return nullptr; + } + + uint8_t saltLen = aKeyHandle[1]; + uint8_t* saltPtr = aKeyHandle + 2; + if (NS_WARN_IF(saltLen != kSaltByteLen)) { + return nullptr; + } + + // Prepare the HKDF (https://tools.ietf.org/html/rfc5869) + CK_NSS_HKDFParams hkdfParams = {true, saltPtr, saltLen, + true, aAppParam, aAppParamLen}; + SECItem kdfParams = {siBuffer, (unsigned char*)&hkdfParams, + sizeof(hkdfParams)}; + + // Derive a wrapping key from aPersistentKey, the salt, and the aAppParam. + // CKM_AES_KEY_GEN and CKA_WRAP are key type and usage attributes of the + // derived symmetric key and don't matter because we ignore them anyway. + UniquePK11SymKey wrapKey( + PK11_Derive(aPersistentKey.get(), CKM_NSS_HKDF_SHA256, &kdfParams, + CKM_AES_KEY_GEN, CKA_WRAP, kWrappingKeyByteLen)); + if (NS_WARN_IF(!wrapKey.get())) { + MOZ_LOG( + gNSSTokenLog, LogLevel::Warning, + ("Failed to derive a wrapping key, NSS error #%d", PORT_GetError())); + return nullptr; + } + + uint8_t wrappedLen = aKeyHandleLen - saltLen - 2; + uint8_t* wrappedPtr = aKeyHandle + saltLen + 2; + + ScopedAutoSECItem wrappedKeyItem(wrappedLen); + memcpy(wrappedKeyItem.data, wrappedPtr, wrappedKeyItem.len); + + ScopedAutoSECItem pubKey(kPublicKeyLen); + + UniqueSECItem param(PK11_ParamFromIV(CKM_NSS_AES_KEY_WRAP_PAD, + /* default IV */ nullptr)); + + CK_ATTRIBUTE_TYPE usages[] = {CKA_SIGN}; + int usageCount = 1; + + UniqueSECKEYPrivateKey unwrappedKey( + PK11_UnwrapPrivKey(aSlot.get(), wrapKey.get(), CKM_NSS_AES_KEY_WRAP_PAD, + param.get(), &wrappedKeyItem, + /* no nickname */ nullptr, + /* discard pubkey */ &pubKey, + /* not permanent */ false, + /* non-exportable */ true, CKK_EC, usages, usageCount, + /* wincx */ nullptr)); + if (NS_WARN_IF(!unwrappedKey)) { + // Not our key. + MOZ_LOG(gNSSTokenLog, LogLevel::Debug, + ("Could not unwrap key handle, NSS Error #%d", PORT_GetError())); + return nullptr; + } + + return unwrappedKey; +} + +// IsRegistered determines if the provided key handle is usable by this token. +nsresult U2FSoftTokenTransport::IsRegistered( + const nsTArray& aKeyHandle, const nsTArray& aAppParam, + bool& aResult) { + if (!mInitialized) { + nsresult rv = Init(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + UniquePK11SlotInfo slot(PK11_GetInternalSlot()); + MOZ_ASSERT(slot.get()); + + // Decode the key handle + UniqueSECKEYPrivateKey privKey = PrivateKeyFromKeyHandle( + slot, mWrappingKey, const_cast(aKeyHandle.Elements()), + aKeyHandle.Length(), const_cast(aAppParam.Elements()), + aAppParam.Length()); + aResult = privKey.get() != nullptr; + return NS_OK; +} + +// A U2F Register operation causes a new key pair to be generated by the token. +// The token then returns the public key of the key pair, and a handle to the +// private key, which is a fancy way of saying "key wrapped private key", as +// well as the generated attestation certificate and a signature using that +// certificate's private key. +// +// The KeyHandleFromPrivateKey and PrivateKeyFromKeyHandle methods perform +// the actual key wrap/unwrap operations. +// +// The format of the return registration data is as follows: +// +// Bytes Value +// 1 0x05 +// 65 public key +// 1 key handle length +// * key handle +// ASN.1 attestation certificate +// * attestation signature +// +NS_IMETHODIMP +U2FSoftTokenTransport::MakeCredential(uint64_t aTransactionId, + uint64_t _aBrowsingContextId, + nsICtapRegisterArgs* args) { + nsresult rv; + + if (!mInitialized) { + rv = Init(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (NS_WARN_IF(!args)) { + return NS_ERROR_FAILURE; + } + + bool requireResidentKey; + // Bug 1737205 will make this infallible + rv = args->GetRequireResidentKey(&requireResidentKey); + if (NS_FAILED(rv)) { + requireResidentKey = false; + } + + bool requireUserVerification = false; + nsString userVerification; + // Bug 1737205 will make this infallible + rv = args->GetUserVerification(userVerification); + if (NS_SUCCEEDED(rv)) { + requireUserVerification = userVerification.EqualsLiteral( + MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED); + } + + bool requirePlatformAttachment = false; + nsString authenticatorAttachment; + // Bug 1737205 will make this infallible + rv = args->GetAuthenticatorAttachment(authenticatorAttachment); + if (NS_SUCCEEDED(rv) && authenticatorAttachment.EqualsLiteral( + MOZ_WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM)) { + requirePlatformAttachment = true; + } + + // The U2F softtoken neither supports resident keys or + // user verification, nor is it a platform authenticator. + if (requireResidentKey || requireUserVerification || + requirePlatformAttachment) { + return NS_ERROR_DOM_NOT_ALLOWED_ERR; + } + + bool noneAttestationRequested = false; + nsString attestationConveyancePreference; + rv = + args->GetAttestationConveyancePreference(attestationConveyancePreference); + if (NS_SUCCEEDED(rv) && + attestationConveyancePreference.EqualsLiteral( + MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE)) { + noneAttestationRequested = true; + } + + nsTArray coseAlgs; + // Bug 1737205 will make this infallible + rv = args->GetCoseAlgs(coseAlgs); + // If the request does not list algs, assume ES256. + if (NS_FAILED(rv) || coseAlgs.IsEmpty()) { + coseAlgs.AppendElement( + static_cast(CoseAlgorithmIdentifier::ES256)); + } + // This token only supports ES256. + coseAlgs.RemoveElementsBy([](auto& alg) { + return alg != static_cast(CoseAlgorithmIdentifier::ES256); + }); + // If there are no acceptable/supported algorithms, exit + if (coseAlgs.IsEmpty()) { + return NS_ERROR_DOM_NOT_SUPPORTED_ERR; + } + + nsString rpId; + args->GetRpId(rpId); + + nsCString clientDataJson; + args->GetClientDataJSON(clientDataJson); + + CryptoBuffer rpIdHash, clientDataHash; + rv = BuildTransactionHashes(NS_ConvertUTF16toUTF8(rpId), clientDataJson, + rpIdHash, clientDataHash); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_DOM_UNKNOWN_ERR; + } + + // Optional exclusion list. + nsTArray> excludeList; + args->GetExcludeList(excludeList); + for (const auto& credId : excludeList) { + bool isRegistered = false; + nsresult rv = IsRegistered(credId, rpIdHash, isRegistered); + if (NS_FAILED(rv)) { + return rv; + } + if (isRegistered) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + } + + // We should already have a wrapping key + MOZ_ASSERT(mWrappingKey); + + UniquePK11SlotInfo slot(PK11_GetInternalSlot()); + MOZ_ASSERT(slot.get()); + + // Construct a one-time-use Attestation Certificate + UniqueSECKEYPrivateKey attestPrivKey; + UniqueCERTCertificate attestCert; + rv = GetAttestationCertificate(slot, attestPrivKey, attestCert); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_FAILURE; + } + MOZ_ASSERT(attestCert); + MOZ_ASSERT(attestPrivKey); + + // Generate a new keypair; the private will be wrapped into a Key Handle + UniqueSECKEYPrivateKey privKey; + UniqueSECKEYPublicKey pubKey; + rv = GenEcKeypair(slot, privKey, pubKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_FAILURE; + } + + // The key handle will be the result of keywrap(privKey, key=mWrappingKey) + UniqueSECItem keyHandleItem = KeyHandleFromPrivateKey( + slot, mWrappingKey, const_cast(rpIdHash.Elements()), + rpIdHash.Length(), privKey); + if (NS_WARN_IF(!keyHandleItem.get())) { + return NS_ERROR_FAILURE; + } + + // Sign the challenge using the Attestation privkey (from attestCert) + mozilla::dom::CryptoBuffer signedDataBuf; + if (NS_WARN_IF(!signedDataBuf.SetCapacity( + 1 + rpIdHash.Length() + clientDataHash.Length() + keyHandleItem->len + + kPublicKeyLen, + mozilla::fallible))) { + return NS_ERROR_FAILURE; + } + + // // It's OK to ignore the return values here because we're writing into + // // pre-allocated space + (void)signedDataBuf.AppendElement(0x00, mozilla::fallible); + (void)signedDataBuf.AppendElements(rpIdHash, mozilla::fallible); + (void)signedDataBuf.AppendElements(clientDataHash, mozilla::fallible); + signedDataBuf.AppendSECItem(keyHandleItem.get()); + signedDataBuf.AppendSECItem(pubKey->u.ec.publicValue); + + ScopedAutoSECItem signatureItem; + SECStatus srv = SEC_SignData(&signatureItem, signedDataBuf.Elements(), + signedDataBuf.Length(), attestPrivKey.get(), + SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE); + if (NS_WARN_IF(srv != SECSuccess)) { + MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + ("Signature failure: %d", PORT_GetError())); + return NS_ERROR_FAILURE; + } + + CryptoBuffer keyHandleBuf; + if (!keyHandleBuf.AppendSECItem(keyHandleItem.get())) { + return NS_ERROR_FAILURE; + } + + CryptoBuffer attestCertBuf; + if (!attestCertBuf.AppendSECItem(attestCert.get()->derCert)) { + return NS_ERROR_FAILURE; + } + + CryptoBuffer signatureBuf; + if (!signatureBuf.AppendSECItem(signatureItem)) { + return NS_ERROR_FAILURE; + } + + CryptoBuffer pubKeyBuf; + if (!pubKeyBuf.AppendSECItem(pubKey->u.ec.publicValue)) { + return NS_ERROR_FAILURE; + } + + CryptoBuffer attObj; + rv = AssembleAttestationObject(rpIdHash, pubKeyBuf, keyHandleBuf, + attestCertBuf, signatureBuf, + noneAttestationRequested, attObj); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + + nsTArray outAttObj(std::move(attObj)); + nsTArray outKeyHandleBuf(std::move(keyHandleBuf)); + mController->FinishRegister( + aTransactionId, + new CtapRegisterResult(NS_OK, std::move(clientDataJson), + std::move(outAttObj), std::move(outKeyHandleBuf))); + return NS_OK; +} + +bool U2FSoftTokenTransport::FindRegisteredKeyHandle( + const nsTArray>& aAppIds, + const nsTArray>& aCredentialIds, + /*out*/ nsTArray& aKeyHandle, + /*out*/ nsTArray& aAppId) { + for (const nsTArray& app_id : aAppIds) { + for (const nsTArray& credId : aCredentialIds) { + bool isRegistered = false; + nsresult rv = IsRegistered(credId, app_id, isRegistered); + if (NS_SUCCEEDED(rv) && isRegistered) { + aKeyHandle.Clear(); + aKeyHandle.AppendElements(credId); + aAppId.Assign(app_id); + return true; + } + } + } + + return false; +} + +// A U2F Sign operation creates a signature over the "param" arguments (plus +// some other stuff) using the private key indicated in the key handle argument. +// +// The format of the signed data is as follows: +// +// 32 Application parameter +// 1 User presence (0x01) +// 4 Counter +// 32 Challenge parameter +// +// The format of the signature data is as follows: +// +// 1 User presence +// 4 Counter +// * Signature +// +NS_IMETHODIMP +U2FSoftTokenTransport::GetAssertion(uint64_t aTransactionId, + uint64_t _aBrowsingContextId, + nsICtapSignArgs* args) { + nsresult rv; + if (!mInitialized) { + rv = Init(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (NS_WARN_IF(!args)) { + return NS_ERROR_FAILURE; + } + + bool requireUserVerification = false; + nsString userVerification; + // Bug 1737205 will make this infallible + rv = args->GetUserVerification(userVerification); + if (NS_SUCCEEDED(rv)) { + requireUserVerification = userVerification.EqualsLiteral( + MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED); + } + if (requireUserVerification) { + return NS_ERROR_DOM_NOT_ALLOWED_ERR; + } + + nsCString clientDataJson; + args->GetClientDataJSON(clientDataJson); + + nsString rpId; + args->GetRpId(rpId); + + CryptoBuffer rpIdHash, clientDataHash; + rv = BuildTransactionHashes(NS_ConvertUTF16toUTF8(rpId), clientDataJson, + rpIdHash, clientDataHash); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_DOM_UNKNOWN_ERR; + } + + nsTArray> appIdHashes; + appIdHashes.AppendElement(std::move(rpIdHash)); + + nsTArray appIdHash; + rv = args->GetAppIdHash(appIdHash); + if (NS_SUCCEEDED(rv)) { + appIdHashes.AppendElement(std::move(appIdHash)); + } + + nsTArray> allowList; + args->GetAllowList(allowList); + + nsTArray chosenAppId; + nsTArray keyHandle; + + // Fail if we can't find a valid key handle. + if (!FindRegisteredKeyHandle(appIdHashes, allowList, keyHandle, + chosenAppId)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + MOZ_ASSERT(mWrappingKey); + + UniquePK11SlotInfo slot(PK11_GetInternalSlot()); + MOZ_ASSERT(slot.get()); + + if (NS_WARN_IF((clientDataHash.Length() != kParamLen) || + (chosenAppId.Length() != kParamLen))) { + MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + ("Parameter lengths are wrong! challenge=%d app=%d expected=%d", + (uint32_t)clientDataHash.Length(), (uint32_t)chosenAppId.Length(), + kParamLen)); + + return NS_ERROR_ILLEGAL_VALUE; + } + + // Decode the key handle + UniqueSECKEYPrivateKey privKey = PrivateKeyFromKeyHandle( + slot, mWrappingKey, const_cast(keyHandle.Elements()), + keyHandle.Length(), const_cast(chosenAppId.Elements()), + chosenAppId.Length()); + if (NS_WARN_IF(!privKey.get())) { + MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Couldn't get the priv key!")); + return NS_ERROR_FAILURE; + } + + // Increment the counter and turn it into a SECItem + mCounter += 1; + ScopedAutoSECItem counterItem(4); + counterItem.data[0] = (mCounter >> 24) & 0xFF; + counterItem.data[1] = (mCounter >> 16) & 0xFF; + counterItem.data[2] = (mCounter >> 8) & 0xFF; + counterItem.data[3] = (mCounter >> 0) & 0xFF; + uint32_t counter = mCounter; + GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction( + "dom::U2FSoftTokenTransport::GetAssertion", [counter]() { + MOZ_ASSERT(NS_IsMainThread()); + Preferences::SetUint(PREF_U2F_NSSTOKEN_COUNTER, counter); + })); + + // Compute the signature + mozilla::dom::CryptoBuffer signedDataBuf; + if (NS_WARN_IF(!signedDataBuf.SetCapacity(1 + 4 + (2 * kParamLen), + mozilla::fallible))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // It's OK to ignore the return values here because we're writing into + // pre-allocated space + (void)signedDataBuf.AppendElements(chosenAppId.Elements(), + chosenAppId.Length(), mozilla::fallible); + (void)signedDataBuf.AppendElement(0x01, mozilla::fallible); + signedDataBuf.AppendSECItem(counterItem); + (void)signedDataBuf.AppendElements( + clientDataHash.Elements(), clientDataHash.Length(), mozilla::fallible); + + if (MOZ_LOG_TEST(gNSSTokenLog, LogLevel::Debug)) { + nsAutoCString base64; + rv = Base64URLEncode(signedDataBuf.Length(), signedDataBuf.Elements(), + Base64URLEncodePaddingPolicy::Omit, base64); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_FAILURE; + } + + MOZ_LOG(gNSSTokenLog, LogLevel::Debug, + ("U2F Token signing bytes (base64): %s", base64.get())); + } + + ScopedAutoSECItem signatureItem; + SECStatus srv = SEC_SignData(&signatureItem, signedDataBuf.Elements(), + signedDataBuf.Length(), privKey.get(), + SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE); + if (NS_WARN_IF(srv != SECSuccess)) { + MOZ_LOG(gNSSTokenLog, LogLevel::Warning, + ("Signature failure: %d", PORT_GetError())); + return NS_ERROR_FAILURE; + } + + nsTArray extensions; + + if (appIdHashes.Length() == 2) { + bool usedAppId = (chosenAppId == appIdHashes[1]); + extensions.AppendElement(WebAuthnExtensionResultAppId(usedAppId)); + } + + CryptoBuffer counterBuf; + if (!counterBuf.AppendSECItem(counterItem)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + CryptoBuffer signatureBuf; + if (!signatureBuf.AppendSECItem(signatureItem)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + CryptoBuffer chosenAppIdBuf; + if (!chosenAppIdBuf.Assign(chosenAppId)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + CryptoBuffer authenticatorData; + CryptoBuffer emptyAttestationData; + rv = AssembleAuthenticatorData(chosenAppIdBuf, 0x01, counterBuf, + emptyAttestationData, authenticatorData); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_FAILURE; + } + + nsTArray outSignatureBuf(std::move(signatureBuf)); + nsTArray outAuthenticatorData(std::move(authenticatorData)); + nsTArray userHandle; // unused because this is a CTAP1 token + + nsTArray> results; + results.AppendElement(new CtapSignResult( + NS_OK, std::move(keyHandle), std::move(outSignatureBuf), + std::move(outAuthenticatorData), std::move(userHandle), + std::move(chosenAppId))); + + mController->FinishSign(aTransactionId, clientDataJson, results); + return NS_OK; +} + +NS_IMETHODIMP +U2FSoftTokenTransport::Cancel(void) { + // This implementation is sync, requests can't be aborted. + return NS_OK; +} + +NS_IMETHODIMP +U2FSoftTokenTransport::PinCallback(uint64_t aTransactionId, + const nsACString& aPin) { + // This is a U2F/CTAP1 token. It doesn't support pins. + return NS_ERROR_FAILURE; +} + +} // namespace mozilla::dom diff --git a/dom/webauthn/U2FSoftTokenTransport.h b/dom/webauthn/U2FSoftTokenTransport.h new file mode 100644 index 0000000000000..9cf4dfda718b9 --- /dev/null +++ b/dom/webauthn/U2FSoftTokenTransport.h @@ -0,0 +1,53 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_U2FSoftTokenTransport_h +#define mozilla_dom_U2FSoftTokenTransport_h + +#include "nsIWebAuthnController.h" +#include "ScopedNSSTypes.h" + +/* + * U2FSoftTokenManager is a software implementation of a secure token manager + * for the U2F and WebAuthn APIs. + */ + +namespace mozilla::dom { + +class U2FSoftTokenTransport final : public nsIWebAuthnTransport { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIWEBAUTHNTRANSPORT + + explicit U2FSoftTokenTransport(uint32_t aCounter); + + private: + ~U2FSoftTokenTransport() = default; + nsresult Init(); + + nsresult IsRegistered(const nsTArray& aKeyHandle, + const nsTArray& aAppParam, bool& aResult); + + bool FindRegisteredKeyHandle( + const nsTArray>& aAppIds, + const nsTArray>& aCredentialIds, + /*out*/ nsTArray& aKeyHandle, + /*out*/ nsTArray& aAppId); + + bool mInitialized; + mozilla::UniquePK11SymKey mWrappingKey; + + static const nsCString mSecretNickname; + + nsresult GetOrCreateWrappingKey(const mozilla::UniquePK11SlotInfo& aSlot); + uint32_t mCounter; + + nsCOMPtr mController; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_U2FSoftTokenTransport_h diff --git a/dom/webauthn/U2FTokenManager.cpp b/dom/webauthn/U2FTokenManager.cpp index cddf3e7e3137c..804f9afd25b13 100644 --- a/dom/webauthn/U2FTokenManager.cpp +++ b/dom/webauthn/U2FTokenManager.cpp @@ -6,7 +6,6 @@ #include "json/json.h" #include "mozilla/dom/U2FTokenManager.h" #include "mozilla/dom/U2FTokenTransport.h" -#include "mozilla/dom/CTAPHIDTokenManager.h" #include "mozilla/dom/U2FHIDTokenManager.h" #include "mozilla/dom/U2FSoftTokenManager.h" #include "mozilla/dom/PWebAuthnTransactionParent.h" @@ -41,7 +40,6 @@ "security.webauth.webauthn_testing_allow_direct_attestation" #define PREF_WEBAUTHN_ANDROID_FIDO2_ENABLED \ "security.webauth.webauthn_enable_android_fido2" -#define PREF_WEBAUTHN_CTAP2 "security.webauthn.ctap2" namespace mozilla::dom { /*********************************************************************** @@ -58,29 +56,17 @@ static nsIThread* gBackgroundThread; } // namespace // Data for WebAuthn UI prompt notifications. -static const char16_t kRegisterPromptNotifcation[] = - u"{\"action\":\"register\",\"tid\":%llu,\"origin\":\"%s\"," - u"\"browsingContextId\":%llu,\"is_ctap2\":%s, \"device_selected\":%s}"; -static const char16_t kRegisterDirectPromptNotifcation[] = - u"{\"action\":\"register-direct\",\"tid\":%llu,\"origin\":\"%s\"," - u"\"browsingContextId\":%llu}"; -static const char16_t kSignPromptNotifcation[] = - u"{\"action\":\"sign\",\"tid\":%llu,\"origin\":\"%s\"," - u"\"browsingContextId\":%llu,\"is_ctap2\":%s, \"device_selected\":%s}"; -static const char16_t kCancelPromptNotifcation[] = - u"{\"action\":\"cancel\",\"tid\":%llu}"; -static const char16_t kPinRequiredNotifcation[] = - u"{\"action\":\"pin-required\",\"tid\":%llu,\"origin\":\"%s\"," - u"\"browsingContextId\":%llu,\"wasInvalid\":%s,\"retriesLeft\":%i}"; -static const char16_t kSelectDeviceNotifcation[] = - u"{\"action\":\"select-device\",\"tid\":%llu,\"origin\":\"%s\"," - u"\"browsingContextId\":%llu}"; -static const char16_t kSelectSignResultNotifcation[] = - u"{\"action\":\"select-sign-result\",\"tid\":%llu,\"origin\":\"%s\"," - u"\"browsingContextId\":%llu,\"usernames\":[%s]}"; -static const char16_t kPinErrorNotifications[] = - u"{\"action\":\"%s\",\"tid\":%llu,\"origin\":\"%s\"," - u"\"browsingContextId\":%llu}"; +static const char16_t kRegisterPromptNotificationU2F[] = + u"{\"is_ctap2\":false,\"action\":\"register\",\"tid\":%llu," + u"\"origin\":\"%s\",\"browsingContextId\":%llu,\"device_selected\":%s}"; +static const char16_t kRegisterDirectPromptNotificationU2F[] = + u"{\"is_ctap2\":false,\"action\":\"register-direct\",\"tid\":%llu," + u"\"origin\":\"%s\",\"browsingContextId\":%llu}"; +static const char16_t kSignPromptNotificationU2F[] = + u"{\"is_ctap2\":false,\"action\":\"sign\",\"tid\":%llu,\"origin\":\"%s\"," + u"\"browsingContextId\":%llu,\"device_selected\":%s}"; +static const char16_t kCancelPromptNotificationU2F[] = + u"{\"is_ctap2\":false,\"action\":\"cancel\",\"tid\":%llu}"; class U2FPrefManager final : public nsIObserver { private: @@ -103,7 +89,6 @@ class U2FPrefManager final : public nsIObserver { PREF_WEBAUTHN_ANDROID_FIDO2_ENABLED); Preferences::AddStrongObserver(gPrefManager, PREF_WEBAUTHN_ALLOW_DIRECT_ATTESTATION); - Preferences::AddStrongObserver(gPrefManager, PREF_WEBAUTHN_CTAP2); ClearOnShutdown(&gPrefManager, ShutdownPhase::XPCOMShutdownThreads); } return gPrefManager; @@ -116,7 +101,7 @@ class U2FPrefManager final : public nsIObserver { return mSoftTokenEnabled; } - int GetSoftTokenCounter() { + uint32_t GetSoftTokenCounter() { MutexAutoLock lock(mPrefMutex); return mSoftTokenCounter; } @@ -136,10 +121,7 @@ class U2FPrefManager final : public nsIObserver { return mAllowDirectAttestation; } - bool GetIsCtap2() { - MutexAutoLock lock(mPrefMutex); - return mCtap2; - } + bool GetIsCtap2() { return false; } NS_IMETHODIMP Observe(nsISupports* aSubject, const char* aTopic, @@ -159,16 +141,14 @@ class U2FPrefManager final : public nsIObserver { Preferences::GetBool(PREF_WEBAUTHN_ANDROID_FIDO2_ENABLED); mAllowDirectAttestation = Preferences::GetBool(PREF_WEBAUTHN_ALLOW_DIRECT_ATTESTATION); - mCtap2 = Preferences::GetBool(PREF_WEBAUTHN_CTAP2); } Mutex mPrefMutex MOZ_UNANNOTATED; bool mSoftTokenEnabled; - int mSoftTokenCounter; + uint32_t mSoftTokenCounter; bool mUsbTokenEnabled; bool mAndroidFido2Enabled; bool mAllowDirectAttestation; - bool mCtap2; }; NS_IMPL_ISUPPORTS(U2FPrefManager, nsIObserver); @@ -235,14 +215,10 @@ void U2FTokenManager::MaybeClearTransaction( void U2FTokenManager::ClearTransaction(bool send_cancel) { if (mLastTransactionId && send_cancel) { // Remove any prompts we might be showing for the current transaction. - SendPromptNotification(kCancelPromptNotifcation, mLastTransactionId); + SendPromptNotification(kCancelPromptNotificationU2F, mLastTransactionId); } - mTransactionParent = nullptr; - // We have to "hang up" in case auth-rs is still waiting for us to send a PIN - // so it can exit cleanly - status_update_result = nullptr; // Drop managers at the end of all transactions if (mTokenManagerImpl) { mTokenManagerImpl->Drop(); @@ -291,111 +267,6 @@ void U2FTokenManager::RunSendPromptNotification(const nsString& aJSON) { os->NotifyObservers(self, "webauthn-prompt", aJSON.get())); } -void U2FTokenManager::StatusUpdateResFreePolicy::operator()( - rust_ctap2_status_update_res* p) { - rust_ctap2_destroy_status_update_res(p); -} - -static void status_callback(rust_ctap2_status_update_res* status) { - if (!U2FPrefManager::Get()->GetIsCtap2() || (NS_WARN_IF(!status))) { - return; - } - - // The result will be cleared automatically upon exiting this function, - // unless we have a Pin error, then we need it for a callback from JS. - // Then we move ownership of it to U2FTokenManager. - UniquePtr - status_result_update(status); - size_t len; - if (NS_WARN_IF(!rust_ctap2_status_update_len(status, &len))) { - return; - } - nsCString st; - if (NS_WARN_IF(!st.SetLength(len, fallible))) { - return; - } - if (NS_WARN_IF(!rust_ctap2_status_update_copy_json(status, st.Data()))) { - return; - } - - auto* gInstance = U2FTokenManager::Get(); - - Json::Value jsonRoot; - Json::Reader reader; - if (NS_WARN_IF(!reader.parse(st.Data(), jsonRoot))) { - return; - } - if (NS_WARN_IF(!jsonRoot.isObject())) { - return; - } - - nsAutoString notification_json; - - uint64_t browsingCtxId = gInstance->GetCurrentBrowsingCtxId().value(); - NS_ConvertUTF16toUTF8 origin(gInstance->GetCurrentOrigin().value()); - if (jsonRoot.isMember("PinError")) { - uint64_t tid = gInstance->GetCurrentTransactionId(); - bool pinRequired = (jsonRoot["PinError"].isString() && - jsonRoot["PinError"].asString() == "PinRequired"); - bool pinInvalid = (jsonRoot["PinError"].isObject() && - jsonRoot["PinError"].isMember("InvalidPin")); - if (pinRequired || pinInvalid) { - bool wasInvalid = false; - int retries = -1; - if (pinInvalid) { - wasInvalid = true; - if (jsonRoot["PinError"]["InvalidPin"].isInt()) { - retries = jsonRoot["PinError"]["InvalidPin"].asInt(); - } - } - gInstance->status_update_result = std::move(status_result_update); - nsTextFormatter::ssprintf(notification_json, kPinRequiredNotifcation, tid, - origin.get(), browsingCtxId, - wasInvalid ? "true" : "false", retries); - } else if (jsonRoot["PinError"].isString()) { - // Not saving the status_result, so the callback will error out and cancel - // the transaction, because these errors are not recoverable by - // user-input. - if (jsonRoot["PinError"].asString() == "PinAuthBlocked") { - // Pin authentication blocked. Device needs power cycle! - nsTextFormatter::ssprintf(notification_json, kPinErrorNotifications, - "pin-auth-blocked", tid, origin.get(), - browsingCtxId); - } else if (jsonRoot["PinError"].asString() == "PinBlocked") { - // No retries left. Pin blocked. Device needs reset! - nsTextFormatter::ssprintf(notification_json, kPinErrorNotifications, - "device-blocked", tid, origin.get(), - browsingCtxId); - } - } - } else if (jsonRoot.isMember("SelectDeviceNotice")) { - nsTextFormatter::ssprintf(notification_json, kSelectDeviceNotifcation, - gInstance->GetCurrentTransactionId(), - origin.get(), browsingCtxId); - } else if (jsonRoot.isMember("DeviceSelected")) { - if (gInstance->CurrentTransactionIsRegister()) { - nsTextFormatter::ssprintf(notification_json, kRegisterPromptNotifcation, - gInstance->GetCurrentTransactionId(), - origin.get(), browsingCtxId, "true", "true"); - } else if (gInstance->CurrentTransactionIsSign()) { - nsTextFormatter::ssprintf(notification_json, kSignPromptNotifcation, - gInstance->GetCurrentTransactionId(), - origin.get(), browsingCtxId, "true", "true"); - } - } else { - // No-op for now - } - - if (!notification_json.IsEmpty()) { - nsCOMPtr r(NewRunnableMethod( - "U2FTokenManager::RunSendPromptNotification", gInstance, - &U2FTokenManager::RunSendPromptNotification, notification_json)); - MOZ_ALWAYS_SUCCEEDS(GetMainThreadSerialEventTarget()->Dispatch( - r.forget(), NS_DISPATCH_NORMAL)); - } -} - RefPtr U2FTokenManager::GetTokenManagerImpl() { MOZ_ASSERT(U2FPrefManager::Get()); mozilla::ipc::AssertIsOnBackgroundThread(); @@ -423,13 +294,7 @@ RefPtr U2FTokenManager::GetTokenManagerImpl() { // same time as the softtoken would always win the race to register. // We could support it for signing though... if (pm->GetUsbTokenEnabled()) { - U2FTokenTransport* manager; - if (U2FPrefManager::Get()->GetIsCtap2()) { - manager = new CTAPHIDTokenManager(); - } else { - manager = new U2FHIDTokenManager(); - } - return manager; + return new U2FHIDTokenManager(); } if (pm->GetSoftTokenEnabled()) { @@ -498,7 +363,7 @@ void U2FTokenManager::Register( // If the RP request direct attestation, ask the user for permission and // store the transaction info until the user proceeds or cancels. NS_ConvertUTF16toUTF8 origin(aTransactionInfo.Origin()); - SendPromptNotification(kRegisterDirectPromptNotifcation, aTransactionId, + SendPromptNotification(kRegisterDirectPromptNotificationU2F, aTransactionId, origin.get(), aTransactionInfo.BrowsingContextId()); MOZ_ASSERT(mPendingRegisterInfo.isNothing()); @@ -512,25 +377,21 @@ void U2FTokenManager::DoRegister(const WebAuthnMakeCredentialInfo& aInfo, // Show a prompt that lets the user cancel the ongoing transaction. NS_ConvertUTF16toUTF8 origin(aInfo.Origin()); - bool is_ctap2 = U2FPrefManager::Get()->GetIsCtap2(); - SendPromptNotification(kRegisterPromptNotifcation, mLastTransactionId, - origin.get(), aInfo.BrowsingContextId(), - is_ctap2 ? "true" : "false", "false"); + SendPromptNotification(kRegisterPromptNotificationU2F, mLastTransactionId, + origin.get(), aInfo.BrowsingContextId(), "false"); uint64_t tid = mLastTransactionId; - mTokenManagerImpl->Register(aInfo, aForceNoneAttestation, status_callback) + mTokenManagerImpl->Register(aInfo, aForceNoneAttestation) ->Then( GetCurrentSerialEventTarget(), __func__, - [tid, is_ctap2](WebAuthnMakeCredentialResult&& aResult) { + [tid](WebAuthnMakeCredentialResult&& aResult) { U2FTokenManager* mgr = U2FTokenManager::Get(); mgr->MaybeConfirmRegister(tid, aResult); - Telemetry::ScalarAdd( - Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, - is_ctap2 ? u"CTAPRegisterFinish"_ns : u"U2FRegisterFinish"_ns, - 1); + Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, + u"U2FRegisterFinish"_ns, 1); }, - [tid, is_ctap2](nsresult rv) { + [tid](nsresult rv) { MOZ_ASSERT(NS_FAILED(rv)); U2FTokenManager* mgr = U2FTokenManager::Get(); bool shouldCancelActiveDialog = true; @@ -539,9 +400,8 @@ void U2FTokenManager::DoRegister(const WebAuthnMakeCredentialInfo& aInfo, shouldCancelActiveDialog = false; } mgr->MaybeAbortRegister(tid, rv, shouldCancelActiveDialog); - Telemetry::ScalarAdd( - Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, - is_ctap2 ? u"CTAPRegisterAbort"_ns : u"U2FRegisterAbort"_ns, 1); + Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, + u"U2FRegisterAbort"_ns, 1); }) ->Track(mRegisterPromise); } @@ -592,41 +452,22 @@ void U2FTokenManager::DoSign(const WebAuthnGetAssertionInfo& aTransactionInfo) { uint64_t browserCtxId = aTransactionInfo.BrowsingContextId(); // Show a prompt that lets the user cancel the ongoing transaction. - bool is_ctap2 = U2FPrefManager::Get()->GetIsCtap2(); - SendPromptNotification(kSignPromptNotifcation, tid, origin.get(), - browserCtxId, is_ctap2 ? "true" : "false", "false"); + SendPromptNotification(kSignPromptNotificationU2F, tid, origin.get(), + browserCtxId, "false"); - mTokenManagerImpl->Sign(aTransactionInfo, status_callback) + mTokenManagerImpl->Sign(aTransactionInfo) ->Then( GetCurrentSerialEventTarget(), __func__, - [tid, origin, browserCtxId, - is_ctap2](nsTArray&& aResult) { + [tid, origin](nsTArray&& aResult) { U2FTokenManager* mgr = U2FTokenManager::Get(); if (aResult.Length() == 1) { WebAuthnGetAssertionResult result = aResult[0].assertion; mgr->MaybeConfirmSign(tid, result); - } else { - nsCString res; - StringJoinAppend( - res, ","_ns, aResult, - [](nsACString& dst, - const WebAuthnGetAssertionResultWrapper& assertion) { - nsCString username = - assertion.username.valueOr(""_ns); - nsCString escaped_username; - NS_Escape(username, escaped_username, url_XAlphas); - dst.Append("\""_ns + escaped_username + "\""_ns); - }); - mgr->mPendingSignResults.Assign(aResult); - mgr->SendPromptNotification(kSelectSignResultNotifcation, tid, - origin.get(), browserCtxId, - res.get()); } - Telemetry::ScalarAdd( - Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, - is_ctap2 ? u"CTAPSignFinish"_ns : u"U2FSignFinish"_ns, 1); + Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, + u"U2FSignFinish"_ns, 1); }, - [tid, is_ctap2](nsresult rv) { + [tid](nsresult rv) { MOZ_ASSERT(NS_FAILED(rv)); U2FTokenManager* mgr = U2FTokenManager::Get(); bool shouldCancelActiveDialog = true; @@ -635,9 +476,8 @@ void U2FTokenManager::DoSign(const WebAuthnGetAssertionInfo& aTransactionInfo) { shouldCancelActiveDialog = false; } mgr->MaybeAbortSign(tid, rv, shouldCancelActiveDialog); - Telemetry::ScalarAdd( - Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, - is_ctap2 ? u"CTAPSignAbort"_ns : u"U2FSignAbort"_ns, 1); + Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, + u"U2FSignAbort"_ns, 1); }) ->Track(mSignPromise); } @@ -693,66 +533,6 @@ U2FTokenManager::ResumeRegister(uint64_t aTransactionId, return gBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); } -NS_IMETHODIMP -U2FTokenManager::ResumeWithSelectedSignResult(uint64_t aTransactionId, - uint64_t idx) { - MOZ_ASSERT(XRE_IsParentProcess()); - MOZ_ASSERT(NS_IsMainThread()); - - if (!gBackgroundThread) { - return NS_ERROR_FAILURE; - } - - nsCOMPtr r(NewRunnableMethod( - "U2FTokenManager::RunResumeWithSelectedSignResult", this, - &U2FTokenManager::RunResumeWithSelectedSignResult, aTransactionId, idx)); - - return gBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); -} - -void U2FTokenManager::RunResumeWithSelectedSignResult(uint64_t aTransactionId, - uint64_t idx) { - mozilla::ipc::AssertIsOnBackgroundThread(); - if (NS_WARN_IF(mPendingSignResults.IsEmpty())) { - return; - } - - if (NS_WARN_IF(mPendingSignResults.Length() <= idx)) { - return; - } - - if (mLastTransactionId != aTransactionId) { - return; - } - - WebAuthnGetAssertionResult result = mPendingSignResults[idx].assertion; - MaybeConfirmSign(aTransactionId, result); -} - -NS_IMETHODIMP -U2FTokenManager::PinCallback(const nsACString& aPin) { - if (!U2FPrefManager::Get()->GetIsCtap2()) { - // Not used in CTAP1 - return NS_ERROR_FAILURE; - } - MOZ_ASSERT(XRE_IsParentProcess()); - MOZ_ASSERT(NS_IsMainThread()); - if (!gBackgroundThread) { - return NS_ERROR_FAILURE; - } - // Move the result here locally, so it will be freed either way - UniquePtr - result = nullptr; - std::swap(result, status_update_result); - - if (NS_WARN_IF( - !rust_ctap2_status_update_send_pin(result.get(), aPin.Data()))) { - return NS_ERROR_FAILURE; - } - return NS_OK; -} - void U2FTokenManager::RunResumeRegister(uint64_t aTransactionId, bool aForceNoneAttestation) { mozilla::ipc::AssertIsOnBackgroundThread(); @@ -807,9 +587,6 @@ void U2FTokenManager::RunCancel(uint64_t aTransactionId) { return; } - // We have to "hang up" in case auth-rs is still waiting for us to send a PIN - // so it can exit cleanly - status_update_result = nullptr; // Cancel the request. mTokenManagerImpl->Cancel(); diff --git a/dom/webauthn/U2FTokenManager.h b/dom/webauthn/U2FTokenManager.h index 3f5314b226ed1..83c9b14b110e5 100644 --- a/dom/webauthn/U2FTokenManager.h +++ b/dom/webauthn/U2FTokenManager.h @@ -55,17 +55,6 @@ class U2FTokenManager final : public nsIU2FTokenManager { return Nothing(); } - Maybe GetCurrentBrowsingCtxId() { - if (mPendingRegisterInfo.isSome()) { - return Some(mPendingRegisterInfo.value().BrowsingContextId()); - } - - if (mPendingSignInfo.isSome()) { - return Some(mPendingSignInfo.value().BrowsingContextId()); - } - return Nothing(); - } - uint64_t GetCurrentTransactionId() { return mLastTransactionId; } bool CurrentTransactionIsRegister() { return mPendingRegisterInfo.isSome(); } @@ -78,13 +67,6 @@ class U2FTokenManager final : public nsIU2FTokenManager { // The main thread runnable function for "SendPromptNotification". void RunSendPromptNotification(const nsString& aJSON); - struct StatusUpdateResFreePolicy { - void operator()(rust_ctap2_status_update_res* p); - }; - UniquePtr - status_update_result = nullptr; - private: U2FTokenManager(); ~U2FTokenManager() = default; diff --git a/dom/webauthn/U2FTokenTransport.h b/dom/webauthn/U2FTokenTransport.h index db8bfdd6f8bc1..0c6d80bdf31cb 100644 --- a/dom/webauthn/U2FTokenTransport.h +++ b/dom/webauthn/U2FTokenTransport.h @@ -16,8 +16,6 @@ * transport types. */ -struct rust_ctap2_status_update_res; - namespace mozilla::dom { class WebAuthnGetAssertionResultWrapper { @@ -37,12 +35,10 @@ class U2FTokenTransport { U2FTokenTransport() = default; virtual RefPtr Register( - const WebAuthnMakeCredentialInfo& aInfo, bool aForceNoneAttestation, - void status_callback(rust_ctap2_status_update_res*)) = 0; + const WebAuthnMakeCredentialInfo& aInfo, bool aForceNoneAttestation) = 0; virtual RefPtr Sign( - const WebAuthnGetAssertionInfo& aInfo, - void status_callback(rust_ctap2_status_update_res*)) = 0; + const WebAuthnGetAssertionInfo& aInfo) = 0; virtual void Cancel() = 0; diff --git a/dom/webauthn/WebAuthnController.cpp b/dom/webauthn/WebAuthnController.cpp new file mode 100644 index 0000000000000..f0c890b8bebee --- /dev/null +++ b/dom/webauthn/WebAuthnController.cpp @@ -0,0 +1,745 @@ +/* -*- 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 "json/json.h" +#include "mozilla/dom/WebAuthnController.h" +#include "mozilla/dom/PWebAuthnTransactionParent.h" +#include "mozilla/dom/WebAuthnUtil.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_security.h" +#include "mozilla/Unused.h" +#include "nsEscape.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIThread.h" +#include "nsTextFormatter.h" +#include "mozilla/Telemetry.h" + +#include "AuthrsTransport.h" +#include "CtapArgs.h" +#include "CtapResults.h" +#include "U2FSoftTokenTransport.h" +#include "WebAuthnEnumStrings.h" + +#ifdef MOZ_WIDGET_ANDROID +# include "mozilla/dom/AndroidWebAuthnTokenManager.h" +#endif + +namespace mozilla::dom { + +/*********************************************************************** + * Statics + **********************************************************************/ + +namespace { +static mozilla::LazyLogModule gWebAuthnControllerLog("webauthncontroller"); +StaticRefPtr gWebAuthnController; +static nsIThread* gWebAuthnBackgroundThread; +} // namespace + +// Data for WebAuthn UI prompt notifications. +static const char16_t kRegisterPromptNotification[] = + u"{\"is_ctap2\":true,\"action\":\"register\",\"tid\":%llu," + u"\"origin\":\"%s\",\"browsingContextId\":%llu,\"device_selected\":%s}"; +static const char16_t kRegisterDirectPromptNotification[] = + u"{\"is_ctap2\":true,\"action\":\"register-direct\",\"tid\":%llu," + u"\"origin\":\"%s\",\"browsingContextId\":%llu}"; +static const char16_t kSignPromptNotification[] = + u"{\"is_ctap2\":true,\"action\":\"sign\",\"tid\":%llu,\"origin\":\"%s\"," + u"\"browsingContextId\":%llu,\"device_selected\":%s}"; +static const char16_t kCancelPromptNotification[] = + u"{\"is_ctap2\":true,\"action\":\"cancel\",\"tid\":%llu}"; +static const char16_t kSelectSignResultNotification[] = + u"{\"is_ctap2\":true,\"action\":\"select-sign-result\",\"tid\":%llu," + u"\"origin\":\"%s\",\"browsingContextId\":%llu,\"usernames\":[%s]}"; + +/*********************************************************************** + * U2FManager Implementation + **********************************************************************/ + +NS_IMPL_ISUPPORTS(WebAuthnController, nsIWebAuthnController, + nsIU2FTokenManager); + +WebAuthnController::WebAuthnController() : mTransactionParent(nullptr) { + MOZ_ASSERT(XRE_IsParentProcess()); + // Create on the main thread to make sure ClearOnShutdown() works. + MOZ_ASSERT(NS_IsMainThread()); +} + +// static +void WebAuthnController::Initialize() { + if (!XRE_IsParentProcess()) { + return; + } + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!gWebAuthnController); + gWebAuthnController = new WebAuthnController(); + ClearOnShutdown(&gWebAuthnController); +} + +// static +WebAuthnController* WebAuthnController::Get() { + MOZ_ASSERT(XRE_IsParentProcess()); + mozilla::ipc::AssertIsOnBackgroundThread(); + return gWebAuthnController; +} + +void WebAuthnController::AbortTransaction(const uint64_t& aTransactionId, + const nsresult& aError, + bool shouldCancelActiveDialog) { + if (mTransactionParent && mTransaction.isSome() && aTransactionId > 0 && + aTransactionId == mTransaction.ref().mTransactionId) { + Unused << mTransactionParent->SendAbort(aTransactionId, aError); + ClearTransaction(shouldCancelActiveDialog); + } +} + +void WebAuthnController::AbortOngoingTransaction() { + if (mTransaction.isSome()) { + AbortTransaction(mTransaction.ref().mTransactionId, NS_ERROR_DOM_ABORT_ERR, + true); + } +} + +void WebAuthnController::MaybeClearTransaction( + PWebAuthnTransactionParent* aParent) { + // Only clear if we've been requested to do so by our current transaction + // parent. + if (mTransactionParent == aParent) { + ClearTransaction(true); + } +} + +void WebAuthnController::ClearTransaction(bool cancel_prompt) { + if (cancel_prompt && mTransaction.isSome() && + mTransaction.ref().mTransactionId > 0) { + // Remove any prompts we might be showing for the current transaction. + SendPromptNotification(kCancelPromptNotification, + mTransaction.ref().mTransactionId); + } + mTransactionParent = nullptr; + mTransportImpl = nullptr; + + // Forget any pending registration. + mPendingRegisterInfo.reset(); + mPendingSignInfo.reset(); + mPendingSignResults.Clear(); + mTransaction.reset(); +} + +template +void WebAuthnController::SendPromptNotification(const char16_t* aFormat, + T... aArgs) { + MOZ_ASSERT(!NS_IsMainThread()); + nsAutoString json; + nsTextFormatter::ssprintf(json, aFormat, aArgs...); + + nsCOMPtr r(NewRunnableMethod( + "WebAuthnController::RunSendPromptNotification", this, + &WebAuthnController::RunSendPromptNotification, json)); + + MOZ_ALWAYS_SUCCEEDS(GetMainThreadSerialEventTarget()->Dispatch( + r.forget(), NS_DISPATCH_NORMAL)); +} + +NS_IMETHODIMP +WebAuthnController::SendPromptNotificationPreformatted( + uint64_t aTransactionId, const nsACString& aJson) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr r(NewRunnableMethod( + "WebAuthnController::RunSendPromptNotification", this, + &WebAuthnController::RunSendPromptNotification, + NS_ConvertUTF8toUTF16(aJson))); + MOZ_ALWAYS_SUCCEEDS(GetMainThreadSerialEventTarget()->Dispatch( + r.forget(), NS_DISPATCH_NORMAL)); + return NS_OK; +} + +void WebAuthnController::RunSendPromptNotification(const nsString& aJSON) { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr os = services::GetObserverService(); + if (NS_WARN_IF(!os)) { + return; + } + + nsCOMPtr self = this; + MOZ_ALWAYS_SUCCEEDS( + os->NotifyObservers(self, "webauthn-prompt", aJSON.get())); +} + +nsCOMPtr WebAuthnController::GetTransportImpl() { + mozilla::ipc::AssertIsOnBackgroundThread(); + + if (mTransportImpl) { + return mTransportImpl; + } + +/* Enable in Bug 1819414 */ +#if 0 +# ifdef MOZ_WIDGET_ANDROID + // On Android, prefer the platform support if enabled. + if (StaticPrefs::security_webauth_webauthn_enable_android_fido2()) { + nsCOMPtr transport = AndroidWebAuthnTokenManager::GetInstance(); + transport->SetController(this); + return transport; + } +# endif +#endif + + // Prefer the HW token, even if the softtoken is enabled too. + // We currently don't support soft and USB tokens enabled at the + // same time as the softtoken would always win the race to register. + // We could support it for signing though... + if (StaticPrefs::security_webauth_webauthn_enable_usbtoken()) { + nsCOMPtr transport = NewAuthrsTransport(); + transport->SetController(this); + return transport; + } + + if (StaticPrefs::security_webauth_webauthn_enable_softtoken()) { + nsCOMPtr transport = new U2FSoftTokenTransport( + StaticPrefs::security_webauth_softtoken_counter()); + transport->SetController(this); + return transport; + } + + // TODO Use WebAuthnRequest to aggregate results from all transports, + // once we have multiple HW transport types. + + return nullptr; +} + +void WebAuthnController::Cancel(PWebAuthnTransactionParent* aTransactionParent, + const Tainted& aTransactionId) { + // The last transaction ID also suffers from the issue described in Bug + // 1696159. A content process could cancel another content processes + // transaction by guessing the last transaction ID. + if (mTransactionParent != aTransactionParent || mTransaction.isNothing() || + !MOZ_IS_VALID(aTransactionId, + mTransaction.ref().mTransactionId == aTransactionId)) { + return; + } + + if (mTransportImpl) { + mTransportImpl->Cancel(); + } + + ClearTransaction(true); +} + +void WebAuthnController::Register( + PWebAuthnTransactionParent* aTransactionParent, + const uint64_t& aTransactionId, const WebAuthnMakeCredentialInfo& aInfo) { + mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug, + ("WebAuthnController::Register")); + MOZ_ASSERT(aTransactionId > 0); + + if (!gWebAuthnBackgroundThread) { + gWebAuthnBackgroundThread = NS_GetCurrentThread(); + MOZ_ASSERT(gWebAuthnBackgroundThread, "This should never be null!"); + } + + AbortOngoingTransaction(); + mTransactionParent = aTransactionParent; + + /* We could refactor to defer the hashing here */ + CryptoBuffer rpIdHash, clientDataHash; + NS_ConvertUTF16toUTF8 rpId(aInfo.RpId()); + nsresult rv = BuildTransactionHashes(rpId, aInfo.ClientDataJSON(), rpIdHash, + clientDataHash); + if (NS_WARN_IF(NS_FAILED(rv))) { + // We haven't set mTransaction yet, so we can't use AbortTransaction + Unused << mTransactionParent->SendAbort(aTransactionId, + NS_ERROR_DOM_UNKNOWN_ERR); + return; + } + + // Hold on to any state that we need to finish the transaction. + mTransaction = Some( + Transaction(aTransactionId, rpIdHash, Nothing(), aInfo.ClientDataJSON())); + + MOZ_ASSERT(mPendingRegisterInfo.isNothing()); + mPendingRegisterInfo = Some(aInfo); + + // Determine whether direct attestation was requested. + bool noneAttestationRequested = true; + +// On Android, let's always reject direct attestations until we have a +// mechanism to solicit user consent, from Bug 1550164 +#ifndef MOZ_WIDGET_ANDROID + const auto& extra = aInfo.Extra().ref(); + + // The default attestation type is "none", so set + // noneAttestationRequested=false only if the RP's preference matches one of + // the other known types. This needs to be reviewed if values are added to + // the AttestationConveyancePreference enum. + const nsString& attestation = extra.attestationConveyancePreference(); + static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 2); + if (attestation.EqualsLiteral( + MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT) || + attestation.EqualsLiteral( + MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT) || + attestation.EqualsLiteral( + MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_ENTERPRISE)) { + noneAttestationRequested = false; + } +#endif // not MOZ_WIDGET_ANDROID + + // Start a register request immediately if direct attestation + // wasn't requested or the test pref is set. + if (noneAttestationRequested || + StaticPrefs:: + security_webauth_webauthn_testing_allow_direct_attestation()) { + DoRegister(aInfo, noneAttestationRequested); + return; + } + + // If the RP request direct attestation, ask the user for permission and + // store the transaction info until the user proceeds or cancels. + NS_ConvertUTF16toUTF8 origin(aInfo.Origin()); + SendPromptNotification(kRegisterDirectPromptNotification, aTransactionId, + origin.get(), aInfo.BrowsingContextId()); +} + +void WebAuthnController::DoRegister(const WebAuthnMakeCredentialInfo& aInfo, + bool aForceNoneAttestation) { + mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(mTransaction.isSome()); + if (NS_WARN_IF(mTransaction.isNothing())) { + // Clear prompt? + return; + } + + // Show a prompt that lets the user cancel the ongoing transaction. + NS_ConvertUTF16toUTF8 origin(aInfo.Origin()); + SendPromptNotification(kRegisterPromptNotification, + mTransaction.ref().mTransactionId, origin.get(), + aInfo.BrowsingContextId(), "false"); + + RefPtr args( + new CtapRegisterArgs(aInfo, aForceNoneAttestation)); + + mTransportImpl = GetTransportImpl(); + if (!mTransportImpl) { + AbortTransaction(mTransaction.ref().mTransactionId, + NS_ERROR_DOM_NOT_ALLOWED_ERR, true); + return; + } + nsresult rv = mTransportImpl->MakeCredential( + mTransaction.ref().mTransactionId, aInfo.BrowsingContextId(), args); + if (NS_FAILED(rv)) { + AbortTransaction(mTransaction.ref().mTransactionId, rv, true); + return; + } +} + +NS_IMETHODIMP +WebAuthnController::ResumeRegister(uint64_t aTransactionId, + bool aForceNoneAttestation) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + if (!gWebAuthnBackgroundThread) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr r(NewRunnableMethod( + "WebAuthnController::RunResumeRegister", this, + &WebAuthnController::RunResumeRegister, aTransactionId, + aForceNoneAttestation)); + + return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void WebAuthnController::RunResumeRegister(uint64_t aTransactionId, + bool aForceNoneAttestation) { + mozilla::ipc::AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(mPendingRegisterInfo.isNothing())) { + return; + } + + if (mTransaction.isNothing() || + mTransaction.ref().mTransactionId != aTransactionId) { + return; + } + + // Resume registration and cleanup. + DoRegister(mPendingRegisterInfo.ref(), aForceNoneAttestation); +} + +NS_IMETHODIMP +WebAuthnController::FinishRegister(uint64_t aTransactionId, + nsICtapRegisterResult* aResult) { + MOZ_ASSERT(XRE_IsParentProcess()); + nsCOMPtr r( + NewRunnableMethod>( + "WebAuthnController::RunFinishRegister", this, + &WebAuthnController::RunFinishRegister, aTransactionId, aResult)); + + if (!gWebAuthnBackgroundThread) { + return NS_ERROR_FAILURE; + } + return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void WebAuthnController::RunFinishRegister( + uint64_t aTransactionId, const RefPtr& aResult) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mTransaction.isNothing() || + aTransactionId != mTransaction.ref().mTransactionId) { + // The previous transaction was likely cancelled from the prompt. + return; + } + + nsresult rv; + nsresult status; + rv = aResult->GetStatus(&status); + if (NS_WARN_IF(NS_FAILED(rv)) || NS_FAILED(status)) { + bool shouldCancelActiveDialog = true; + if (status == NS_ERROR_DOM_OPERATION_ERR) { + // PIN-related errors. Let the dialog show to inform the user + shouldCancelActiveDialog = false; + } + AbortTransaction(aTransactionId, status, shouldCancelActiveDialog); + Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, + u"CTAPRegisterAbort"_ns, 1); + return; + } + + nsCString clientDataJson; + rv = aResult->GetClientDataJSON(clientDataJson); + if (NS_WARN_IF(NS_FAILED(rv))) { + AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true); + return; + } + + nsTArray attObj; + rv = aResult->GetAttestationObject(attObj); + if (NS_WARN_IF(NS_FAILED(rv))) { + AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true); + return; + } + + nsTArray credentialId; + rv = aResult->GetCredentialId(credentialId); + if (NS_WARN_IF(NS_FAILED(rv))) { + AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true); + return; + } + + nsTArray extensions; + nsTArray regData; /* Only used in U2F */ + WebAuthnMakeCredentialResult result(clientDataJson, attObj, credentialId, + regData, extensions); + + Unused << mTransactionParent->SendConfirmRegister(aTransactionId, result); + ClearTransaction(true); + Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, + u"CTAPRegisterFinish"_ns, 1); +} + +void WebAuthnController::Sign(PWebAuthnTransactionParent* aTransactionParent, + const uint64_t& aTransactionId, + const WebAuthnGetAssertionInfo& aInfo) { + mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug, ("WebAuthnSign")); + MOZ_ASSERT(aTransactionId > 0); + + if (!gWebAuthnBackgroundThread) { + gWebAuthnBackgroundThread = NS_GetCurrentThread(); + MOZ_ASSERT(gWebAuthnBackgroundThread, "This should never be null!"); + } + + AbortOngoingTransaction(); + mTransactionParent = aTransactionParent; + + /* We could refactor to defer the hashing here */ + CryptoBuffer rpIdHash, clientDataHash; + NS_ConvertUTF16toUTF8 rpId(aInfo.RpId()); + nsresult rv = BuildTransactionHashes(rpId, aInfo.ClientDataJSON(), rpIdHash, + clientDataHash); + if (NS_WARN_IF(NS_FAILED(rv))) { + // We haven't set mTransaction yet, so we can't use AbortTransaction + Unused << mTransactionParent->SendAbort(aTransactionId, + NS_ERROR_DOM_UNKNOWN_ERR); + return; + } + + Maybe> appIdHash = Nothing(); + if (aInfo.Extra().isSome()) { + const auto& extra = aInfo.Extra().ref(); + for (const WebAuthnExtension& ext : extra.Extensions()) { + if (ext.type() == WebAuthnExtension::TWebAuthnExtensionAppId) { + appIdHash = Some(ext.get_WebAuthnExtensionAppId().AppId().Clone()); + } + } + } + + // Hold on to any state that we need to finish the transaction. + mTransaction = Some( + Transaction(aTransactionId, rpIdHash, appIdHash, aInfo.ClientDataJSON())); + + mPendingSignInfo = Some(aInfo); + + NS_ConvertUTF16toUTF8 origin(aInfo.Origin()); + + // Show a prompt that lets the user cancel the ongoing transaction. + SendPromptNotification(kSignPromptNotification, + mTransaction.ref().mTransactionId, origin.get(), + aInfo.BrowsingContextId(), "false"); + + RefPtr args(new CtapSignArgs(aInfo)); + + mTransportImpl = GetTransportImpl(); + if (!mTransportImpl) { + AbortTransaction(mTransaction.ref().mTransactionId, + NS_ERROR_DOM_NOT_ALLOWED_ERR, true); + return; + } + + rv = mTransportImpl->GetAssertion(mTransaction.ref().mTransactionId, + aInfo.BrowsingContextId(), args.get()); + if (NS_FAILED(rv)) { + AbortTransaction(mTransaction.ref().mTransactionId, rv, true); + return; + } +} + +NS_IMETHODIMP +WebAuthnController::FinishSign( + uint64_t aTransactionId, const nsACString& aClientDataJson, + const nsTArray>& aResult) { + MOZ_ASSERT(XRE_IsParentProcess()); + nsTArray> ownedResult = aResult.Clone(); + + nsCOMPtr r( + NewRunnableMethod>>( + "WebAuthnController::RunFinishSign", this, + &WebAuthnController::RunFinishSign, aTransactionId, aClientDataJson, + std::move(ownedResult))); + + if (!gWebAuthnBackgroundThread) { + return NS_ERROR_FAILURE; + } + return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void WebAuthnController::RunFinishSign( + uint64_t aTransactionId, const nsACString& aClientDataJson, + const nsTArray>& aResult) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mTransaction.isNothing() || + aTransactionId != mTransaction.ref().mTransactionId) { + return; + } + + if (aResult.Length() == 0) { + AbortTransaction(aTransactionId, NS_ERROR_DOM_UNKNOWN_ERR, true); + Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, + u"CTAPSignAbort"_ns, 1); + return; + } + + if (aResult.Length() == 1) { + nsresult status; + aResult[0]->GetStatus(&status); + if (NS_FAILED(status)) { + bool shouldCancelActiveDialog = true; + if (status == NS_ERROR_DOM_OPERATION_ERR) { + // PIN-related errors, e.g. blocked token. Let the dialog show to inform + // the user + shouldCancelActiveDialog = false; + } + AbortTransaction(aTransactionId, status, shouldCancelActiveDialog); + Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, + u"CTAPSignAbort"_ns, 1); + return; + } + mPendingSignResults = aResult.Clone(); + mTransaction.ref().mClientDataJSON = aClientDataJson; + RunResumeWithSelectedSignResult(aTransactionId, 0); + return; + } + + // If we more than one assertion, all of them should have OK status. + for (const auto& assertion : aResult) { + nsresult status; + assertion->GetStatus(&status); + if (NS_WARN_IF(NS_FAILED(status))) { + AbortTransaction(aTransactionId, status, true); + Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, + u"CTAPSignAbort"_ns, 1); + return; + } + } + + nsCString usernames; + StringJoinAppend( + usernames, ","_ns, aResult, + [](nsACString& dst, const RefPtr& assertion) { + nsCString username; + nsresult rv = assertion->GetUserName(username); + if (NS_FAILED(rv)) { + username.Assign(""); + } + nsCString escaped_username; + NS_Escape(username, escaped_username, url_XAlphas); + dst.Append("\""_ns + escaped_username + "\""_ns); + }); + + mPendingSignResults = aResult.Clone(); + mTransaction.ref().mClientDataJSON = aClientDataJson; + NS_ConvertUTF16toUTF8 origin(mPendingSignInfo.ref().Origin()); + SendPromptNotification(kSelectSignResultNotification, + mTransaction.ref().mTransactionId, origin.get(), + mPendingSignInfo.ref().BrowsingContextId(), + usernames.get()); +} + +NS_IMETHODIMP +WebAuthnController::SignatureSelectionCallback(uint64_t aTransactionId, + uint64_t idx) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr r(NewRunnableMethod( + "WebAuthnController::RunResumeWithSelectedSignResult", this, + &WebAuthnController::RunResumeWithSelectedSignResult, aTransactionId, + idx)); + + if (!gWebAuthnBackgroundThread) { + return NS_ERROR_FAILURE; + } + return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void WebAuthnController::RunResumeWithSelectedSignResult( + uint64_t aTransactionId, uint64_t idx) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mTransaction.isNothing() || + mTransaction.ref().mTransactionId != aTransactionId) { + return; + } + + if (NS_WARN_IF(mPendingSignResults.Length() <= idx)) { + return; + } + + RefPtr& selected = mPendingSignResults[idx]; + + nsTArray credentialId; + nsresult rv = selected->GetCredentialId(credentialId); + if (NS_WARN_IF(NS_FAILED(rv))) { + AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true); + return; + } + + nsTArray signature; + rv = selected->GetSignature(signature); + if (NS_WARN_IF(NS_FAILED(rv))) { + AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true); + return; + } + + nsTArray authenticatorData; + rv = selected->GetAuthenticatorData(authenticatorData); + if (NS_WARN_IF(NS_FAILED(rv))) { + AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true); + return; + } + + nsTArray rpIdHash; + rv = selected->GetRpIdHash(rpIdHash); + if (NS_WARN_IF(NS_FAILED(rv))) { + AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true); + return; + } + + nsTArray userHandle; + Unused << selected->GetUserHandle(userHandle); // optional + + nsTArray extensions; + if (mTransaction.ref().mAppIdHash.isSome()) { + bool usedAppId = (rpIdHash == mTransaction.ref().mAppIdHash.ref()); + extensions.AppendElement(WebAuthnExtensionResultAppId(usedAppId)); + } + + nsTArray signatureData; // Only used in U2F + WebAuthnGetAssertionResult result(mTransaction.ref().mClientDataJSON, + credentialId, signature, authenticatorData, + extensions, signatureData, userHandle); + + Unused << mTransactionParent->SendConfirmSign(aTransactionId, result); + ClearTransaction(true); + Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, + u"CTAPSignFinish"_ns, 1); +} + +NS_IMETHODIMP +WebAuthnController::PinCallback(uint64_t aTransactionId, + const nsACString& aPin) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr r(NewRunnableMethod( + "WebAuthnController::RunPinCallback", this, + &WebAuthnController::RunPinCallback, aTransactionId, aPin)); + + if (!gWebAuthnBackgroundThread) { + return NS_ERROR_FAILURE; + } + return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void WebAuthnController::RunPinCallback(uint64_t aTransactionId, + const nsCString& aPin) { + mozilla::ipc::AssertIsOnBackgroundThread(); + + mTransportImpl->PinCallback(aTransactionId, aPin); +} + +NS_IMETHODIMP +WebAuthnController::Cancel(uint64_t aTransactionId) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr r(NewRunnableMethod( + "WebAuthnController::RunCancel", this, &WebAuthnController::RunCancel, + aTransactionId)); + + if (!gWebAuthnBackgroundThread) { + return NS_ERROR_FAILURE; + } + return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void WebAuthnController::RunCancel(uint64_t aTransactionId) { + mozilla::ipc::AssertIsOnBackgroundThread(); + + if (mTransaction.isNothing() || + mTransaction.ref().mTransactionId != aTransactionId) { + return; + } + + // Cancel the request. + if (mTransportImpl) { + mTransportImpl->Cancel(); + } + + // Reject the promise. + AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR, true); +} + +} // namespace mozilla::dom diff --git a/dom/webauthn/WebAuthnController.h b/dom/webauthn/WebAuthnController.h new file mode 100644 index 0000000000000..bb837dba238a0 --- /dev/null +++ b/dom/webauthn/WebAuthnController.h @@ -0,0 +1,130 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_WebAuthnController_h +#define mozilla_dom_WebAuthnController_h + +#include "nsIWebAuthnController.h" +#include "mozilla/dom/PWebAuthnTransaction.h" +#include "mozilla/Tainting.h" + +/* + * Parent process manager for WebAuthn API transactions. Handles process + * transactions from all content processes, make sure only one transaction is + * live at any time. Manages access to hardware and software based key systems. + * + */ + +namespace mozilla::dom { + +class WebAuthnController final : public nsIWebAuthnController { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIU2FTOKENMANAGER + NS_DECL_NSIWEBAUTHNCONTROLLER + + static void Initialize(); + static WebAuthnController* Get(); + void Register(PWebAuthnTransactionParent* aTransactionParent, + const uint64_t& aTransactionId, + const WebAuthnMakeCredentialInfo& aInfo); + + void Sign(PWebAuthnTransactionParent* aTransactionParent, + const uint64_t& aTransactionId, + const WebAuthnGetAssertionInfo& aInfo); + + void Cancel(PWebAuthnTransactionParent* aTransactionParent, + const Tainted& aTransactionId); + + void MaybeClearTransaction(PWebAuthnTransactionParent* aParent); + + uint64_t GetCurrentTransactionId() { + return mTransaction.isNothing() ? 0 : mTransaction.ref().mTransactionId; + } + + bool CurrentTransactionIsRegister() { return mPendingRegisterInfo.isSome(); } + + bool CurrentTransactionIsSign() { return mPendingSignInfo.isSome(); } + + // Sends a "webauthn-prompt" observer notification with the given data. + template + void SendPromptNotification(const char16_t* aFormat, T... aArgs); + + // Same as SendPromptNotification, but with the already formatted string + // void SendPromptNotificationPreformatted(const nsACString& aJSON); + // The main thread runnable function for "SendPromptNotification". + void RunSendPromptNotification(const nsString& aJSON); + + private: + WebAuthnController(); + ~WebAuthnController() = default; + nsCOMPtr GetTransportImpl(); + + void AbortTransaction(const uint64_t& aTransactionId, const nsresult& aError, + bool shouldCancelActiveDialog); + void AbortOngoingTransaction(); + void ClearTransaction(bool cancel_prompt); + + void DoRegister(const WebAuthnMakeCredentialInfo& aInfo, + bool aForceNoneAttestation); + void DoSign(const WebAuthnGetAssertionInfo& aTransactionInfo); + + void RunFinishRegister(uint64_t aTransactionId, + const RefPtr& aResult); + void RunFinishSign(uint64_t aTransactionId, const nsACString& aClientDataJson, + const nsTArray>& aResult); + + // The main thread runnable function for "nsIU2FTokenManager.ResumeRegister". + void RunResumeRegister(uint64_t aTransactionId, bool aForceNoneAttestation); + void RunResumeSign(uint64_t aTransactionId); + void RunResumeWithSelectedSignResult(uint64_t aTransactionId, uint64_t idx); + void RunPinCallback(uint64_t aTransactionId, const nsCString& aPin); + + // The main thread runnable function for "nsIU2FTokenManager.Cancel". + void RunCancel(uint64_t aTransactionId); + + // Using a raw pointer here, as the lifetime of the IPC object is managed by + // the PBackground protocol code. This means we cannot be left holding an + // invalid IPC protocol object after the transaction is finished. + PWebAuthnTransactionParent* mTransactionParent; + + nsCOMPtr mTransportImpl; + + // Pending registration info while we wait for user input. + Maybe mPendingRegisterInfo; + + // Pending registration info while we wait for user input. + Maybe mPendingSignInfo; + + nsTArray> mPendingSignResults; + + class Transaction { + public: + Transaction(uint64_t aTransactionId, const nsTArray& aRpIdHash, + const Maybe>& aAppIdHash, + const nsCString& aClientDataJSON, + bool aForceNoneAttestation = false) + : mTransactionId(aTransactionId), + mRpIdHash(aRpIdHash.Clone()), + mClientDataJSON(aClientDataJSON) { + if (aAppIdHash.isSome()) { + mAppIdHash = Some(aAppIdHash.ref().Clone()); + } else { + mAppIdHash = Nothing(); + } + } + uint64_t mTransactionId; + nsTArray mRpIdHash; + Maybe> mAppIdHash; + nsCString mClientDataJSON; + }; + + Maybe mTransaction; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_U2FTokenManager_h diff --git a/dom/webauthn/WebAuthnManager.cpp b/dom/webauthn/WebAuthnManager.cpp index f85b3b9fd0cb0..9e079893fc940 100644 --- a/dom/webauthn/WebAuthnManager.cpp +++ b/dom/webauthn/WebAuthnManager.cpp @@ -10,6 +10,7 @@ #include "nsThreadUtils.h" #include "WebAuthnCoseIdentifiers.h" #include "WebAuthnEnumStrings.h" +#include "WebAuthnTransportIdentifiers.h" #include "mozilla/BasePrincipal.h" #include "mozilla/dom/AuthenticatorAssertionResponse.h" #include "mozilla/dom/AuthenticatorAttestationResponse.h" @@ -565,16 +566,16 @@ already_AddRefed WebAuthnManager::GetAssertion( static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 2); for (const nsAString& str : s.mTransports.Value()) { if (str.EqualsLiteral(MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_USB)) { - transports |= U2F_AUTHENTICATOR_TRANSPORT_USB; + transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_USB; } else if (str.EqualsLiteral( MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_NFC)) { - transports |= U2F_AUTHENTICATOR_TRANSPORT_NFC; + transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_NFC; } else if (str.EqualsLiteral( MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_BLE)) { - transports |= U2F_AUTHENTICATOR_TRANSPORT_BLE; + transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_BLE; } else if (str.EqualsLiteral( MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_INTERNAL)) { - transports |= CTAP_AUTHENTICATOR_TRANSPORT_INTERNAL; + transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_INTERNAL; } } c.transports() = transports; diff --git a/dom/webauthn/WebAuthnTransactionParent.cpp b/dom/webauthn/WebAuthnTransactionParent.cpp index e829057c0dc2b..9e27cec45c504 100644 --- a/dom/webauthn/WebAuthnTransactionParent.cpp +++ b/dom/webauthn/WebAuthnTransactionParent.cpp @@ -5,9 +5,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/dom/WebAuthnTransactionParent.h" +#include "mozilla/dom/WebAuthnController.h" #include "mozilla/dom/U2FTokenManager.h" #include "mozilla/ipc/PBackgroundParent.h" #include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/StaticPrefs_security.h" + +#include "nsThreadUtils.h" #ifdef OS_WIN # include "WinWebAuthnManager.h" @@ -15,6 +19,11 @@ namespace mozilla::dom { +// Bug 1737205: we need to continue to support the legacy U2F interface until ~ +// FX 114, but we don't want to add an extra compatibility layer to our new +// CTAP2-compatible controller. So the Register and Sign methods below use the +// U2FTokenManager when the request is from the legacy U2F api. + mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestRegister( const uint64_t& aTransactionId, const WebAuthnMakeCredentialInfo& aTransactionInfo) { @@ -24,14 +33,24 @@ mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestRegister( if (WinWebAuthnManager::AreWebAuthNApisAvailable()) { WinWebAuthnManager* mgr = WinWebAuthnManager::Get(); mgr->Register(this, aTransactionId, aTransactionInfo); + return IPC_OK(); + } +#endif + + bool allowCtap2 = StaticPrefs::security_webauthn_ctap2(); + bool androidFido2 = + StaticPrefs::security_webauth_webauthn_enable_android_fido2(); + + // Remove as part of Bug 1737205. + bool legacyReq = aTransactionInfo.Extra().isNothing(); + + if (allowCtap2 && !androidFido2 && !legacyReq) { + WebAuthnController* ctrl = WebAuthnController::Get(); + ctrl->Register(this, aTransactionId, aTransactionInfo); } else { U2FTokenManager* mgr = U2FTokenManager::Get(); mgr->Register(this, aTransactionId, aTransactionInfo); } -#else - U2FTokenManager* mgr = U2FTokenManager::Get(); - mgr->Register(this, aTransactionId, aTransactionInfo); -#endif return IPC_OK(); } @@ -45,14 +64,24 @@ mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestSign( if (WinWebAuthnManager::AreWebAuthNApisAvailable()) { WinWebAuthnManager* mgr = WinWebAuthnManager::Get(); mgr->Sign(this, aTransactionId, aTransactionInfo); + return IPC_OK(); + } +#endif + + bool allowCtap2 = StaticPrefs::security_webauthn_ctap2(); + bool androidFido2 = + StaticPrefs::security_webauth_webauthn_enable_android_fido2(); + + // Remove as part of Bug 1737205. + bool legacyReq = aTransactionInfo.Extra().isNothing(); + + if (allowCtap2 && !androidFido2 && !legacyReq) { + WebAuthnController* ctrl = WebAuthnController::Get(); + ctrl->Sign(this, aTransactionId, aTransactionInfo); } else { U2FTokenManager* mgr = U2FTokenManager::Get(); mgr->Sign(this, aTransactionId, aTransactionInfo); } -#else - U2FTokenManager* mgr = U2FTokenManager::Get(); - mgr->Sign(this, aTransactionId, aTransactionInfo); -#endif return IPC_OK(); } @@ -65,15 +94,22 @@ mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestCancel( if (WinWebAuthnManager::AreWebAuthNApisAvailable()) { WinWebAuthnManager* mgr = WinWebAuthnManager::Get(); mgr->Cancel(this, aTransactionId); - } else { - U2FTokenManager* mgr = U2FTokenManager::Get(); - mgr->Cancel(this, aTransactionId); + return IPC_OK(); } -#else - U2FTokenManager* mgr = U2FTokenManager::Get(); - mgr->Cancel(this, aTransactionId); #endif + // We don't know whether WebAuthnController or U2FTokenManager was used, so + // try cancelling both. + WebAuthnController* ctrl = WebAuthnController::Get(); + if (ctrl) { + ctrl->Cancel(this, aTransactionId); + } + + U2FTokenManager* mgr = U2FTokenManager::Get(); + if (mgr) { + mgr->Cancel(this, aTransactionId); + } + return IPC_OK(); } @@ -107,18 +143,19 @@ void WebAuthnTransactionParent::ActorDestroy(ActorDestroyReason aWhy) { if (mgr) { mgr->MaybeClearTransaction(this); } - } else { - U2FTokenManager* mgr = U2FTokenManager::Get(); - if (mgr) { - mgr->MaybeClearTransaction(this); - } + return; } -#else +#endif + + WebAuthnController* ctrl = WebAuthnController::Get(); + if (ctrl) { + ctrl->MaybeClearTransaction(this); + } + U2FTokenManager* mgr = U2FTokenManager::Get(); if (mgr) { mgr->MaybeClearTransaction(this); } -#endif } } // namespace mozilla::dom diff --git a/dom/webauthn/WebAuthnTransportIdentifiers.h b/dom/webauthn/WebAuthnTransportIdentifiers.h new file mode 100644 index 0000000000000..f8846481621e9 --- /dev/null +++ b/dom/webauthn/WebAuthnTransportIdentifiers.h @@ -0,0 +1,15 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_WebAuthnTransportIdentifiers_h +#define mozilla_dom_WebAuthnTransportIdentifiers_h + +#define MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_USB 1 +#define MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_NFC 2 +#define MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_BLE 4 +#define MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_INTERNAL 8 + +#endif // mozilla_dom_WebAuthnTransportIdentifiers_h diff --git a/dom/webauthn/WinWebAuthnManager.cpp b/dom/webauthn/WinWebAuthnManager.cpp index 5eb16fcb98844..80f0fb2694e8f 100644 --- a/dom/webauthn/WinWebAuthnManager.cpp +++ b/dom/webauthn/WinWebAuthnManager.cpp @@ -12,6 +12,7 @@ #include "nsTextFormatter.h" #include "nsWindowsHelpers.h" #include "WebAuthnEnumStrings.h" +#include "WebAuthnTransportIdentifiers.h" #include "winwebauthn/webauthn.h" #include "WinWebAuthnManager.h" @@ -333,16 +334,16 @@ void WinWebAuthnManager::Register( for (auto& cred : aInfo.ExcludeList()) { uint8_t transports = cred.transports(); DWORD winTransports = 0; - if (transports & U2F_AUTHENTICATOR_TRANSPORT_USB) { + if (transports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_USB) { winTransports |= WEBAUTHN_CTAP_TRANSPORT_USB; } - if (transports & U2F_AUTHENTICATOR_TRANSPORT_NFC) { + if (transports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_NFC) { winTransports |= WEBAUTHN_CTAP_TRANSPORT_NFC; } - if (transports & U2F_AUTHENTICATOR_TRANSPORT_BLE) { + if (transports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_BLE) { winTransports |= WEBAUTHN_CTAP_TRANSPORT_BLE; } - if (transports & CTAP_AUTHENTICATOR_TRANSPORT_INTERNAL) { + if (transports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_INTERNAL) { winTransports |= WEBAUTHN_CTAP_TRANSPORT_INTERNAL; } @@ -611,16 +612,16 @@ void WinWebAuthnManager::Sign(PWebAuthnTransactionParent* aTransactionParent, for (auto& cred : aInfo.AllowList()) { uint8_t transports = cred.transports(); DWORD winTransports = 0; - if (transports & U2F_AUTHENTICATOR_TRANSPORT_USB) { + if (transports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_USB) { winTransports |= WEBAUTHN_CTAP_TRANSPORT_USB; } - if (transports & U2F_AUTHENTICATOR_TRANSPORT_NFC) { + if (transports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_NFC) { winTransports |= WEBAUTHN_CTAP_TRANSPORT_NFC; } - if (transports & U2F_AUTHENTICATOR_TRANSPORT_BLE) { + if (transports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_BLE) { winTransports |= WEBAUTHN_CTAP_TRANSPORT_BLE; } - if (transports & CTAP_AUTHENTICATOR_TRANSPORT_INTERNAL) { + if (transports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_INTERNAL) { winTransports |= WEBAUTHN_CTAP_TRANSPORT_INTERNAL; } diff --git a/dom/webauthn/authrs_bridge/Cargo.toml b/dom/webauthn/authrs_bridge/Cargo.toml new file mode 100644 index 0000000000000..5fd6b540e59ad --- /dev/null +++ b/dom/webauthn/authrs_bridge/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "authrs_bridge" +version = "0.1.0" +edition = "2021" +authors = ["Martin Sirringhaus", "John Schanck"] + +[dependencies] +authenticator = { version = "0.4.0-alpha.10", features = ["gecko"] } +log = "0.4" +moz_task = { path = "../../../xpcom/rust/moz_task" } +nserror = { path = "../../../xpcom/rust/nserror" } +nsstring = { path = "../../../xpcom/rust/nsstring" } +serde_cbor = "0.11" +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } +xpcom = { path = "../../../xpcom/rust/xpcom" } diff --git a/dom/webauthn/authrs_bridge/src/lib.rs b/dom/webauthn/authrs_bridge/src/lib.rs new file mode 100644 index 0000000000000..ec0fd0e3f0617 --- /dev/null +++ b/dom/webauthn/authrs_bridge/src/lib.rs @@ -0,0 +1,749 @@ +/* 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/. */ + +#[macro_use] +extern crate log; + +#[macro_use] +extern crate xpcom; + +use authenticator::{ + authenticatorservice::{ + AuthenticatorService, CtapVersion, GetAssertionOptions, MakeCredentialsOptions, + RegisterArgsCtap2, SignArgsCtap2, + }, + ctap2::attestation::AttestationStatement, + ctap2::server::{ + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, User, + }, + errors::{AuthenticatorError, PinError, U2FTokenError}, + statecallback::StateCallback, + Assertion, Pin, RegisterResult, SignResult, StatusUpdate, +}; +use moz_task::RunnableBuilder; +use nserror::{ + nsresult, NS_ERROR_DOM_INVALID_STATE_ERR, NS_ERROR_DOM_NOT_ALLOWED_ERR, + NS_ERROR_DOM_NOT_SUPPORTED_ERR, NS_ERROR_DOM_OPERATION_ERR, NS_ERROR_DOM_UNKNOWN_ERR, + NS_ERROR_FAILURE, NS_ERROR_NOT_AVAILABLE, NS_ERROR_NOT_IMPLEMENTED, NS_ERROR_NULL_POINTER, + NS_OK, +}; +use nsstring::{nsACString, nsCString, nsString}; +use serde_cbor; +use std::cell::RefCell; +use std::sync::mpsc::{channel, Receiver, RecvError, Sender}; +use std::sync::{Arc, Mutex}; +use thin_vec::ThinVec; +use xpcom::interfaces::{ + nsICtapRegisterArgs, nsICtapRegisterResult, nsICtapSignArgs, nsICtapSignResult, + nsIWebAuthnController, nsIWebAuthnTransport, +}; +use xpcom::{xpcom_method, RefPtr}; + +fn make_register_prompt(tid: u64, origin: &str, browsing_context_id: u64) -> String { + format!( + r#"{{"is_ctap2":true,"action":"register","tid":{tid},"origin":"{origin}","browsingContextId":{browsing_context_id},"device_selected":true}}"#, + ) +} + +fn make_sign_prompt(tid: u64, origin: &str, browsing_context_id: u64) -> String { + format!( + r#"{{"is_ctap2":true,"action":"sign","tid":{tid},"origin":"{origin}","browsingContextId":{browsing_context_id},"device_selected":true}}"#, + ) +} + +fn make_select_device_prompt(tid: u64, origin: &str, browsing_context_id: u64) -> String { + format!( + r#"{{"is_ctap2":true,"action":"select-device","tid":{tid},"origin":"{origin}","browsingContextId":{browsing_context_id}}}"#, + ) +} + +fn make_pin_error_prompt(action: &str, tid: u64, origin: &str, browsing_context_id: u64) -> String { + format!( + r#"{{"is_ctap2":true,"action":"{action}","tid":{tid},"origin":"{origin}","browsingContextId":{browsing_context_id}}}"#, + ) +} + +fn make_pin_required_prompt( + tid: u64, + origin: &str, + browsing_context_id: u64, + was_invalid: bool, + retries: i64, +) -> String { + format!( + r#"{{"is_ctap2":true,"action":"pin-required","tid":{tid},"origin":"{origin}","browsingContextId":{browsing_context_id},"wasInvalid":{was_invalid},"retriesLeft":{retries}}}"#, + ) +} + +fn authrs_to_nserror(e: &AuthenticatorError) -> nsresult { + match e { + AuthenticatorError::U2FToken(U2FTokenError::Unknown) => NS_ERROR_DOM_UNKNOWN_ERR, + AuthenticatorError::U2FToken(U2FTokenError::NotSupported) => NS_ERROR_DOM_NOT_SUPPORTED_ERR, + AuthenticatorError::U2FToken(U2FTokenError::InvalidState) => NS_ERROR_DOM_INVALID_STATE_ERR, + AuthenticatorError::U2FToken(U2FTokenError::ConstraintError) => NS_ERROR_DOM_UNKNOWN_ERR, + AuthenticatorError::U2FToken(U2FTokenError::NotAllowed) => NS_ERROR_DOM_NOT_ALLOWED_ERR, + AuthenticatorError::PinError(PinError::PinRequired) => NS_ERROR_DOM_OPERATION_ERR, + AuthenticatorError::PinError(PinError::PinIsTooShort) => NS_ERROR_DOM_UNKNOWN_ERR, + AuthenticatorError::PinError(PinError::PinIsTooLong(_)) => NS_ERROR_DOM_UNKNOWN_ERR, + AuthenticatorError::PinError(PinError::InvalidKeyLen) => NS_ERROR_DOM_UNKNOWN_ERR, + AuthenticatorError::PinError(PinError::InvalidPin(_)) => NS_ERROR_DOM_OPERATION_ERR, + AuthenticatorError::PinError(PinError::PinAuthBlocked) => NS_ERROR_DOM_OPERATION_ERR, + AuthenticatorError::PinError(PinError::PinBlocked) => NS_ERROR_DOM_OPERATION_ERR, + AuthenticatorError::PinError(PinError::PinNotSet) => NS_ERROR_DOM_UNKNOWN_ERR, + AuthenticatorError::PinError(PinError::Backend(_)) => NS_ERROR_DOM_UNKNOWN_ERR, + _ => NS_ERROR_DOM_UNKNOWN_ERR, + } +} + +#[xpcom(implement(nsICtapRegisterResult), atomic)] +pub struct CtapRegisterResult { + result: Result, +} + +impl CtapRegisterResult { + xpcom_method!(get_client_data_json => GetClientDataJSON() -> nsACString); + fn get_client_data_json(&self) -> Result { + match &self.result { + Ok(RegisterResult::CTAP2(_, client_data)) => { + return Ok(nsCString::from(client_data.serialized_data.clone())) + } + _ => return Err(NS_ERROR_FAILURE), + } + } + + xpcom_method!(get_attestation_object => GetAttestationObject() -> ThinVec); + fn get_attestation_object(&self) -> Result, nsresult> { + let mut out = ThinVec::new(); + if let Ok(RegisterResult::CTAP2(attestation, _)) = &self.result { + if let Ok(encoded_att_obj) = serde_cbor::to_vec(&attestation) { + out.extend_from_slice(&encoded_att_obj); + return Ok(out); + } + } + Err(NS_ERROR_FAILURE) + } + + xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec); + fn get_credential_id(&self) -> Result, nsresult> { + let mut out = ThinVec::new(); + if let Ok(RegisterResult::CTAP2(attestation, _)) = &self.result { + if let Some(credential_data) = &attestation.auth_data.credential_data { + out.extend(credential_data.credential_id.clone()); + return Ok(out); + } + } + Err(NS_ERROR_FAILURE) + } + + xpcom_method!(get_status => GetStatus() -> nsresult); + fn get_status(&self) -> Result { + match &self.result { + Ok(_) => Ok(NS_OK), + Err(e) => Err(authrs_to_nserror(e)), + } + } +} + +#[xpcom(implement(nsICtapSignResult), atomic)] +pub struct CtapSignResult { + result: Result, +} + +impl CtapSignResult { + xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec); + fn get_credential_id(&self) -> Result, nsresult> { + let mut out = ThinVec::new(); + if let Ok(assertion) = &self.result { + if let Some(cred) = &assertion.credentials { + out.extend_from_slice(&cred.id); + return Ok(out); + } + } + Err(NS_ERROR_FAILURE) + } + + xpcom_method!(get_signature => GetSignature() -> ThinVec); + fn get_signature(&self) -> Result, nsresult> { + let mut out = ThinVec::new(); + if let Ok(assertion) = &self.result { + out.extend_from_slice(&assertion.signature); + return Ok(out); + } + Err(NS_ERROR_FAILURE) + } + + xpcom_method!(get_authenticator_data => GetAuthenticatorData() -> ThinVec); + fn get_authenticator_data(&self) -> Result, nsresult> { + let mut out = ThinVec::new(); + if let Ok(assertion) = &self.result { + if let Ok(encoded_auth_data) = assertion.auth_data.to_vec() { + out.extend_from_slice(&encoded_auth_data); + return Ok(out); + } + } + Err(NS_ERROR_FAILURE) + } + + xpcom_method!(get_user_handle => GetUserHandle() -> ThinVec); + fn get_user_handle(&self) -> Result, nsresult> { + let mut out = ThinVec::new(); + if let Ok(assertion) = &self.result { + if let Some(user) = &assertion.user { + out.extend_from_slice(&user.id); + return Ok(out); + } + } + Err(NS_ERROR_FAILURE) + } + + xpcom_method!(get_user_name => GetUserName() -> nsACString); + fn get_user_name(&self) -> Result { + if let Ok(assertion) = &self.result { + if let Some(user) = &assertion.user { + if let Some(name) = &user.name { + return Ok(nsCString::from(name)); + } + } + } + Err(NS_ERROR_NOT_AVAILABLE) + } + + xpcom_method!(get_rp_id_hash => GetRpIdHash() -> ThinVec); + fn get_rp_id_hash(&self) -> Result, nsresult> { + // assertion.auth_data.rp_id_hash + let mut out = ThinVec::new(); + if let Ok(assertion) = &self.result { + out.extend(assertion.auth_data.rp_id_hash.0.clone()); + return Ok(out); + } + Err(NS_ERROR_FAILURE) + } + + xpcom_method!(get_status => GetStatus() -> nsresult); + fn get_status(&self) -> Result { + match &self.result { + Ok(_) => Ok(NS_OK), + Err(e) => Err(authrs_to_nserror(e)), + } + } +} + +/// Controller wraps a raw pointer to an nsIWebAuthnController. The AuthrsTransport struct holds a +/// Controller which we need to initialize from the SetController XPCOM method. Since XPCOM +/// methods take &self, Controller must have interior mutability. +#[derive(Clone)] +struct Controller(RefCell<*const nsIWebAuthnController>); + +/// Our implementation of nsIWebAuthnController in WebAuthnController.cpp has the property that its +/// XPCOM methods are safe to call from any thread, hence a raw pointer to an nsIWebAuthnController +/// is Send. +unsafe impl Send for Controller {} + +impl Controller { + fn init(&self, ptr: *const nsIWebAuthnController) -> Result<(), nsresult> { + self.0.replace(ptr); + Ok(()) + } + + fn send_prompt(&self, tid: u64, msg: &str) { + if (*self.0.borrow()).is_null() { + warn!("Controller not initialized"); + return; + } + let notification_str = nsCString::from(msg); + unsafe { + (**(self.0.borrow())).SendPromptNotificationPreformatted(tid, &*notification_str); + } + } + + fn finish_register( + &self, + tid: u64, + result: Result, + ) -> Result<(), nsresult> { + if (*self.0.borrow()).is_null() { + return Err(NS_ERROR_FAILURE); + } + let wrapped_result = CtapRegisterResult::allocate(InitCtapRegisterResult { result }) + .query_interface::() + .ok_or(NS_ERROR_FAILURE)?; + unsafe { + (**(self.0.borrow())).FinishRegister(tid, wrapped_result.coerce()); + } + Ok(()) + } + + fn finish_sign( + &self, + tid: u64, + result: Result, + ) -> Result<(), nsresult> { + if (*self.0.borrow()).is_null() { + return Err(NS_ERROR_FAILURE); + } + + // If result is an error, we return a single CtapSignResult that has its status field set + // to an error. Otherwise we convert the entries of SignResult (= Vec) into + // CtapSignResults with OK statuses. + let mut assertions: ThinVec>> = ThinVec::new(); + let mut client_data = nsCString::new(); + match result { + Err(e) => assertions.push( + CtapSignResult::allocate(InitCtapSignResult { result: Err(e) }) + .query_interface::(), + ), + Ok(SignResult::CTAP2(assertion_vec, json)) => { + client_data = nsCString::from(json.serialized_data); + for assertion in assertion_vec.0 { + assertions.push( + CtapSignResult::allocate(InitCtapSignResult { + result: Ok(assertion), + }) + .query_interface::(), + ); + } + } + _ => return Err(NS_ERROR_NOT_IMPLEMENTED), // SignResult::CTAP1 shouldn't happen. + } + + unsafe { + (**(self.0.borrow())).FinishSign(tid, &mut *client_data, &mut assertions); + } + Ok(()) + } +} + +enum EventType { + Register, + Sign, +} + +// The state machine creates a Sender/Receiver channel in ask_user_for_pin. It passes the +// Sender through status_callback, which stores the Sender in the pin_receiver field of an +// AuthrsTransport. The u64 in PinReceiver is a transaction ID, which the AuthrsTransport uses the +// transaction ID as a consistency check. +type PinReceiver = Option<(u64, Sender)>; + +fn status_callback( + status_rx: Receiver, + tid: u64, + origin: &String, + browsing_context_id: u64, + controller: Controller, + event_type: EventType, + pin_receiver: Arc>, /* Shared with an AuthrsTransport */ +) { + loop { + match status_rx.recv() { + Ok(StatusUpdate::DeviceAvailable { dev_info }) => { + debug!("STATUS: device available: {}", dev_info) + } + Ok(StatusUpdate::DeviceUnavailable { dev_info }) => { + debug!("STATUS: device unavailable: {}", dev_info) + } + Ok(StatusUpdate::Success { dev_info }) => { + debug!("STATUS: success using device: {}", dev_info); + } + Ok(StatusUpdate::SelectDeviceNotice) => { + debug!("STATUS: Please select a device by touching one of them."); + let notification_str = make_select_device_prompt(tid, origin, browsing_context_id); + controller.send_prompt(tid, ¬ification_str); + } + Ok(StatusUpdate::DeviceSelected(dev_info)) => { + debug!("STATUS: Continuing with device: {}", dev_info); + let notification_str = match event_type { + EventType::Register => make_register_prompt(tid, origin, browsing_context_id), + EventType::Sign => make_sign_prompt(tid, origin, browsing_context_id), + }; + controller.send_prompt(tid, ¬ification_str); + } + Ok(StatusUpdate::PinError(error, sender)) => match error { + PinError::PinRequired => { + let guard = pin_receiver.lock(); + if let Ok(mut entry) = guard { + entry.replace((tid, sender)); + } else { + return; + } + let notification_str = + make_pin_required_prompt(tid, origin, browsing_context_id, false, -1); + controller.send_prompt(tid, ¬ification_str); + continue; + } + PinError::InvalidPin(attempts) => { + let guard = pin_receiver.lock(); + if let Ok(mut entry) = guard { + entry.replace((tid, sender)); + } else { + return; + } + let notification_str = make_pin_required_prompt( + tid, + origin, + browsing_context_id, + true, + attempts.map_or(-1, |x| x as i64), + ); + controller.send_prompt(tid, ¬ification_str); + continue; + } + PinError::PinAuthBlocked => { + let notification_str = + make_pin_error_prompt("pin-auth-blocked", tid, origin, browsing_context_id); + controller.send_prompt(tid, ¬ification_str); + } + PinError::PinBlocked => { + let notification_str = + make_pin_error_prompt("device-blocked", tid, origin, browsing_context_id); + controller.send_prompt(tid, ¬ification_str); + } + e => { + warn!("Unexpected error: {:?}", e) + } + }, + Err(RecvError) => { + debug!("STATUS: end"); + return; + } + } + } +} + +// AuthrsTransport provides an nsIWebAuthnTransport interface to an AuthenticatorService. This +// allows an nsIWebAuthnController to dispatch MakeCredential and GetAssertion requests to USB HID +// tokens. The AuthrsTransport struct also keeps 1) a pointer to the active nsIWebAuthnController, +// which is used to prompt the user for input and to return results to the controller; and +// 2) a channel through which to receive a pin callback. +#[xpcom(implement(nsIWebAuthnTransport), atomic)] +pub struct AuthrsTransport { + auth_service: RefCell, // interior mutable for use in XPCOM methods + controller: Controller, + pin_receiver: Arc>, +} + +impl AuthrsTransport { + xpcom_method!(get_controller => GetController() -> *const nsIWebAuthnController); + fn get_controller(&self) -> Result, nsresult> { + Err(NS_ERROR_NOT_IMPLEMENTED) + } + + xpcom_method!(set_controller => SetController(aController: *const nsIWebAuthnController)); + fn set_controller(&self, controller: *const nsIWebAuthnController) -> Result<(), nsresult> { + self.controller.init(controller) + } + + xpcom_method!(pin_callback => PinCallback(aTransactionId: u64, aPin: *const nsACString)); + fn pin_callback(&self, transaction_id: u64, pin: &nsACString) -> Result<(), nsresult> { + let mut guard = self.pin_receiver.lock().or(Err(NS_ERROR_FAILURE))?; + match guard.take() { + // The pin_receiver is single-use. + Some((tid, channel)) if tid == transaction_id => channel + .send(Pin::new(&pin.to_string())) + .or(Err(NS_ERROR_FAILURE)), + // Either we weren't expecting a pin, or the controller is confused + // about which transaction is active. Neither is recoverable, so it's + // OK to drop the PinReceiver here. + _ => Err(NS_ERROR_FAILURE), + } + } + + xpcom_method!(make_credential => MakeCredential(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsICtapRegisterArgs)); + fn make_credential( + &self, + tid: u64, + browsing_context_id: u64, + args: *const nsICtapRegisterArgs, + ) -> Result<(), nsresult> { + if args.is_null() { + return Err(NS_ERROR_NULL_POINTER); + } + let args = unsafe { &*args }; + + let mut origin = nsString::new(); + unsafe { args.GetOrigin(&mut *origin) }.to_result()?; + + let mut relying_party_id = nsString::new(); + unsafe { args.GetRpId(&mut *relying_party_id) }.to_result()?; + + let mut challenge = ThinVec::new(); + unsafe { args.GetChallenge(&mut challenge) }.to_result()?; + + let mut client_data_json = nsCString::new(); + unsafe { args.GetClientDataJSON(&mut *client_data_json) }.to_result()?; + + let mut timeout_ms = 0u32; + unsafe { args.GetTimeoutMS(&mut timeout_ms) }.to_result()?; + + let mut exclude_list = ThinVec::new(); + unsafe { args.GetExcludeList(&mut exclude_list) }.to_result()?; + let exclude_list = exclude_list + .iter_mut() + .map(|id| PublicKeyCredentialDescriptor { + id: id.to_vec(), + transports: vec![], + }) + .collect(); + + let mut relying_party_name = nsString::new(); + unsafe { args.GetRpName(&mut *relying_party_name) }.to_result()?; + + let mut user_id = ThinVec::new(); + unsafe { args.GetUserId(&mut user_id) }.to_result()?; + + let mut user_name = nsString::new(); + unsafe { args.GetUserName(&mut *user_name) }.to_result()?; + + let mut user_display_name = nsString::new(); + unsafe { args.GetUserDisplayName(&mut *user_display_name) }.to_result()?; + + let mut cose_algs = ThinVec::new(); + unsafe { args.GetCoseAlgs(&mut cose_algs) }.to_result()?; + let pub_cred_params = cose_algs + .iter() + .map(|alg| PublicKeyCredentialParameters::try_from(*alg).unwrap()) + .collect(); + + let mut require_resident_key = false; + unsafe { args.GetRequireResidentKey(&mut require_resident_key) }.to_result()?; + + let mut user_verification = nsString::new(); + unsafe { args.GetUserVerification(&mut *user_verification) }.to_result()?; + let require_user_verification = user_verification.eq("required"); + + let mut attestation_conveyance_preference = nsString::new(); + unsafe { args.GetAttestationConveyancePreference(&mut *attestation_conveyance_preference) } + .to_result()?; + let none_attestation = attestation_conveyance_preference.eq("none"); + + // TODO(Bug 1593571) - Add this to the extensions + // let mut hmac_create_secret = None; + // let mut maybe_hmac_create_secret = false; + // match unsafe { args.GetHmacCreateSecret(&mut maybe_hmac_create_secret) }.to_result() { + // Ok(_) => hmac_create_secret = Some(maybe_hmac_create_secret), + // _ => (), + // } + + let info = RegisterArgsCtap2 { + challenge: challenge.to_vec(), + relying_party: RelyingParty { + id: relying_party_id.to_string(), + name: None, + icon: None, + }, + origin: origin.to_string(), + user: User { + id: user_id.to_vec(), + icon: None, + name: Some(user_name.to_string()), + display_name: None, + }, + pub_cred_params, + exclude_list, + options: MakeCredentialsOptions { + resident_key: require_resident_key.then_some(true), + user_verification: require_user_verification.then_some(true), + }, + extensions: Default::default(), + pin: None, + }; + + let (status_tx, status_rx) = channel::(); + let pin_receiver = self.pin_receiver.clone(); + let controller = self.controller.clone(); + let status_origin = origin.to_string(); + RunnableBuilder::new( + "AuthrsTransport::MakeCredential::StatusReceiver", + move || { + status_callback( + status_rx, + tid, + &status_origin, + browsing_context_id, + controller, + EventType::Register, + pin_receiver, + ) + }, + ) + .may_block(true) + .dispatch_background_task()?; + + let controller = self.controller.clone(); + let state_callback = StateCallback::>::new( + Box::new(move |result| { + let result = match result { + Ok(RegisterResult::CTAP1(..)) => Err(AuthenticatorError::VersionMismatch( + "AuthrsTransport::MakeCredential", + 2, + )), + Ok(RegisterResult::CTAP2(mut attestation_object, client_data)) => { + // Tokens always provide attestation, but the user may have asked we not + // include the attestation statement in the response. + if none_attestation { + attestation_object.att_statement = AttestationStatement::None; + } + Ok(RegisterResult::CTAP2(attestation_object, client_data)) + } + Err(e) => Err(e), + }; + let _ = controller.finish_register(tid, result); + }), + ); + + self.auth_service + .borrow_mut() + .register(timeout_ms as u64, info.into(), status_tx, state_callback) + .or(Err(NS_ERROR_FAILURE)) + } + + xpcom_method!(get_assertion => GetAssertion(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsICtapSignArgs)); + fn get_assertion( + &self, + tid: u64, + browsing_context_id: u64, + args: *const nsICtapSignArgs, + ) -> Result<(), nsresult> { + if args.is_null() { + return Err(NS_ERROR_NULL_POINTER); + } + let args = unsafe { &*args }; + + let mut origin = nsString::new(); + unsafe { args.GetOrigin(&mut *origin) }.to_result()?; + + let mut relying_party_id = nsString::new(); + unsafe { args.GetRpId(&mut *relying_party_id) }.to_result()?; + + let mut challenge = ThinVec::new(); + unsafe { args.GetChallenge(&mut challenge) }.to_result()?; + + let mut client_data_json = nsCString::new(); + unsafe { args.GetClientDataJSON(&mut *client_data_json) }.to_result()?; + + let mut timeout_ms = 0u32; + unsafe { args.GetTimeoutMS(&mut timeout_ms) }.to_result()?; + + let mut allow_list = ThinVec::new(); + unsafe { args.GetAllowList(&mut allow_list) }.to_result()?; + let allow_list: Vec<_> = allow_list + .iter_mut() + .map(|id| PublicKeyCredentialDescriptor { + id: id.to_vec(), + transports: vec![], + }) + .collect(); + + let mut user_verification = nsString::new(); + unsafe { args.GetUserVerification(&mut *user_verification) }.to_result()?; + let require_user_verification = user_verification.eq("required"); + + let mut alternate_rp_id = None; + let mut maybe_alternate_rp_id = nsString::new(); + match unsafe { args.GetAppId(&mut *maybe_alternate_rp_id) }.to_result() { + Ok(_) => alternate_rp_id = Some(maybe_alternate_rp_id.to_string()), + _ => (), + } + + let (status_tx, status_rx) = channel::(); + let pin_receiver = self.pin_receiver.clone(); + let controller = self.controller.clone(); + let status_origin = origin.to_string(); + RunnableBuilder::new("AuthrsTransport::GetAssertion::StatusReceiver", move || { + status_callback( + status_rx, + tid, + &status_origin, + browsing_context_id, + controller, + EventType::Sign, + pin_receiver, + ) + }) + .may_block(true) + .dispatch_background_task()?; + + let uniq_allowed_cred = if allow_list.len() == 1 { + allow_list.first().cloned() + } else { + None + }; + + let controller = self.controller.clone(); + let state_callback = + StateCallback::>::new(Box::new(move |result| { + let result = match result { + Ok(SignResult::CTAP1(..)) => Err(AuthenticatorError::VersionMismatch( + "AuthrsTransport::GetAssertion", + 2, + )), + Ok(SignResult::CTAP2(mut assertion_object, client_data)) => { + // In CTAP 2.0, but not CTAP 2.1, the assertion object's credential field + // "May be omitted if the allowList has exactly one Credential." If we had + // a unique allowed credential, then copy its descriptor to the output. + if uniq_allowed_cred.is_some() { + if let Some(assertion) = assertion_object.0.first_mut() { + if assertion.credentials.is_none() { + assertion.credentials = uniq_allowed_cred.clone(); + } + } + } + Ok(SignResult::CTAP2(assertion_object, client_data)) + } + Err(e) => Err(e), + }; + let _ = controller.finish_sign(tid, result); + })); + + let info = SignArgsCtap2 { + challenge: challenge.to_vec(), + relying_party_id: relying_party_id.to_string(), + origin: origin.to_string(), + allow_list, + options: GetAssertionOptions { + user_presence: Some(true), + user_verification: require_user_verification.then_some(true), + }, + extensions: Default::default(), + pin: None, + alternate_rp_id, + }; + + self.auth_service + .borrow_mut() + .sign(timeout_ms as u64, info.into(), status_tx, state_callback) + .or(Err(NS_ERROR_FAILURE)) + } + + xpcom_method!(cancel => Cancel()); + fn cancel(&self) -> Result { + // We may be waiting for a pin. Drop the channel to release the + // state machine from `ask_user_for_pin`. + drop(self.pin_receiver.lock().or(Err(NS_ERROR_FAILURE))?.take()); + + match &self.auth_service.borrow_mut().cancel() { + Ok(_) => Ok(NS_OK), + Err(e) => Err(authrs_to_nserror(e)), + } + } +} + +#[no_mangle] +pub extern "C" fn authrs_transport_constructor( + result: *mut *const nsIWebAuthnTransport, +) -> nsresult { + let mut auth_service = match AuthenticatorService::new(CtapVersion::CTAP2) { + Ok(auth_service) => auth_service, + _ => return NS_ERROR_FAILURE, + }; + auth_service.add_detected_transports(); + let wrapper = AuthrsTransport::allocate(InitAuthrsTransport { + auth_service: RefCell::new(auth_service), + controller: Controller(RefCell::new(std::ptr::null())), + pin_receiver: Arc::new(Mutex::new(None)), + }); + unsafe { + RefPtr::new(wrapper.coerce::()).forget(&mut *result); + } + NS_OK +} diff --git a/dom/webauthn/moz.build b/dom/webauthn/moz.build index 58c03de1eab32..838d8923e63b5 100644 --- a/dom/webauthn/moz.build +++ b/dom/webauthn/moz.build @@ -9,7 +9,7 @@ with Files("**"): IPDL_SOURCES += ["PWebAuthnTransaction.ipdl"] -XPIDL_SOURCES += ["nsIU2FTokenManager.idl"] +XPIDL_SOURCES += ["nsIU2FTokenManager.idl", "nsIWebAuthnController.idl"] XPIDL_MODULE = "dom_webauthn" @@ -17,13 +17,13 @@ EXPORTS.mozilla.dom += [ "AuthenticatorAssertionResponse.h", "AuthenticatorAttestationResponse.h", "AuthenticatorResponse.h", - "CTAPHIDTokenManager.h", "PublicKeyCredential.h", "U2FHIDTokenManager.h", "U2FSoftTokenManager.h", "U2FTokenManager.h", "U2FTokenTransport.h", "WebAuthnCBORUtil.h", + "WebAuthnController.h", "WebAuthnManager.h", "WebAuthnManagerBase.h", "WebAuthnTransactionChild.h", @@ -36,14 +36,18 @@ UNIFIED_SOURCES += [ "AuthenticatorAssertionResponse.cpp", "AuthenticatorAttestationResponse.cpp", "AuthenticatorResponse.cpp", + "AuthrsTransport.cpp", "cbor-cpp/src/encoder.cpp", "cbor-cpp/src/output_dynamic.cpp", - "CTAPHIDTokenManager.cpp", + "CtapArgs.cpp", + "CtapResults.cpp", "PublicKeyCredential.cpp", "U2FHIDTokenManager.cpp", "U2FSoftTokenManager.cpp", + "U2FSoftTokenTransport.cpp", "U2FTokenManager.cpp", "WebAuthnCBORUtil.cpp", + "WebAuthnController.cpp", "WebAuthnManager.cpp", "WebAuthnManagerBase.cpp", "WebAuthnTransactionChild.cpp", diff --git a/dom/webauthn/nsIU2FTokenManager.idl b/dom/webauthn/nsIU2FTokenManager.idl index 685342d9eb8bb..4f835dc853246 100644 --- a/dom/webauthn/nsIU2FTokenManager.idl +++ b/dom/webauthn/nsIU2FTokenManager.idl @@ -6,9 +6,9 @@ #include "nsISupports.idl" /** - * TODO: U2FTokenManager needs to be renamed to CTAPTokenManager or similar, - * because it now contains also CTAP2 functionality (e.g. pinCallback) - * See bug 1801643 + * TODO(1737205,1819414) Fold this interface into nsIWebAuthnController when we + * remove the legacy U2F DOM API. + * * nsIU2FTokenManager * * An interface to the U2FTokenManager singleton. @@ -30,29 +30,6 @@ interface nsIU2FTokenManager : nsISupports void resumeRegister(in uint64_t aTransactionID, in bool aForceNoneAttestation); - /** - * Resumes the current WebAuthn transaction. - * This is used only when the hardware token requires - * user-verification and is thus protected by a PIN. - * - * @param aPin : PIN the user entered after being prompted. - */ - void pinCallback(in ACString aPin); - - /** - * Resumes the current WebAuthn transaction if that matches the given - * transaction ID. This is used only when the hardware token returned - * multiple results for signin in and the user needs to select with which - * to log in. - * TODO(MS): This is a CTAP2 operation, so U2FTokenManager is probably - * not the ideal place for this function. It is a shortcut for now. - * - * @param aTransactionID : The ID of the transaction to resume. - * @param idx : The index of the selected result - */ - void resumeWithSelectedSignResult(in uint64_t aTransactionID, - in uint64_t idx); - /** * Cancels the current WebAuthn/U2F transaction if that matches the given * transaction ID. diff --git a/dom/webauthn/nsIWebAuthnController.idl b/dom/webauthn/nsIWebAuthnController.idl new file mode 100644 index 0000000000000..63183aa4f1df4 --- /dev/null +++ b/dom/webauthn/nsIWebAuthnController.idl @@ -0,0 +1,219 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" +#include "nsIU2FTokenManager.idl" + +typedef long COSEAlgorithmIdentifier; + +// The nsICtapRegisterArgs interface encapsulates the arguments to the CTAP +// authenticatorMakeCredential command as defined in +// https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorMakeCredential +// It is essentially a shim that allows data to be copied from an IPDL-defined +// WebAuthnMakeCredentialInfo C++ struct to an authenticator-rs defined +// RegisterArgsCtap2 Rust struct. +// +[uuid(2fc8febe-a277-11ed-bda2-8f6495a5e75c)] +interface nsICtapRegisterArgs : nsISupports { + // TODO(Bug 1820035) The origin is only used for prompt callbacks. Refactor and remove. + readonly attribute AString origin; + + // TODO(Bug 1820037) The spec only has `clientDataHash` here. We could refactor + // authenticator-rs to remove clientDataJSON + // readonly attribute Array clientDataHash; + readonly attribute ACString clientDataJSON; + // The challenge field of clientDataJSON. Can be removed if we refactor + // authenticators to only require clientDataHash. + readonly attribute Array challenge; + + // A PublicKeyCredentialRpEntity + readonly attribute AString rpId; + [must_use] readonly attribute AString rpName; + + // A PublicKeyCredentialUserEntity + [must_use] readonly attribute Array userId; + [must_use] readonly attribute AString userName; + [must_use] readonly attribute AString userDisplayName; + + // The spec defines this as a sequence. + // We require type = "public-key" and only serialize the alg fields. + [must_use] readonly attribute Array coseAlgs; + + // The spec defines this as a sequence. + // We only include the ID field, as the transport field is optional and we + // can assume that the type is "public-key". + readonly attribute Array > excludeList; + + // CTAP2 passes extensions in a CBOR map of extension identifier -> + // WebAuthn AuthenticationExtensionsClientInputs. That's not feasible here. + // So we define a getter for each supported extension input and use the + // return code to signal presence. + [must_use] readonly attribute bool hmacCreateSecret; + + // Options. + [must_use] readonly attribute bool requireResidentKey; + [must_use] readonly attribute AString userVerification; + [must_use] readonly attribute AString authenticatorAttachment; + + // This is the WebAuthn PublicKeyCredentialCreationOptions timeout. + // Arguably we don't need to pass it through since WebAuthnController can + // cancel transactions. + readonly attribute uint32_t timeoutMS; + + // This is the WebAuthn PublicKeyCredentialCreationOptions attestation. + // We might overwrite the provided value with "none" if the user declines the + // consent popup. + [must_use] readonly attribute AString attestationConveyancePreference; +}; + +// The nsICtapSignArgs interface encapsulates the arguments to the CTAP +// authenticatorGetAssertion command as defined in +// https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorGetAssertion +// It is essentially a shim that allows data to be copied from an IPDL-defined +// WebAuthnGetAssertionInfo C++ struct to an authenticator-rs defined +// SignArgsCtap2 Rust struct. +// +[uuid(2e621cf4-a277-11ed-ae00-bf41a54ef553)] +interface nsICtapSignArgs : nsISupports { + // TODO(Bug 1820035) The origin is only used for prompt callbacks. Refactor and remove. + readonly attribute AString origin; + + // The spec only asks for the ID field of a PublicKeyCredentialRpEntity here + readonly attribute AString rpId; + + // TODO(Bug 1820037) The spec only has `clientDataHash` here. We could refactor + // authenticator-rs to remove clientDataJSON + // readonly attribute Array clientDataHash; + readonly attribute ACString clientDataJSON; + // The challenge field of clientDataJSON. Can be removed if we refactor + // authenticators to only require clientDataHash. + readonly attribute Array challenge; + + // The spec defines this as a sequence. + // We only include the ID field, as the transport field is optional and we + // can assume that the type is "public-key". + readonly attribute Array > allowList; + + // CTAP2 passes extensions in a CBOR map of extension identifier -> + // WebAuthn AuthenticationExtensionsClientInputs. That's not feasible here. + // So we define a getter for each supported extension input and use the + // return code to signal presence. + [must_use] readonly attribute bool hmacCreateSecret; + [must_use] readonly attribute AString appId; + [must_use] readonly attribute Array appIdHash; + + // Options + [must_use] readonly attribute AString userVerification; + + // This is the WebAuthn PublicKeyCredentialCreationOptions timeout. + // Arguably we don't need to pass it through since WebAuthnController can + // cancel transactions. + readonly attribute unsigned long timeoutMS; +}; + +// The nsICtapRegisterResult interface is used to construct IPDL-defined +// WebAuthnMakeCredentialResult from either Rust or C++. +// +[uuid(0567c384-a728-11ed-85f7-030324a370f0)] +interface nsICtapRegisterResult : nsISupports { + readonly attribute nsresult status; + + readonly attribute ACString clientDataJSON; + + // The serialied attestation object as defined in + // https://www.w3.org/TR/webauthn-2/#sctn-attestation + // Includes the format, the attestation statement, and + // the authenticator data. + readonly attribute Array attestationObject; + + // The Credential ID field of the Attestation Object's Attested + // Credential Data. This is used to construct the rawID field of a + // WebAuthn PublicKeyCredential without having to parse the + // attestationObject. + readonly attribute Array credentialId; + + // Bug 1536155 + // readonly attribute Array transports; + + // Bug 1816519 + // readonly attribute Array authenticatorData; + + // Bug 1816520 + // readonly attribute Array publicKey + // readonly attribute COSEAlgorithmIdentifier publicKeyAlgorithm; + + // bug 1593571 + // readonly attribute bool hmacCreateSecret; +}; + + +// The nsICtapSignResult interface is used to construct IPDL-defined +// WebAuthnGetAssertionResult from either Rust or C++. +// +[uuid(05fff816-a728-11ed-b9ac-ff38cc2c8c28)] +interface nsICtapSignResult : nsISupports { + readonly attribute nsresult status; + + // The ID field of the PublicKeyCredentialDescriptor returned + // from authenticatorGetAssertion. + readonly attribute Array credentialId; + + // The authData field of the authenticatorGetAssertion response + readonly attribute Array authenticatorData; + + // The signature field of the authenticatorGetAssertion response + readonly attribute Array signature; + + // The ID field of the PublicKeyCredentialUserEntity returned from + // authenticatorGetAssertion. (Optional) + [must_use] readonly attribute Array userHandle; + + // The displayName field of the PublicKeyCredentialUserEntity + // returned from authenticatorGetAssertion. (Optional) + [must_use] readonly attribute ACString userName; + + // The SHA-256 hash of the RP ID of the requester. Used to + // implement the FIDO AppID extension. + readonly attribute Array rpIdHash; + + // bug 1593571 + // readonly attribute bool hmacCreateSecret; +}; + +// The nsIWebAuthnController interface coordinates interactions between the user +// and the authenticator to drive a WebAuthn transaction forward. +// It allows an nsIWebAuthnTransport to +// 1) prompt the user for input, +// 2) receive a callback from a prompt, and +// 3) return results to the content process. +// +[scriptable, uuid(c0744f48-ad64-11ed-b515-cf5149f4d6a6)] +interface nsIWebAuthnController : nsIU2FTokenManager +{ + // Prompt callbacks + void pinCallback(in uint64_t aTransactionId, in ACString aPin); + void signatureSelectionCallback(in uint64_t aTransactionId, in uint64_t aIndex); + + // Authenticator callbacks + [noscript] void sendPromptNotificationPreformatted(in uint64_t aTransactionId, in ACString aJSON); + [noscript] void finishRegister(in uint64_t aTransactionId, in nsICtapRegisterResult aResult); + [noscript] void finishSign(in uint64_t aTransactionId, in ACString aClientDataJson, in Array aResult); +}; + +// The nsIWebAuthnTransport interface allows a C++ implemented nsIWebAuthnController to interact +// with authenticators written in both Rust and C++ +[uuid(e236a9b4-a26f-11ed-b6cc-07a9834e19b1)] +interface nsIWebAuthnTransport : nsISupports +{ + attribute nsIWebAuthnController controller; + + void makeCredential(in uint64_t aTransactionId, in uint64_t browsingContextId, in nsICtapRegisterArgs args); + void getAssertion(in uint64_t aTransactionId, in uint64_t browsingContextId, in nsICtapSignArgs args); + + // These are prompt callbacks but they're not intended to be called directly from + // JavaScript---they are proxied through the nsIWebAuthnController first. + [noscript] void pinCallback(in uint64_t aTransactionId, in ACString aPin); + [noscript] void cancel(); +}; diff --git a/layout/build/nsLayoutStatics.cpp b/layout/build/nsLayoutStatics.cpp index 5c01a002eb8d8..da8dac26ffa3e 100644 --- a/layout/build/nsLayoutStatics.cpp +++ b/layout/build/nsLayoutStatics.cpp @@ -107,6 +107,7 @@ #include "mozilla/dom/Document.h" #include "mozilla/dom/WebIDLGlobalNameHash.h" #include "mozilla/dom/U2FTokenManager.h" +#include "mozilla/dom/WebAuthnController.h" #ifdef OS_WIN # include "mozilla/dom/WinWebAuthnManager.h" #endif @@ -263,6 +264,7 @@ nsresult nsLayoutStatics::Initialize() { mozilla::RemoteLazyInputStreamStorage::Initialize(); mozilla::dom::U2FTokenManager::Initialize(); + mozilla::dom::WebAuthnController::Initialize(); #ifdef OS_WIN mozilla::dom::WinWebAuthnManager::Initialize(); diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js index aebd4015e2712..e57b5565ae9cd 100644 --- a/mobile/android/app/mobile.js +++ b/mobile/android/app/mobile.js @@ -608,6 +608,3 @@ pref("extensions.systemAddon.update.enabled", true); // E10s stuff. We don't support 'privileged' process types. pref("browser.tabs.remote.separatePrivilegedContentProcess", false); pref("browser.tabs.remote.enforceRemoteTypeRestrictions", false); - -// Allow Web Authentication -pref("security.webauth.webauthn_enable_android_fido2", true); diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index acc4f840b9a28..41c2c14c7f519 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -13528,10 +13528,42 @@ # WebAuthn CTAP2 support - name: security.webauthn.ctap2 - type: bool + type: RelaxedAtomicBool value: true mirror: always +# Dispatch WebAuthn requests to the software CTAP1 token. +# (mutually exclusive with webauthn_enable_android_fido2, +# and webauthn_enable_usbtoken) +- name: security.webauth.webauthn_enable_softtoken + type: RelaxedAtomicBool + value: false + mirror: always + +# Dispatch WebAuthn requests to the Android platform API +- name: security.webauth.webauthn_enable_android_fido2 + type: RelaxedAtomicBool + value: @IS_ANDROID@ + mirror: always + +# Dispatch WebAuthn requests to authenticator-rs +- name: security.webauth.webauthn_enable_usbtoken + type: RelaxedAtomicBool + value: @IS_NOT_ANDROID@ + mirror: always + +# Skip direct attestation consent prompts (for tests). +- name: security.webauth.webauthn_testing_allow_direct_attestation + type: RelaxedAtomicBool + value: false + mirror: always + +# Global counter for U2F soft token operations +- name: security.webauth.softtoken_counter + type: RelaxedAtomicUint32 + value: 0 + mirror: always + # Block Worker/SharedWorker scripts with wrong MIME type. - name: security.block_Worker_with_wrong_mime type: bool diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index 5392a97147788..0b156249e14a6 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -37,19 +37,6 @@ pref("security.remember_cert_checkbox_default_setting", true); // x_11_x: COSE is required, PKCS#7 disabled (fail when present) pref("security.signed_app_signatures.policy", 2); -// Only one of ["enable_softtoken", "enable_usbtoken", -// "webauthn_enable_android_fido2"] should be true at a time, as the -// softtoken will override the other two. Note android's pref is set in -// mobile.js / geckoview-prefs.js -pref("security.webauth.webauthn_enable_softtoken", false); - -#ifdef MOZ_WIDGET_ANDROID - // the Rust usbtoken support does not function on Android - pref("security.webauth.webauthn_enable_usbtoken", false); -#else - pref("security.webauth.webauthn_enable_usbtoken", true); -#endif - pref("security.xfocsp.errorReporting.enabled", true); pref("security.xfocsp.errorReporting.automatic", false); diff --git a/toolkit/library/rust/shared/Cargo.toml b/toolkit/library/rust/shared/Cargo.toml index b6e1b6d8f3b04..e23158160aed3 100644 --- a/toolkit/library/rust/shared/Cargo.toml +++ b/toolkit/library/rust/shared/Cargo.toml @@ -39,7 +39,7 @@ tokio-reactor = { version = "=0.1.3", optional = true } # audioipc2-client and audioipc2-server. tokio-threadpool = { version = "=0.1.17", optional = true } encoding_glue = { path = "../../../../intl/encoding_glue" } -authenticator = { version = "0.4.0-alpha.10", features = ["gecko"] } +authrs_bridge = { path = "../../../../dom/webauthn/authrs_bridge" } gkrust_utils = { path = "../../../../xpcom/rust/gkrust_utils" } gecko_logger = { path = "../../../../xpcom/rust/gecko_logger" } rsdparsa_capi = { path = "../../../../dom/media/webrtc/sdp/rsdparsa_capi" } diff --git a/toolkit/library/rust/shared/lib.rs b/toolkit/library/rust/shared/lib.rs index d099dc48258f5..f5cf5b455de0c 100644 --- a/toolkit/library/rust/shared/lib.rs +++ b/toolkit/library/rust/shared/lib.rs @@ -13,7 +13,7 @@ extern crate audioipc2_server; extern crate audioipc_client; #[cfg(feature = "cubeb-remoting")] extern crate audioipc_server; -extern crate authenticator; +extern crate authrs_bridge; #[cfg(feature = "bitsdownload")] extern crate bitsdownload; #[cfg(feature = "moz_places")]