tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit bd2ee9467d71dcede62051cb3819e1bea90ff301
parent 5298097d24bec811fbe949217f2865d45c42d117
Author: John M. Schanck <jschanck@mozilla.com>
Date:   Mon, 27 Oct 2025 21:38:42 +0000

Bug 1992469 - support for WebAuthn PublicKeyCredentialHint. r=keeler,webidl,tschuster

Differential Revision: https://phabricator.services.mozilla.com/D270060

Diffstat:
Mdom/webauthn/MacOSWebAuthnService.mm | 26++++++++++++++++++++++++--
Mdom/webauthn/PWebAuthnTransaction.ipdl | 2++
Mdom/webauthn/PublicKeyCredential.cpp | 4++++
Mdom/webauthn/WebAuthnArgs.cpp | 12++++++++++++
Mdom/webauthn/WebAuthnHandler.cpp | 4++--
Mdom/webauthn/WinWebAuthnService.cpp | 52++++++++++++++++++++++++++++++++++++++++++----------
Mdom/webauthn/nsIWebAuthnArgs.idl | 4++++
Mdom/webauthn/tests/test_webauthn_serialization.html | 24++++++++++++++++++++----
Mdom/webidl/WebAuthentication.webidl | 2++
9 files changed, 112 insertions(+), 18 deletions(-)

diff --git a/dom/webauthn/MacOSWebAuthnService.mm b/dom/webauthn/MacOSWebAuthnService.mm @@ -823,6 +823,20 @@ MacOSWebAuthnService::MakeCredential(uint64_t aTransactionId, *userVerificationPreference; } + if (__builtin_available(macos 13.5, *)) { + // Show the hybrid transport unless we have a non-empty hint list and + // none of the hints are for the hybrid transport. + bool hasHybridHint = false; + nsTArray<nsString> hints; + (void)aArgs->GetHints(hints); + for (nsString& hint : hints) { + if (hint.Equals(u"hybrid"_ns)) { + hasHybridHint = true; + } + } + platformRegistrationRequest.shouldShowHybridTransport = + hints.Length() == 0 || hasHybridHint; + } if (__builtin_available(macos 14.0, *)) { bool largeBlobSupportRequired; nsresult rv = @@ -1173,11 +1187,19 @@ void MacOSWebAuthnService::DoGetAssertion( *userVerificationPreference; } if (__builtin_available(macos 13.5, *)) { - // Show the hybrid transport option if (1) we have no transport hints - // or (2) at least one allow list entry lists the hybrid transport. + // Show the hybrid transport option if (1) none of the allowlist + // credentials list transports, or (2) at least one allow list entry + // lists the hybrid transport, or (3) the request has the hybrid hint. bool shouldShowHybridTransport = !transports || (transports & MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_HYBRID); + nsTArray<nsString> hints; + (void)aArgs->GetHints(hints); + for (nsString& hint : hints) { + if (hint.Equals(u"hybrid"_ns)) { + shouldShowHybridTransport = true; + } + } platformAssertionRequest.shouldShowHybridTransport = shouldShowHybridTransport; } diff --git a/dom/webauthn/PWebAuthnTransaction.ipdl b/dom/webauthn/PWebAuthnTransaction.ipdl @@ -143,6 +143,7 @@ struct WebAuthnMakeCredentialInfo { WebAuthnExtension[] Extensions; WebAuthnAuthenticatorSelection AuthenticatorSelection; nsString attestationConveyancePreference; + nsString[] Hints; }; struct WebAuthnMakeCredentialResult { @@ -168,6 +169,7 @@ struct WebAuthnGetAssertionInfo { WebAuthnExtension[] Extensions; nsString userVerificationRequirement; bool ConditionallyMediated; + nsString[] Hints; }; struct WebAuthnGetAssertionResult { diff --git a/dom/webauthn/PublicKeyCredential.cpp b/dom/webauthn/PublicKeyCredential.cpp @@ -660,6 +660,8 @@ void PublicKeyCredential::ParseCreationOptionsFromJSON( aResult.mAuthenticatorSelection = aOptions.mAuthenticatorSelection.Value(); } + aResult.mHints = aOptions.mHints; + aResult.mAttestation = aOptions.mAttestation; if (aOptions.mExtensions.WasPassed()) { @@ -753,6 +755,8 @@ void PublicKeyCredential::ParseRequestOptionsFromJSON( aResult.mUserVerification = aOptions.mUserVerification; + aResult.mHints = aOptions.mHints; + if (aOptions.mExtensions.WasPassed()) { if (aOptions.mExtensions.Value().mAppid.WasPassed()) { aResult.mExtensions.mAppid.Construct( diff --git a/dom/webauthn/WebAuthnArgs.cpp b/dom/webauthn/WebAuthnArgs.cpp @@ -246,6 +246,12 @@ WebAuthnRegisterArgs::GetLargeBlobSupportRequired( return NS_ERROR_NOT_AVAILABLE; } +NS_IMETHODIMP +WebAuthnRegisterArgs::GetHints(nsTArray<nsString>& aHints) { + aHints.Assign(mInfo.Hints()); + return NS_OK; +} + NS_IMPL_ISUPPORTS(WebAuthnSignArgs, nsIWebAuthnSignArgs) NS_IMETHODIMP @@ -485,4 +491,10 @@ WebAuthnSignArgs::GetLargeBlobWrite(nsTArray<uint8_t>& aLargeBlobWrite) { return NS_ERROR_NOT_AVAILABLE; } +NS_IMETHODIMP +WebAuthnSignArgs::GetHints(nsTArray<nsString>& aHints) { + aHints.Assign(mInfo.Hints()); + return NS_OK; +} + } // namespace mozilla::dom diff --git a/dom/webauthn/WebAuthnHandler.cpp b/dom/webauthn/WebAuthnHandler.cpp @@ -393,7 +393,7 @@ already_AddRefed<Promise> WebAuthnHandler::MakeCredential( WebAuthnMakeCredentialInfo info(rpId, challenge, adjustedTimeout, excludeList, rpInfo, userInfo, coseAlgos, extensions, - authSelection, attestation); + authSelection, attestation, aOptions.mHints); // Set up the transaction state. Fallible operations should not be performed // below this line, as we must not leave the transaction state partially @@ -667,7 +667,7 @@ already_AddRefed<Promise> WebAuthnHandler::GetAssertion( WebAuthnGetAssertionInfo info( rpId, maybeAppId, challenge, adjustedTimeout, allowList, extensions, - aOptions.mUserVerification, aConditionallyMediated); + aOptions.mUserVerification, aConditionallyMediated, aOptions.mHints); // Set up the transaction state. Fallible operations should not be performed // below this line, as we must not leave the transaction state partially diff --git a/dom/webauthn/WinWebAuthnService.cpp b/dom/webauthn/WinWebAuthnService.cpp @@ -29,6 +29,10 @@ StaticRWLock gWinWebAuthnModuleLock; static bool gWinWebAuthnModuleUnusable = false; static HMODULE gWinWebAuthnModule = 0; +static const LPCWSTR gWebAuthnHintStrings[3] = { + WEBAUTHN_CREDENTIAL_HINT_SECURITY_KEY, + WEBAUTHN_CREDENTIAL_HINT_CLIENT_DEVICE, WEBAUTHN_CREDENTIAL_HINT_HYBRID}; + static decltype(WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable)* gWinWebauthnIsUVPAA = nullptr; static decltype(WebAuthNAuthenticatorMakeCredential)* @@ -49,7 +53,6 @@ static decltype(WebAuthNGetPlatformCredentialList)* gWinWebauthnGetPlatformCredentialList = nullptr; static decltype(WebAuthNFreePlatformCredentialList)* gWinWebauthnFreePlatformCredentialList = nullptr; - } // namespace /*********************************************************************** @@ -176,6 +179,18 @@ WinWebAuthnService::~WinWebAuthnService() { } // static +void PrunePublicKeyCredentialHints(const nsTArray<nsString>& aInHints, + /* out */ nsTArray<LPCWSTR>& aOutHints) { + for (const nsString& inputHint : aInHints) { + for (const LPCWSTR knownHint : gWebAuthnHintStrings) { + if (inputHint.Equals(knownHint)) { + aOutHints.AppendElement(knownHint); + } + } + } +} + +// static bool WinWebAuthnService::AreWebAuthNApisAvailable() { nsresult rv = EnsureWinWebAuthnModuleLoaded(); NS_ENSURE_SUCCESS(rv, false); @@ -593,10 +608,16 @@ WinWebAuthnService::MakeCredential(uint64_t aTransactionId, winPrivateBrowsing = TRUE; } + nsTArray<nsString> inputHints; + (void)aArgs->GetHints(inputHints); + + nsTArray<LPCWSTR> hints; + PrunePublicKeyCredentialHints(inputHints, hints); + // MakeCredentialOptions WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS WebAuthNCredentialOptions = { - WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_7, + WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_8, timeout, {0, NULL}, {0, NULL}, @@ -608,13 +629,16 @@ WinWebAuthnService::MakeCredential(uint64_t aTransactionId, &cancellationId, // CancellationId pExcludeCredentialList, WEBAUTHN_ENTERPRISE_ATTESTATION_NONE, - largeBlobSupport, // LargeBlobSupport - winPreferResidentKey, // PreferResidentKey - winPrivateBrowsing, // BrowserInPrivateMode - winEnablePrf, // EnablePrf - NULL, // LinkedDevice - 0, // size of JsonExt - NULL, // JsonExt + largeBlobSupport, // LargeBlobSupport + winPreferResidentKey, // PreferResidentKey + winPrivateBrowsing, // BrowserInPrivateMode + winEnablePrf, // EnablePrf + NULL, // LinkedDevice + 0, // size of JsonExt + NULL, // JsonExt + NULL, // PRFGlobalEval + (DWORD)hints.Length(), // Size of CredentialHints + hints.Elements(), // CredentialHints }; if (rgExtension.Length() != 0) { @@ -967,6 +991,12 @@ void WinWebAuthnService::DoGetAssertion( pAllowCredentialList = &allowCredentialList; } + nsTArray<nsString> inputHints; + (void)aArgs->GetHints(inputHints); + + nsTArray<LPCWSTR> hints; + PrunePublicKeyCredentialHints(inputHints, hints); + uint32_t timeout_u32; (void)aArgs->GetTimeoutMS(&timeout_u32); DWORD timeout = timeout_u32; @@ -980,7 +1010,7 @@ void WinWebAuthnService::DoGetAssertion( WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS WebAuthNAssertionOptions = { - WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_7, + WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_VERSION_8, timeout, {0, NULL}, {0, NULL}, @@ -1000,6 +1030,8 @@ void WinWebAuthnService::DoGetAssertion( FALSE, // AutoFill 0, // Size of JsonExt NULL, // JsonExt + (DWORD)hints.Length(), // Size of CredentialHints + hints.Elements(), // CredentialHints }; PWEBAUTHN_ASSERTION pWebAuthNAssertion = nullptr; diff --git a/dom/webauthn/nsIWebAuthnArgs.idl b/dom/webauthn/nsIWebAuthnArgs.idl @@ -65,6 +65,8 @@ interface nsIWebAuthnRegisterArgs : nsISupports { // consent popup. [must_use] readonly attribute AString attestationConveyancePreference; + readonly attribute Array<AString> hints; + readonly attribute boolean privateBrowsing; }; @@ -111,6 +113,8 @@ interface nsIWebAuthnSignArgs : nsISupports { // cancel transactions. readonly attribute unsigned long timeoutMS; + readonly attribute Array<AString> hints; + readonly attribute boolean conditionallyMediated; readonly attribute boolean privateBrowsing; diff --git a/dom/webauthn/tests/test_webauthn_serialization.html b/dom/webauthn/tests/test_webauthn_serialization.html @@ -36,6 +36,18 @@ is(arr.length, 0, `${description} (array should be empty)`); } + function stringArrayEquals(actual, expected, description) { + is(actual.length, expected.length, `${description} (actual and expected should have the same length)`); + for (let i = 0; i < actual.length; i++) { + if (actual[i] instanceof String) { + throw new Error(`actual[${i}] is not a string` + typeof actual[i]); + } + if (actual[i] !== expected[i]) { + throw new Error(`actual and expected differ in position ${i}: ${actual[i]} vs ${expected[i]}`); + } + } + } + function shouldThrow(func, expectedError, description) { let threw = false; try { @@ -55,7 +67,7 @@ pubKeyCredParams: [], }; let creationOptions = PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON); - is(Object.getOwnPropertyNames(creationOptions).length, 8, "creation options should have 8 properties"); + is(Object.getOwnPropertyNames(creationOptions).length, 9, "creation options should have 9 properties"); is(creationOptions.rp.id, undefined, "rp.id should be undefined"); is(creationOptions.rp.name, "Example", "rp.name should be Example"); arrayBufferEqualsArray(creationOptions.user.id, [ 250, 93, 234, 52, 180, 202, 38, 120 ], "user.id should be as expected"); @@ -70,6 +82,7 @@ is(creationOptions.authenticatorSelection.requireResidentKey, false, "creationOptions.authenticatorSelection.requireResidentKey should be false"); is(creationOptions.authenticatorSelection.userVerification, "preferred", "creationOptions.authenticatorSelection.userVerification should be preferred"); is(creationOptions.attestation, "none", "attestation should be none"); + stringArrayEquals(creationOptions.hints, [], "hints should be an empty array"); is(Object.getOwnPropertyNames(creationOptions.extensions).length, 0, "extensions should be an empty object"); }); @@ -105,7 +118,7 @@ }, }; let creationOptions = PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON); - is(Object.getOwnPropertyNames(creationOptions).length, 9, "creation options should have 9 properties"); + is(Object.getOwnPropertyNames(creationOptions).length, 10, "creation options should have 10 properties"); is(creationOptions.rp.name, "Example", "rp.name should be Example"); is(creationOptions.rp.id, "example.com", "rp.id should be example.com"); arrayBufferEqualsArray(creationOptions.user.id, [ 215, 212, 213, 166, 160, 65, 56, 3 ], "user.id should be as expected"); @@ -125,6 +138,7 @@ is(creationOptions.authenticatorSelection.residentKey, "required", "creationOptions.authenticatorSelection.residentKey should be required"); is(creationOptions.authenticatorSelection.requireResidentKey, true, "creationOptions.authenticatorSelection.requireResidentKey should be true"); is(creationOptions.authenticatorSelection.userVerification, "discouraged", "creationOptions.authenticatorSelection.userVerification should be discouraged"); + stringArrayEquals(creationOptions.hints, creationOptionsJSON.hints, "creationOptions.hints should be as expected"); is(creationOptions.attestation, "indirect", "attestation should be indirect"); is(creationOptions.extensions.appid, "https://www.example.com/appID", "extensions.appid should be https://www.example.com/appID"); is(creationOptions.extensions.credProps, true, "extensions.credProps should be true"); @@ -186,12 +200,13 @@ challenge: "3yW2WHD_jbU", }; let requestOptions = PublicKeyCredential.parseRequestOptionsFromJSON(requestOptionsJSON); - is(Object.getOwnPropertyNames(requestOptions).length, 4, "request options should have 4 properties"); + is(Object.getOwnPropertyNames(requestOptions).length, 5, "request options should have 5 properties"); arrayBufferEqualsArray(requestOptions.challenge, [ 223, 37, 182, 88, 112, 255, 141, 181 ], "challenge should be as expected"); is(requestOptions.timeout, undefined, "timeout should be undefined"); is(requestOptions.rpId, undefined, "rpId should be undefined"); isEmptyArray(requestOptions.allowCredentials, "allowCredentials should be an empty array"); is(requestOptions.userVerification, "preferred", "userVerification should be preferred"); + stringArrayEquals(requestOptions.hints, [], "hints should be an empty array"); is(Object.getOwnPropertyNames(requestOptions.extensions).length, 0, "extensions should be an empty object"); }); @@ -222,7 +237,7 @@ }, }; let requestOptions = PublicKeyCredential.parseRequestOptionsFromJSON(requestOptionsJSON); - is(Object.getOwnPropertyNames(requestOptions).length, 6, "request options should have 6 properties"); + is(Object.getOwnPropertyNames(requestOptions).length, 7, "request options should have 7 properties"); arrayBufferEqualsArray(requestOptions.challenge, [ 64, 7, 218, 103, 1, 16, 10, 68 ], "challenge should be as expected"); is(requestOptions.timeout, 25000, "timeout should be 25000"); is(requestOptions.rpId, "example.com", "rpId should be example.com"); @@ -232,6 +247,7 @@ is(requestOptions.allowCredentials[0].transports.length, 1, "allowCredentials[0].transports should have one element"); is(requestOptions.allowCredentials[0].transports[0], "smart-card", "allowCredentials[0].transports[0] should be usb"); is(requestOptions.userVerification, "discouraged", "userVerification should be discouraged"); + stringArrayEquals(requestOptions.hints, requestOptionsJSON.hints, "requestOptions.hints should be as expected"); is(requestOptions.extensions.appid, "https://www.example.com/anotherAppID", "extensions.appid should be https://www.example.com/anotherAppID"); arrayBufferEqualsArray(requestOptions.extensions.prf.eval.first, [102, 105, 114, 115, 116], "extensions.prf.eval.first should be 'first'"); arrayBufferEqualsArray(requestOptions.extensions.prf.eval.second, [115, 101, 99, 111, 110, 100], "extensions.prf.eval.second should be 'second'"); diff --git a/dom/webidl/WebAuthentication.webidl b/dom/webidl/WebAuthentication.webidl @@ -177,6 +177,7 @@ dictionary PublicKeyCredentialCreationOptions { sequence<PublicKeyCredentialDescriptor> excludeCredentials = []; // FIXME: bug 1493860: should this "= {}" be here? AuthenticatorSelectionCriteria authenticatorSelection = {}; + sequence<DOMString> hints = []; DOMString attestation = "none"; // FIXME: bug 1493860: should this "= {}" be here? AuthenticationExtensionsClientInputs extensions = {}; @@ -208,6 +209,7 @@ dictionary PublicKeyCredentialRequestOptions { USVString rpId; sequence<PublicKeyCredentialDescriptor> allowCredentials = []; DOMString userVerification = "preferred"; + sequence<DOMString> hints = []; // FIXME: bug 1493860: should this "= {}" be here? AuthenticationExtensionsClientInputs extensions = {}; };