| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <stdint.h> |
| |
| #include <memory> |
| #include <vector> |
| |
| #include "base/base64.h" |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/command_line.h" |
| #include "base/json/json_reader.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/bind.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/values.h" |
| #include "build/build_config.h" |
| #include "components/cbor/reader.h" |
| #include "content/browser/renderer_host/back_forward_cache_disable.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/browser/webauth/authenticator_environment_impl.h" |
| #include "content/browser/webauth/authenticator_impl.h" |
| #include "content/public/browser/authenticator_request_client_delegate.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/navigation_throttle.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_contents_observer.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/test/back_forward_cache_util.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/content_browser_test.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/public/test/content_mock_cert_verifier.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/shell/browser/shell.h" |
| #include "content/test/did_commit_navigation_interceptor.h" |
| #include "crypto/sha2.h" |
| #include "crypto/signature_verifier.h" |
| #include "device/base/features.h" |
| #include "device/fido/device_public_key_extension.h" |
| #include "device/fido/fake_fido_discovery.h" |
| #include "device/fido/features.h" |
| #include "device/fido/fido_discovery_factory.h" |
| #include "device/fido/fido_test_data.h" |
| #include "device/fido/fido_types.h" |
| #include "device/fido/hid/fake_hid_impl_for_testing.h" |
| #include "device/fido/mock_fido_device.h" |
| #include "device/fido/p256_public_key.h" |
| #include "device/fido/public_key.h" |
| #include "device/fido/test_callback_receiver.h" |
| #include "device/fido/virtual_fido_device_factory.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/mojom/webauthn/authenticator.mojom.h" |
| |
| #if BUILDFLAG(IS_WIN) |
| #include <windows.h> |
| |
| #include "device/fido/win/fake_webauthn_api.h" |
| #endif |
| |
| namespace content { |
| |
| namespace { |
| |
| using blink::mojom::Authenticator; |
| using blink::mojom::AuthenticatorStatus; |
| using blink::mojom::GetAssertionAuthenticatorResponsePtr; |
| using blink::mojom::MakeCredentialAuthenticatorResponsePtr; |
| using blink::mojom::WebAuthnDOMExceptionDetailsPtr; |
| |
| using TestCreateCallbackReceiver = |
| device::test::StatusAndValuesCallbackReceiver< |
| AuthenticatorStatus, |
| MakeCredentialAuthenticatorResponsePtr, |
| WebAuthnDOMExceptionDetailsPtr>; |
| |
| using TestGetCallbackReceiver = device::test::StatusAndValuesCallbackReceiver< |
| AuthenticatorStatus, |
| GetAssertionAuthenticatorResponsePtr, |
| WebAuthnDOMExceptionDetailsPtr>; |
| |
| constexpr char kOkMessage[] = "webauth: OK"; |
| |
| constexpr char kPublicKeyErrorMessage[] = |
| "webauth: TypeError: Failed to execute 'create' on 'CredentialsContainer': " |
| "Failed to read the 'publicKey' property from 'CredentialCreationOptions': " |
| "Failed to read the 'rp' property from 'PublicKeyCredentialCreationOptions'" |
| ": The provided value is not of type 'PublicKeyCredentialRpEntity'."; |
| |
| constexpr char kNotAllowedErrorMessage[] = |
| "webauth: NotAllowedError: The operation either timed out or was not " |
| "allowed. See: " |
| "https://www.w3.org/TR/webauthn-2/#sctn-privacy-considerations-client."; |
| |
| #if BUILDFLAG(IS_WIN) |
| constexpr char kInvalidStateErrorMessage[] = |
| "webauth: InvalidStateError: The user attempted to register an " |
| "authenticator that contains one of the credentials already registered " |
| "with the relying party."; |
| #endif // BUILDFLAG(IS_WIN) |
| |
| constexpr char kResidentCredentialsErrorMessage[] = |
| "webauth: NotSupportedError: Resident credentials or empty " |
| "'allowCredentials' lists are not supported at this time."; |
| |
| constexpr char kRelyingPartySecurityErrorMessage[] = |
| "webauth: SecurityError: The relying party ID is not a registrable domain " |
| "suffix of, nor equal to the current domain."; |
| |
| constexpr char kAbortErrorMessage[] = |
| "webauth: AbortError: signal is aborted without reason"; |
| |
| constexpr char kAbortReasonMessage[] = "webauth: Error"; |
| |
| constexpr char kGetPermissionsPolicyMissingMessage[] = |
| "webauth: NotAllowedError: The 'publickey-credentials-get' feature is " |
| "not enabled in this document. Permissions Policy may be used to delegate " |
| "Web Authentication capabilities to cross-origin child frames."; |
| |
| constexpr char kCrossOriginAncestorMessage[] = |
| "webauth: NotAllowedError: The following credential operations can only " |
| "occur in a document which is same-origin with all of its ancestors: " |
| "storage/retrieval of 'PasswordCredential' and 'FederatedCredential', " |
| "storage of 'PublicKeyCredential'."; |
| |
| constexpr char kAllowCredentialsRangeErrorMessage[] = |
| "webauth: RangeError: The `allowCredentials` attribute exceeds the maximum " |
| "allowed size (64)."; |
| |
| constexpr char kExcludeCredentialsRangeErrorMessage[] = |
| "webauth: RangeError: The `excludeCredentials` attribute exceeds the " |
| "maximum allowed size (64)."; |
| |
| // Templates to be used with base::ReplaceStringPlaceholders. Can be |
| // modified to include up to 9 replacements. The default values for |
| // any additional replacements added should also be added to the |
| // CreateParameters struct. |
| constexpr char kCreatePublicKeyTemplate[] = |
| "navigator.credentials.create({ publicKey: {" |
| " challenge: new TextEncoder().encode('climb a mountain')," |
| " rp: { id: '$3', name: 'Acme'}," |
| " user: { " |
| " id: new TextEncoder().encode('1098237235409872')," |
| " name: 'avery.a.jones@example.com'," |
| " displayName: 'Avery A. Jones'}," |
| " pubKeyCredParams: [{ type: 'public-key', alg: '$4'}]," |
| " timeout: _timeout_," |
| " excludeCredentials: $7," |
| " authenticatorSelection: {" |
| " requireResidentKey: $1," |
| " userVerification: '$2'," |
| " authenticatorAttachment: '$5'," |
| " }," |
| " attestation: '$6'," |
| "}}).then(c => window.domAutomationController.send('webauth: OK')," |
| " e => window.domAutomationController.send(" |
| " 'webauth: ' + e.toString()));"; |
| |
| constexpr char kCreatePublicKeyWithAbortSignalTemplate[] = |
| "navigator.credentials.create({ publicKey: {" |
| " challenge: new TextEncoder().encode('climb a mountain')," |
| " rp: { id: '$3', name: 'Acme'}," |
| " user: { " |
| " id: new TextEncoder().encode('1098237235409872')," |
| " name: 'avery.a.jones@example.com'," |
| " displayName: 'Avery A. Jones'}," |
| " pubKeyCredParams: [{ type: 'public-key', alg: '$4'}]," |
| " timeout: _timeout_," |
| " excludeCredentials: $7," |
| " authenticatorSelection: {" |
| " requireResidentKey: $1," |
| " userVerification: '$2'," |
| " authenticatorAttachment: '$5'," |
| " }," |
| " attestation: '$6'," |
| "}, signal: _signal_}" |
| ").then(c => window.domAutomationController.send('webauth: OK')," |
| " e => window.domAutomationController.send(" |
| " 'webauth: ' + e.toString()));"; |
| |
| constexpr char kShortTimeout[] = "100"; |
| |
| // Default values for kCreatePublicKeyTemplate. |
| struct CreateParameters { |
| std::string rp_id = "acme.com"; |
| bool require_resident_key = false; |
| std::string user_verification = "preferred"; |
| std::string authenticator_attachment = "cross-platform"; |
| std::string algorithm_identifier = "-7"; |
| std::string attestation = "none"; |
| std::string exclude_credentials = "[]"; |
| std::string signal = ""; |
| std::string timeout = "1000"; |
| }; |
| |
| std::string BuildCreateCallWithParameters(const CreateParameters& parameters) { |
| std::vector<std::string> substitutions; |
| substitutions.push_back(parameters.require_resident_key ? "true" : "false"); |
| substitutions.push_back(parameters.user_verification); |
| substitutions.push_back(parameters.rp_id); |
| substitutions.push_back(parameters.algorithm_identifier); |
| substitutions.push_back(parameters.authenticator_attachment); |
| substitutions.push_back(parameters.attestation); |
| substitutions.push_back(parameters.exclude_credentials); |
| |
| std::string result; |
| if (parameters.signal.empty()) { |
| result = base::ReplaceStringPlaceholders(kCreatePublicKeyTemplate, |
| substitutions, nullptr); |
| } else { |
| result = base::ReplaceStringPlaceholders( |
| kCreatePublicKeyWithAbortSignalTemplate, substitutions, nullptr); |
| base::ReplaceFirstSubstringAfterOffset(&result, 0, "_signal_", |
| parameters.signal); |
| } |
| |
| base::ReplaceFirstSubstringAfterOffset(&result, 0, "_timeout_", |
| parameters.timeout); |
| return result; |
| } |
| |
| constexpr char kGetPublicKeyTemplate[] = |
| "navigator.credentials.get({ publicKey: {" |
| " challenge: new TextEncoder().encode('climb a mountain')," |
| " userVerification: '$1'," |
| " allowCredentials: $2," |
| " timeout: $3}" |
| "}).then(c => window.domAutomationController.send('webauth: OK')," |
| " e => window.domAutomationController.send(" |
| " 'webauth: ' + e.toString()));"; |
| |
| constexpr char kGetPublicKeyWithAbortSignalTemplate[] = |
| "navigator.credentials.get({ publicKey: {" |
| " challenge: new TextEncoder().encode('climb a mountain')," |
| " userVerification: '$1'," |
| " allowCredentials: $2," |
| " timeout: $3," |
| "}, signal: $4}" |
| ").catch(c => window.domAutomationController.send(" |
| " 'webauth: ' + c.toString()));"; |
| |
| // Default values for kGetPublicKeyTemplate. |
| struct GetParameters { |
| std::string user_verification = "preferred"; |
| std::string allow_credentials = |
| "[{type: 'public-key'," |
| " id: new TextEncoder().encode('allowedCredential')," |
| " transports: ['usb', 'nfc', 'ble']}]"; |
| std::string signal = ""; |
| std::string timeout = "1000"; |
| }; |
| |
| std::string BuildGetCallWithParameters(const GetParameters& parameters) { |
| std::vector<std::string> substitutions; |
| substitutions.push_back(parameters.user_verification); |
| substitutions.push_back(parameters.allow_credentials); |
| substitutions.push_back(parameters.timeout); |
| if (parameters.signal.empty()) { |
| return base::ReplaceStringPlaceholders(kGetPublicKeyTemplate, substitutions, |
| nullptr); |
| } |
| substitutions.push_back(parameters.signal); |
| return base::ReplaceStringPlaceholders(kGetPublicKeyWithAbortSignalTemplate, |
| substitutions, nullptr); |
| } |
| |
| // Helper class that executes the given |closure| the very last moment before |
| // the next navigation commits in a given WebContents. |
| class ClosureExecutorBeforeNavigationCommit |
| : public DidCommitNavigationInterceptor { |
| public: |
| ClosureExecutorBeforeNavigationCommit(WebContents* web_contents, |
| base::OnceClosure closure) |
| : DidCommitNavigationInterceptor(web_contents), |
| closure_(std::move(closure)) {} |
| |
| ClosureExecutorBeforeNavigationCommit( |
| const ClosureExecutorBeforeNavigationCommit&) = delete; |
| ClosureExecutorBeforeNavigationCommit& operator=( |
| const ClosureExecutorBeforeNavigationCommit&) = delete; |
| |
| ~ClosureExecutorBeforeNavigationCommit() override = default; |
| |
| protected: |
| bool WillProcessDidCommitNavigation( |
| RenderFrameHost* render_frame_host, |
| NavigationRequest* navigation_request, |
| mojom::DidCommitProvisionalLoadParamsPtr* params, |
| mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params) |
| override { |
| if (closure_) |
| std::move(closure_).Run(); |
| return true; |
| } |
| |
| private: |
| base::OnceClosure closure_; |
| }; |
| |
| // Cancels all navigations in a WebContents while in scope. |
| class ScopedNavigationCancellingThrottleInstaller : public WebContentsObserver { |
| public: |
| explicit ScopedNavigationCancellingThrottleInstaller( |
| WebContents* web_contents) |
| : WebContentsObserver(web_contents) {} |
| |
| ScopedNavigationCancellingThrottleInstaller( |
| const ScopedNavigationCancellingThrottleInstaller&) = delete; |
| ScopedNavigationCancellingThrottleInstaller& operator=( |
| const ScopedNavigationCancellingThrottleInstaller&) = delete; |
| |
| ~ScopedNavigationCancellingThrottleInstaller() override = default; |
| |
| protected: |
| class CancellingThrottle : public NavigationThrottle { |
| public: |
| explicit CancellingThrottle(NavigationHandle* handle) |
| : NavigationThrottle(handle) {} |
| |
| CancellingThrottle(const CancellingThrottle&) = delete; |
| CancellingThrottle& operator=(const CancellingThrottle&) = delete; |
| |
| ~CancellingThrottle() override = default; |
| |
| protected: |
| const char* GetNameForLogging() override { |
| return "ScopedNavigationCancellingThrottleInstaller::CancellingThrottle"; |
| } |
| |
| ThrottleCheckResult WillStartRequest() override { |
| return ThrottleCheckResult(CANCEL); |
| } |
| }; |
| |
| void DidStartNavigation(NavigationHandle* navigation_handle) override { |
| navigation_handle->RegisterThrottleForTesting( |
| std::make_unique<CancellingThrottle>(navigation_handle)); |
| } |
| }; |
| |
| struct WebAuthBrowserTestState { |
| // Called when the browser is asked to display an attestation prompt. There is |
| // no default so if no callback is installed then the test will crash. |
| base::OnceCallback<void(base::OnceCallback<void(bool)>)> |
| attestation_prompt_callback_; |
| |
| // Set when |IsFocused| is called. |
| bool focus_checked = false; |
| |
| // This is incremented when an |AuthenticatorRequestClientDelegate| is |
| // created. |
| int delegate_create_count = 0; |
| }; |
| |
| class WebAuthBrowserTestWebAuthenticationDelegate |
| : public WebAuthenticationDelegate { |
| public: |
| explicit WebAuthBrowserTestWebAuthenticationDelegate( |
| WebAuthBrowserTestState* test_state) |
| : test_state_(test_state) {} |
| |
| bool IsFocused(content::WebContents* web_contents) override { |
| test_state_->focus_checked = true; |
| return WebAuthenticationDelegate::IsFocused(web_contents); |
| } |
| |
| private: |
| const raw_ptr<WebAuthBrowserTestState> test_state_; |
| }; |
| |
| class WebAuthBrowserTestClientDelegate |
| : public AuthenticatorRequestClientDelegate { |
| public: |
| explicit WebAuthBrowserTestClientDelegate(WebAuthBrowserTestState* test_state) |
| : test_state_(test_state) {} |
| |
| WebAuthBrowserTestClientDelegate(const WebAuthBrowserTestClientDelegate&) = |
| delete; |
| WebAuthBrowserTestClientDelegate& operator=( |
| const WebAuthBrowserTestClientDelegate&) = delete; |
| |
| void ShouldReturnAttestation( |
| const std::string& relying_party_id, |
| const ::device::FidoAuthenticator* authenticator, |
| bool is_enterprise_attestation, |
| base::OnceCallback<void(bool)> callback) override { |
| std::move(test_state_->attestation_prompt_callback_) |
| .Run(std::move(callback)); |
| } |
| |
| private: |
| const raw_ptr<WebAuthBrowserTestState> test_state_; |
| }; |
| |
| // Implements ContentBrowserClient and allows webauthn-related calls to be |
| // mocked. |
| class WebAuthBrowserTestContentBrowserClient : public ContentBrowserClient { |
| public: |
| explicit WebAuthBrowserTestContentBrowserClient( |
| WebAuthBrowserTestState* test_state) |
| : test_state_(test_state) {} |
| |
| WebAuthBrowserTestContentBrowserClient( |
| const WebAuthBrowserTestContentBrowserClient&) = delete; |
| WebAuthBrowserTestContentBrowserClient& operator=( |
| const WebAuthBrowserTestContentBrowserClient&) = delete; |
| |
| WebAuthenticationDelegate* GetWebAuthenticationDelegate() override { |
| return &web_authentication_delegate_; |
| } |
| |
| std::unique_ptr<AuthenticatorRequestClientDelegate> |
| GetWebAuthenticationRequestDelegate( |
| RenderFrameHost* render_frame_host) override { |
| test_state_->delegate_create_count++; |
| return std::make_unique<WebAuthBrowserTestClientDelegate>(test_state_); |
| } |
| |
| private: |
| const raw_ptr<WebAuthBrowserTestState> test_state_; |
| WebAuthBrowserTestWebAuthenticationDelegate web_authentication_delegate_{ |
| test_state_}; |
| }; |
| |
| // Test fixture base class for common tasks. |
| class WebAuthBrowserTestBase : public content::ContentBrowserTest { |
| public: |
| WebAuthBrowserTestBase(const WebAuthBrowserTestBase&) = delete; |
| WebAuthBrowserTestBase& operator=(const WebAuthBrowserTestBase&) = delete; |
| |
| protected: |
| WebAuthBrowserTestBase() = default; |
| |
| void SetUpOnMainThread() override { |
| ContentBrowserTest::SetUpOnMainThread(); |
| mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK); |
| |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| https_server().ServeFilesFromSourceDirectory(GetTestDataFilePath()); |
| ASSERT_TRUE(https_server().Start()); |
| |
| test_client_ = |
| std::make_unique<WebAuthBrowserTestContentBrowserClient>(&test_state_); |
| old_client_ = SetBrowserClientForTesting(test_client_.get()); |
| |
| EXPECT_TRUE( |
| NavigateToURL(shell(), GetHttpsURL("www.acme.com", "/title1.html"))); |
| } |
| |
| void TearDown() override { |
| CHECK_EQ(SetBrowserClientForTesting(old_client_), test_client_.get()); |
| ContentBrowserTest::TearDown(); |
| } |
| |
| GURL GetHttpsURL(const std::string& hostname, |
| const std::string& relative_url) { |
| return https_server_.GetURL(hostname, relative_url); |
| } |
| |
| device::test::VirtualFidoDeviceFactory* InjectVirtualFidoDeviceFactory() { |
| auto owned_virtual_device_factory = |
| std::make_unique<device::test::VirtualFidoDeviceFactory>(); |
| auto* virtual_device_factory = owned_virtual_device_factory.get(); |
| AuthenticatorEnvironmentImpl::GetInstance() |
| ->ReplaceDefaultDiscoveryFactoryForTesting( |
| std::move(owned_virtual_device_factory)); |
| return virtual_device_factory; |
| } |
| |
| net::EmbeddedTestServer& https_server() { return https_server_; } |
| |
| WebAuthBrowserTestState* test_state() { return &test_state_; } |
| |
| private: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| ContentBrowserTest::SetUpCommandLine(command_line); |
| mock_cert_verifier_.SetUpCommandLine(command_line); |
| command_line->AppendSwitch( |
| switches::kEnableExperimentalWebPlatformFeatures); |
| } |
| |
| void SetUpInProcessBrowserTestFixture() override { |
| ContentBrowserTest::SetUpInProcessBrowserTestFixture(); |
| mock_cert_verifier_.SetUpInProcessBrowserTestFixture(); |
| } |
| |
| void TearDownInProcessBrowserTestFixture() override { |
| ContentBrowserTest::TearDownInProcessBrowserTestFixture(); |
| mock_cert_verifier_.TearDownInProcessBrowserTestFixture(); |
| } |
| |
| private: |
| content::ContentMockCertVerifier mock_cert_verifier_; |
| |
| net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS}; |
| std::unique_ptr<WebAuthBrowserTestContentBrowserClient> test_client_; |
| WebAuthBrowserTestState test_state_; |
| raw_ptr<ContentBrowserClient> old_client_ = nullptr; |
| }; |
| |
| // WebAuthLocalClientBrowserTest ---------------------------------------------- |
| |
| // Browser test fixture where the blink::mojom::Authenticator interface is |
| // accessed from a testing client in the browser process. |
| class WebAuthLocalClientBrowserTest : public WebAuthBrowserTestBase { |
| public: |
| WebAuthLocalClientBrowserTest() = default; |
| |
| WebAuthLocalClientBrowserTest(const WebAuthLocalClientBrowserTest&) = delete; |
| WebAuthLocalClientBrowserTest& operator=( |
| const WebAuthLocalClientBrowserTest&) = delete; |
| |
| ~WebAuthLocalClientBrowserTest() override = default; |
| |
| protected: |
| void SetUpOnMainThread() override { |
| WebAuthBrowserTestBase::SetUpOnMainThread(); |
| ConnectToAuthenticator(); |
| } |
| |
| void TearDownOnMainThread() override { |
| authenticator_remote_.reset(); |
| WebAuthBrowserTestBase::TearDownOnMainThread(); |
| } |
| |
| void ConnectToAuthenticator() { |
| if (authenticator_remote_.is_bound()) |
| authenticator_remote_.reset(); |
| static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()) |
| ->GetWebAuthenticationService( |
| authenticator_remote_.BindNewPipeAndPassReceiver()); |
| } |
| |
| blink::mojom::PublicKeyCredentialCreationOptionsPtr |
| BuildBasicCreateOptions() { |
| device::PublicKeyCredentialRpEntity rp("acme.com"); |
| rp.name = "acme.com"; |
| |
| std::vector<uint8_t> kTestUserId{0, 0, 0}; |
| device::PublicKeyCredentialUserEntity user(kTestUserId); |
| user.name = "name"; |
| user.display_name = "displayName"; |
| |
| static constexpr int32_t kCOSEAlgorithmIdentifierES256 = -7; |
| device::PublicKeyCredentialParams::CredentialInfo param; |
| param.type = device::CredentialType::kPublicKey; |
| param.algorithm = kCOSEAlgorithmIdentifierES256; |
| std::vector<device::PublicKeyCredentialParams::CredentialInfo> parameters; |
| parameters.push_back(param); |
| |
| std::vector<uint8_t> kTestChallenge{0, 0, 0}; |
| auto mojo_options = blink::mojom::PublicKeyCredentialCreationOptions::New(); |
| mojo_options->relying_party = rp; |
| mojo_options->user = user; |
| mojo_options->challenge = kTestChallenge; |
| mojo_options->public_key_parameters = parameters; |
| mojo_options->timeout = base::Seconds(30); |
| return mojo_options; |
| } |
| |
| blink::mojom::PublicKeyCredentialRequestOptionsPtr BuildBasicGetOptions() { |
| std::vector<device::PublicKeyCredentialDescriptor> credentials; |
| base::flat_set<device::FidoTransportProtocol> transports; |
| transports.emplace(device::FidoTransportProtocol::kUsbHumanInterfaceDevice); |
| |
| device::PublicKeyCredentialDescriptor descriptor( |
| device::CredentialType::kPublicKey, |
| device::fido_parsing_utils::Materialize( |
| device::test_data::kTestGetAssertionCredentialId), |
| transports); |
| credentials.push_back(descriptor); |
| |
| std::vector<uint8_t> kTestChallenge{0, 0, 0}; |
| auto mojo_options = blink::mojom::PublicKeyCredentialRequestOptions::New(); |
| mojo_options->challenge = kTestChallenge; |
| mojo_options->timeout = base::Seconds(30); |
| mojo_options->relying_party_id = "acme.com"; |
| mojo_options->allow_credentials = std::move(credentials); |
| mojo_options->user_verification = |
| device::UserVerificationRequirement::kPreferred; |
| return mojo_options; |
| } |
| |
| void WaitForConnectionError() { |
| ASSERT_TRUE(authenticator_remote_); |
| ASSERT_TRUE(authenticator_remote_.is_bound()); |
| if (!authenticator_remote_.is_connected()) |
| return; |
| |
| base::RunLoop run_loop; |
| authenticator_remote_.set_disconnect_handler(run_loop.QuitClosure()); |
| run_loop.Run(); |
| } |
| |
| blink::mojom::Authenticator* authenticator() { |
| return authenticator_remote_.get(); |
| } |
| |
| raw_ptr<device::test::FakeFidoDiscoveryFactory> discovery_factory_; |
| |
| private: |
| mojo::Remote<blink::mojom::Authenticator> authenticator_remote_; |
| }; |
| |
| // Tests that no crash occurs when the implementation is destroyed with a |
| // pending navigator.credentials.create({publicKey: ...}) call. |
| IN_PROC_BROWSER_TEST_F(WebAuthLocalClientBrowserTest, |
| CreatePublicKeyCredentialThenNavigateAway) { |
| device::test::VirtualFidoDeviceFactory* virtual_device_factory = |
| InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->mutable_state()->simulate_press_callback = |
| base::BindLambdaForTesting( |
| [&](device::VirtualFidoDevice* device) { return false; }); |
| |
| TestCreateCallbackReceiver create_callback_receiver; |
| authenticator()->MakeCredential(BuildBasicCreateOptions(), |
| create_callback_receiver.callback()); |
| ASSERT_FALSE(create_callback_receiver.was_called()); |
| EXPECT_TRUE( |
| NavigateToURL(shell(), GetHttpsURL("www.acme.com", "/title2.html"))); |
| WaitForConnectionError(); |
| |
| // The next active document should be able to successfully call |
| // navigator.credentials.create({publicKey: ...}) again. |
| ConnectToAuthenticator(); |
| InjectVirtualFidoDeviceFactory(); |
| TestCreateCallbackReceiver create_callback_receiver2; |
| authenticator()->MakeCredential(BuildBasicCreateOptions(), |
| create_callback_receiver2.callback()); |
| create_callback_receiver2.WaitForCallback(); |
| EXPECT_EQ(create_callback_receiver2.status(), AuthenticatorStatus::SUCCESS); |
| } |
| |
| // Tests that no crash occurs when the implementation is destroyed with a |
| // pending navigator.credentials.get({publicKey: ...}) call. |
| IN_PROC_BROWSER_TEST_F(WebAuthLocalClientBrowserTest, |
| GetPublicKeyCredentialThenNavigateAway) { |
| device::test::VirtualFidoDeviceFactory* virtual_device_factory = |
| InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->mutable_state()->simulate_press_callback = |
| base::BindLambdaForTesting( |
| [&](device::VirtualFidoDevice* device) { return false; }); |
| |
| TestGetCallbackReceiver get_callback_receiver; |
| authenticator()->GetAssertion(BuildBasicGetOptions(), |
| get_callback_receiver.callback()); |
| ASSERT_FALSE(get_callback_receiver.was_called()); |
| EXPECT_TRUE( |
| NavigateToURL(shell(), GetHttpsURL("www.acme.com", "/title2.html"))); |
| WaitForConnectionError(); |
| |
| // The next active document should be able to successfully call |
| // navigator.credentials.get({publicKey: ...}) again. |
| ConnectToAuthenticator(); |
| InjectVirtualFidoDeviceFactory(); |
| TestGetCallbackReceiver get_callback_receiver2; |
| authenticator()->GetAssertion(BuildBasicGetOptions(), |
| get_callback_receiver2.callback()); |
| get_callback_receiver2.WaitForCallback(); |
| EXPECT_EQ(get_callback_receiver2.status(), |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| |
| enum class AttestationCallbackBehavior { |
| IGNORE_CALLBACK, |
| BEFORE_NAVIGATION, |
| AFTER_NAVIGATION, |
| }; |
| |
| const char* AttestationCallbackBehaviorToString( |
| AttestationCallbackBehavior behavior) { |
| switch (behavior) { |
| case AttestationCallbackBehavior::IGNORE_CALLBACK: |
| return "IGNORE_CALLBACK"; |
| case AttestationCallbackBehavior::BEFORE_NAVIGATION: |
| return "BEFORE_NAVIGATION"; |
| case AttestationCallbackBehavior::AFTER_NAVIGATION: |
| return "AFTER_NAVIGATION"; |
| } |
| } |
| |
| const AttestationCallbackBehavior kAllAttestationCallbackBehaviors[] = { |
| AttestationCallbackBehavior::IGNORE_CALLBACK, |
| AttestationCallbackBehavior::BEFORE_NAVIGATION, |
| AttestationCallbackBehavior::AFTER_NAVIGATION, |
| }; |
| |
| // Tests navigating while an attestation permission prompt is showing. |
| IN_PROC_BROWSER_TEST_F(WebAuthLocalClientBrowserTest, |
| PromptForAttestationThenNavigateAway) { |
| for (auto behavior : kAllAttestationCallbackBehaviors) { |
| SCOPED_TRACE(AttestationCallbackBehaviorToString(behavior)); |
| |
| InjectVirtualFidoDeviceFactory(); |
| TestCreateCallbackReceiver create_callback_receiver; |
| auto options = BuildBasicCreateOptions(); |
| options->attestation = device::AttestationConveyancePreference::kDirect; |
| authenticator()->MakeCredential(std::move(options), |
| create_callback_receiver.callback()); |
| bool attestation_callback_was_invoked = false; |
| test_state()->attestation_prompt_callback_ = base::BindLambdaForTesting( |
| [&](base::OnceCallback<void(bool)> callback) { |
| attestation_callback_was_invoked = true; |
| |
| if (behavior == AttestationCallbackBehavior::BEFORE_NAVIGATION) { |
| std::move(callback).Run(false); |
| // Silence the erroneous bugprone-use-after-move. |
| callback = base::OnceCallback<void(bool)>(); |
| } |
| EXPECT_TRUE(NavigateToURL( |
| shell(), GetHttpsURL("www.acme.com", "/title2.html"))); |
| if (behavior == AttestationCallbackBehavior::AFTER_NAVIGATION) { |
| std::move(callback).Run(false); |
| } |
| }); |
| |
| WaitForConnectionError(); |
| ASSERT_TRUE(attestation_callback_was_invoked); |
| ConnectToAuthenticator(); |
| } |
| } |
| |
| // Tests that the blink::mojom::Authenticator connection is not closed on a |
| // cancelled navigation. |
| IN_PROC_BROWSER_TEST_F(WebAuthLocalClientBrowserTest, |
| CreatePublicKeyCredentialAfterCancelledNavigation) { |
| ScopedNavigationCancellingThrottleInstaller navigation_canceller( |
| shell()->web_contents()); |
| |
| // This navigation should be canceled and hence should not succeed. |
| EXPECT_FALSE( |
| NavigateToURL(shell(), GetHttpsURL("www.acme.com", "/title2.html"))); |
| |
| InjectVirtualFidoDeviceFactory(); |
| TestCreateCallbackReceiver create_callback_receiver; |
| authenticator()->MakeCredential(BuildBasicCreateOptions(), |
| create_callback_receiver.callback()); |
| create_callback_receiver.WaitForCallback(); |
| EXPECT_EQ(create_callback_receiver.status(), AuthenticatorStatus::SUCCESS); |
| } |
| |
| // Tests that a navigator.credentials.create({publicKey: ...}) issued at the |
| // moment just before a navigation commits is not serviced. |
| IN_PROC_BROWSER_TEST_F(WebAuthLocalClientBrowserTest, |
| CreatePublicKeyCredentialRacingWithNavigation) { |
| InjectVirtualFidoDeviceFactory(); |
| |
| TestCreateCallbackReceiver create_callback_receiver; |
| auto request_options = BuildBasicCreateOptions(); |
| |
| ClosureExecutorBeforeNavigationCommit executor( |
| shell()->web_contents(), base::BindLambdaForTesting([&]() { |
| authenticator()->MakeCredential(std::move(request_options), |
| create_callback_receiver.callback()); |
| })); |
| |
| EXPECT_TRUE( |
| NavigateToURL(shell(), GetHttpsURL("www.acme.com", "/title2.html"))); |
| WaitForConnectionError(); |
| |
| // The next active document should be able to successfully call |
| // navigator.credentials.create({publicKey: ...}) again. |
| ConnectToAuthenticator(); |
| TestCreateCallbackReceiver create_callback_receiver2; |
| authenticator()->MakeCredential(BuildBasicCreateOptions(), |
| create_callback_receiver2.callback()); |
| create_callback_receiver2.WaitForCallback(); |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, create_callback_receiver2.status()); |
| } |
| |
| // Regression test for https://crbug.com/818219. |
| IN_PROC_BROWSER_TEST_F(WebAuthLocalClientBrowserTest, |
| CreatePublicKeyCredentialTwiceInARow) { |
| device::test::VirtualFidoDeviceFactory* virtual_device_factory = |
| InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->mutable_state()->simulate_press_callback = |
| base::BindLambdaForTesting( |
| [&](device::VirtualFidoDevice* device) { return false; }); |
| |
| TestCreateCallbackReceiver callback_receiver_1; |
| TestCreateCallbackReceiver callback_receiver_2; |
| authenticator()->MakeCredential(BuildBasicCreateOptions(), |
| callback_receiver_1.callback()); |
| authenticator()->MakeCredential(BuildBasicCreateOptions(), |
| callback_receiver_2.callback()); |
| callback_receiver_2.WaitForCallback(); |
| |
| EXPECT_EQ(AuthenticatorStatus::PENDING_REQUEST, callback_receiver_2.status()); |
| EXPECT_FALSE(callback_receiver_1.was_called()); |
| } |
| |
| // Regression test for https://crbug.com/818219. |
| IN_PROC_BROWSER_TEST_F(WebAuthLocalClientBrowserTest, |
| GetPublicKeyCredentialTwiceInARow) { |
| device::test::VirtualFidoDeviceFactory* virtual_device_factory = |
| InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->mutable_state()->simulate_press_callback = |
| base::BindLambdaForTesting( |
| [&](device::VirtualFidoDevice* device) { return false; }); |
| |
| TestGetCallbackReceiver callback_receiver_1; |
| TestGetCallbackReceiver callback_receiver_2; |
| authenticator()->GetAssertion(BuildBasicGetOptions(), |
| callback_receiver_1.callback()); |
| authenticator()->GetAssertion(BuildBasicGetOptions(), |
| callback_receiver_2.callback()); |
| callback_receiver_2.WaitForCallback(); |
| |
| EXPECT_EQ(AuthenticatorStatus::PENDING_REQUEST, callback_receiver_2.status()); |
| EXPECT_FALSE(callback_receiver_1.was_called()); |
| } |
| |
| // WebAuthJavascriptClientBrowserTest ----------------------------------------- |
| |
| // Browser test fixture where the blink::mojom::Authenticator interface is |
| // normally accessed from Javascript in the renderer process. |
| class WebAuthJavascriptClientBrowserTest : public WebAuthBrowserTestBase { |
| public: |
| WebAuthJavascriptClientBrowserTest() = default; |
| |
| WebAuthJavascriptClientBrowserTest( |
| const WebAuthJavascriptClientBrowserTest&) = delete; |
| WebAuthJavascriptClientBrowserTest& operator=( |
| const WebAuthJavascriptClientBrowserTest&) = delete; |
| |
| ~WebAuthJavascriptClientBrowserTest() override = default; |
| }; |
| |
| constexpr device::ProtocolVersion kAllProtocols[] = { |
| device::ProtocolVersion::kCtap2, device::ProtocolVersion::kU2f}; |
| |
| // Tests that when navigator.credentials.create() is called with an invalid |
| // relying party id, we get a SecurityError. |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| CreatePublicKeyCredentialInvalidRp) { |
| CreateParameters parameters; |
| parameters.rp_id = "localhost"; |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| BuildCreateCallWithParameters(parameters), |
| EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| |
| ASSERT_EQ(kRelyingPartySecurityErrorMessage, |
| result.substr(0, strlen(kRelyingPartySecurityErrorMessage))); |
| } |
| |
| // Tests that when navigator.credentials.create() is called with a null |
| // relying party, we get a NotSupportedError. |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| CreatePublicKeyWithNullRp) { |
| CreateParameters parameters; |
| std::string script = BuildCreateCallWithParameters(parameters); |
| const char kExpectedSubstr[] = "{ id: 'acme.com', name: 'Acme'}"; |
| const std::string::size_type offset = script.find(kExpectedSubstr); |
| ASSERT_TRUE(offset != std::string::npos); |
| script.replace(offset, sizeof(kExpectedSubstr) - 1, "null"); |
| |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| script, EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| ASSERT_EQ(kPublicKeyErrorMessage, result); |
| } |
| |
| // Tests that when navigator.credentials.create() is called with user |
| // verification required, request times out. |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| CreatePublicKeyCredentialWithUserVerification) { |
| for (const auto protocol : kAllProtocols) { |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->SetSupportedProtocol(protocol); |
| |
| CreateParameters parameters; |
| parameters.user_verification = "required"; |
| parameters.timeout = kShortTimeout; |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| BuildCreateCallWithParameters(parameters), |
| EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| ASSERT_EQ(kNotAllowedErrorMessage, result); |
| } |
| } |
| |
| // Tests that when navigator.credentials.create() is called with resident key |
| // required, request times out. |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| CreatePublicKeyCredentialWithResidentKeyRequired) { |
| for (const auto protocol : kAllProtocols) { |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->SetSupportedProtocol(protocol); |
| |
| CreateParameters parameters; |
| parameters.require_resident_key = true; |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| BuildCreateCallWithParameters(parameters), |
| EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| |
| ASSERT_EQ(kResidentCredentialsErrorMessage, result); |
| } |
| } |
| |
| // Tests that when navigator.credentials.create() is called with an |
| // unsupported algorithm, request times out. |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| CreatePublicKeyCredentialAlgorithmNotSupported) { |
| for (const auto protocol : kAllProtocols) { |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->SetSupportedProtocol(protocol); |
| |
| CreateParameters parameters; |
| parameters.algorithm_identifier = "123"; |
| parameters.timeout = kShortTimeout; |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| BuildCreateCallWithParameters(parameters), |
| EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| |
| ASSERT_EQ(kNotAllowedErrorMessage, result); |
| } |
| } |
| |
| // Tests that when navigator.credentials.create() is called with a |
| // platform authenticator requested, request times out. |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| CreatePublicKeyCredentialPlatformAuthenticator) { |
| for (const auto protocol : kAllProtocols) { |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->SetSupportedProtocol(protocol); |
| |
| CreateParameters parameters; |
| parameters.authenticator_attachment = "platform"; |
| parameters.timeout = kShortTimeout; |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| BuildCreateCallWithParameters(parameters), |
| EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| |
| ASSERT_EQ(kNotAllowedErrorMessage, result); |
| } |
| } |
| |
| // Tests that when navigator.credentials.create() is called with abort |
| // signal's aborted flag not set, we get a SUCCESS. |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| CreatePublicKeyCredentialWithAbortNotSet) { |
| for (const auto protocol : kAllProtocols) { |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->SetSupportedProtocol(protocol); |
| |
| CreateParameters parameters; |
| parameters.signal = "authAbortSignal"; |
| std::string script = |
| "authAbortController = new AbortController();" |
| "authAbortSignal = authAbortController.signal;" + |
| BuildCreateCallWithParameters(parameters); |
| |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| script, EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| ASSERT_EQ(kOkMessage, result); |
| } |
| } |
| |
| // Tests that when navigator.credentials.create() is called with abort |
| // signal's aborted flag set before sending request, we get an AbortError. |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| CreatePublicKeyCredentialWithAbortSetBeforeCreate) { |
| CreateParameters parameters; |
| parameters.signal = "authAbortSignal"; |
| std::string script = |
| "authAbortController = new AbortController();" |
| "authAbortSignal = authAbortController.signal;" |
| "authAbortController.abort();" + |
| BuildCreateCallWithParameters(parameters); |
| |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| script, EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| ASSERT_EQ(kAbortErrorMessage, result.substr(0, strlen(kAbortErrorMessage))); |
| } |
| |
| // Tests that when navigator.credentials.create() is called with abort |
| // signal's aborted flag set with reason before sending request, |
| // we get an error from the reason. |
| IN_PROC_BROWSER_TEST_F( |
| WebAuthJavascriptClientBrowserTest, |
| CreatePublicKeyCredentialWithAbortSetWithReasonBeforeCreate) { |
| CreateParameters parameters; |
| parameters.signal = "authAbortSignal"; |
| std::string script = |
| "authAbortController = new AbortController();" |
| "authAbortSignal = authAbortController.signal;" |
| "authAbortController.abort('Error');" + |
| BuildCreateCallWithParameters(parameters); |
| |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| script, EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| ASSERT_EQ(kAbortReasonMessage, result.substr(0, strlen(kAbortReasonMessage))); |
| } |
| |
| // Tests that when navigator.credentials.create() is called with abort |
| // signal's aborted flag set after sending request, we get an AbortError. |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| CreatePublicKeyCredentialWithAbortSetAfterCreate) { |
| // This test sends the abort signal after making the WebAuthn call. However, |
| // the WebAuthn call could complete before the abort signal is sent, leading |
| // to a flakey test. Thus the |simulate_press_callback| is installed and |
| // always returns false, to ensure that the VirtualFidoDevice stalls the |
| // WebAuthn call and the abort signal will happen in time. |
| device::test::VirtualFidoDeviceFactory* virtual_device_factory = |
| InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->mutable_state()->simulate_press_callback = |
| base::BindRepeating( |
| [](device::VirtualFidoDevice*) -> bool { return false; }); |
| |
| CreateParameters parameters; |
| parameters.signal = "authAbortSignal"; |
| std::string script = |
| "authAbortController = new AbortController();" |
| "authAbortSignal = authAbortController.signal;" + |
| BuildCreateCallWithParameters(parameters) + |
| "authAbortController.abort();"; |
| |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| script, EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| ASSERT_EQ(kAbortErrorMessage, result.substr(0, strlen(kAbortErrorMessage))); |
| } |
| |
| // Tests that when navigator.credentials.create() is called with abort |
| // signal's aborted flag set with reason after sending request, we get an error |
| // from the reason. |
| IN_PROC_BROWSER_TEST_F( |
| WebAuthJavascriptClientBrowserTest, |
| CreatePublicKeyCredentialWithAbortSetWithReasonAfterCreate) { |
| // This test sends the abort signal after making the WebAuthn call. However, |
| // the WebAuthn call could complete before the abort signal is sent, leading |
| // to a flakey test. Thus the |simulate_press_callback| is installed and |
| // always returns false, to ensure that the VirtualFidoDevice stalls the |
| // WebAuthn call and the abort signal will happen in time. |
| device::test::VirtualFidoDeviceFactory* virtual_device_factory = |
| InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->mutable_state()->simulate_press_callback = |
| base::BindRepeating( |
| [](device::VirtualFidoDevice*) -> bool { return false; }); |
| |
| CreateParameters parameters; |
| parameters.signal = "authAbortSignal"; |
| std::string script = |
| "authAbortController = new AbortController();" |
| "authAbortSignal = authAbortController.signal;" + |
| BuildCreateCallWithParameters(parameters) + |
| "authAbortController.abort('Error');"; |
| |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| script, EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| ASSERT_EQ(kAbortReasonMessage, result.substr(0, strlen(kAbortReasonMessage))); |
| } |
| |
| // Tests that when navigator.credentials.get() is called with user verification |
| // required, we get an NotAllowedError because the virtual device isn't |
| // configured with UV and GetAssertionRequestHandler will return |
| // |kAuthenticatorMissingUserVerification| when such an authenticator is |
| // touched in that case. |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| GetPublicKeyCredentialUserVerification) { |
| for (const auto protocol : kAllProtocols) { |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->SetSupportedProtocol(protocol); |
| |
| GetParameters parameters; |
| parameters.user_verification = "required"; |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| BuildGetCallWithParameters(parameters), |
| EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| ASSERT_EQ(kNotAllowedErrorMessage, result); |
| } |
| } |
| |
| // Test that unknown transport types are ignored. |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| UnknownTransportType) { |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->SetSupportedProtocol(device::ProtocolVersion::kCtap2); |
| |
| GetParameters parameters; |
| parameters.allow_credentials = |
| "[{" |
| " type: 'public-key'," |
| " id: new TextEncoder().encode('allowedCredential')," |
| " transports: ['carrierpigeon']," |
| "}]"; |
| parameters.timeout = kShortTimeout; |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| BuildGetCallWithParameters(parameters), |
| EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| ASSERT_EQ(kNotAllowedErrorMessage, result); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, HybridRecognised) { |
| // Ensure that both "cable" and "hybrid" are recognised as the same transport. |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->SetTransport(device::FidoTransportProtocol::kHybrid); |
| virtual_device_factory->SetSupportedProtocol(device::ProtocolVersion::kCtap2); |
| static const uint8_t kCredentialId[] = {1}; |
| ASSERT_TRUE(virtual_device_factory->mutable_state()->InjectRegistration( |
| kCredentialId, "www.acme.com")); |
| |
| GetParameters parameters; |
| for (const char* const transport_str : {"hybrid", "cable", "usb"}) { |
| SCOPED_TRACE(transport_str); |
| const bool should_fail = (strcmp(transport_str, "usb") == 0); |
| |
| parameters.allow_credentials = |
| "[{" |
| " type: 'public-key'," |
| " id: new Uint8Array([1])," |
| " transports: ['" + |
| std::string(transport_str) + |
| "']," |
| "}]"; |
| if (should_fail) { |
| parameters.timeout = kShortTimeout; |
| } |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| BuildGetCallWithParameters(parameters), |
| EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| |
| if (should_fail) { |
| ASSERT_EQ(kNotAllowedErrorMessage, result); |
| } else { |
| ASSERT_EQ("webauth: OK", result); |
| } |
| } |
| } |
| |
| // Tests that when navigator.credentials.get() is called with an empty |
| // allowCredentials list, we get a NotSupportedError. |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| GetPublicKeyCredentialEmptyAllowCredentialsList) { |
| InjectVirtualFidoDeviceFactory(); |
| GetParameters parameters; |
| parameters.allow_credentials = "[]"; |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| BuildGetCallWithParameters(parameters), |
| EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| ASSERT_EQ(kResidentCredentialsErrorMessage, result); |
| } |
| |
| // Tests that when navigator.credentials.get() is called with abort |
| // signal's aborted flag not set, we get a NOT_ALLOWED_ERROR, because the |
| // virtual device does not have any registered credentials. |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| GetPublicKeyCredentialWithAbortNotSet) { |
| for (const auto protocol : kAllProtocols) { |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->SetSupportedProtocol(protocol); |
| |
| GetParameters parameters; |
| parameters.signal = "authAbortSignal"; |
| std::string script = |
| "authAbortController = new AbortController();" |
| "authAbortSignal = authAbortController.signal;" + |
| BuildGetCallWithParameters(parameters); |
| |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| script, EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| ASSERT_EQ(kNotAllowedErrorMessage, result); |
| } |
| } |
| |
| // Tests that when navigator.credentials.get() is called with abort |
| // signal's aborted flag set before sending request, we get an AbortError. |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| GetPublicKeyCredentialWithAbortSetBeforeGet) { |
| GetParameters parameters; |
| parameters.signal = "authAbortSignal"; |
| std::string script = |
| "authAbortController = new AbortController();" |
| "authAbortSignal = authAbortController.signal;" |
| "authAbortController.abort();" + |
| BuildGetCallWithParameters(parameters); |
| |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| script, EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| ASSERT_EQ(kAbortErrorMessage, result.substr(0, strlen(kAbortErrorMessage))); |
| } |
| |
| // Tests that when navigator.credentials.get() is called with abort |
| // signal's aborted flag set with reason before sending request, |
| // we get an error from the reason. |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| GetPublicKeyCredentialWithAbortSetWithReasonBeforeGet) { |
| GetParameters parameters; |
| parameters.signal = "authAbortSignal"; |
| std::string script = |
| "authAbortController = new AbortController();" |
| "authAbortSignal = authAbortController.signal;" |
| "authAbortController.abort('Error');" + |
| BuildGetCallWithParameters(parameters); |
| |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| script, EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| ASSERT_EQ(kAbortReasonMessage, result.substr(0, strlen(kAbortReasonMessage))); |
| } |
| |
| // Tests that when navigator.credentials.get() is called with abort |
| // signal's aborted flag set after sending request, we get an AbortError. |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| GetPublicKeyCredentialWithAbortSetAfterGet) { |
| // This test sends the abort signal after making the WebAuthn call. However, |
| // the WebAuthn call could complete before the abort signal is sent, leading |
| // to a flakey test. Thus the |simulate_press_callback| is installed and |
| // always returns false, to ensure that the VirtualFidoDevice stalls the |
| // WebAuthn call and the abort signal will happen in time. |
| device::test::VirtualFidoDeviceFactory* virtual_device_factory = |
| InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->mutable_state()->simulate_press_callback = |
| base::BindRepeating( |
| [](device::VirtualFidoDevice*) -> bool { return false; }); |
| |
| GetParameters parameters; |
| parameters.signal = "authAbortSignal"; |
| std::string script = |
| "authAbortController = new AbortController();" |
| "authAbortSignal = authAbortController.signal;" + |
| BuildGetCallWithParameters(parameters) + "authAbortController.abort();"; |
| |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| script, EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| ASSERT_EQ(kAbortErrorMessage, result.substr(0, strlen(kAbortErrorMessage))); |
| } |
| |
| // Tests that when navigator.credentials.get() is called with abort |
| // signal's aborted flag set with reason after sending request, |
| // we get an error from the reason. |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| GetPublicKeyCredentialWithAbortSetWithReasonAfterGet) { |
| // This test sends the abort signal after making the WebAuthn call. However, |
| // the WebAuthn call could complete before the abort signal is sent, leading |
| // to a flakey test. Thus the |simulate_press_callback| is installed and |
| // always returns false, to ensure that the VirtualFidoDevice stalls the |
| // WebAuthn call and the abort signal will happen in time. |
| device::test::VirtualFidoDeviceFactory* virtual_device_factory = |
| InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->mutable_state()->simulate_press_callback = |
| base::BindRepeating( |
| [](device::VirtualFidoDevice*) -> bool { return false; }); |
| |
| GetParameters parameters; |
| parameters.signal = "authAbortSignal"; |
| std::string script = |
| "authAbortController = new AbortController();" |
| "authAbortSignal = authAbortController.signal;" + |
| BuildGetCallWithParameters(parameters) + |
| "authAbortController.abort('Error');"; |
| |
| std::string result = EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| script, EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| ASSERT_EQ(kAbortReasonMessage, result.substr(0, strlen(kAbortReasonMessage))); |
| } |
| |
| // Executes Javascript in the given WebContents and waits until a string with |
| // the given prefix is received. It will ignore values other than strings, and |
| // strings without the given prefix. Since messages are broadcast to |
| // DOMMessageQueues, this allows other functions that depend on ExecuteScript |
| // (and thus trigger the broadcast of values) to run while this function is |
| // waiting for a specific result. |
| absl::optional<std::string> ExecuteScriptAndExtractPrefixedString( |
| WebContents* web_contents, |
| const std::string& script, |
| const std::string& result_prefix) { |
| DOMMessageQueue dom_message_queue(web_contents); |
| web_contents->GetPrimaryMainFrame()->ExecuteJavaScriptForTests( |
| base::UTF8ToUTF16(script), base::NullCallback()); |
| |
| for (;;) { |
| std::string json; |
| if (!dom_message_queue.WaitForMessage(&json)) { |
| return absl::nullopt; |
| } |
| |
| absl::optional<base::Value> result = |
| base::JSONReader::Read(json, base::JSON_ALLOW_TRAILING_COMMAS); |
| if (!result) { |
| return absl::nullopt; |
| } |
| |
| std::string str; |
| if (result->is_string()) |
| str = result->GetString(); |
| if (str.find(result_prefix) == 0) |
| return str; |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| RequestsFromIFrames) { |
| static constexpr char kOuterHost[] = "acme.com"; |
| static constexpr char kInnerHost[] = "notacme.com"; |
| EXPECT_TRUE(NavigateToURL(shell(), |
| GetHttpsURL(kOuterHost, "/page_with_iframe.html"))); |
| |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| static constexpr uint8_t kOuterCredentialID = 1; |
| static constexpr uint8_t kOuterCredentialIDArray[] = {kOuterCredentialID}; |
| static constexpr uint8_t kInnerCredentialID = 2; |
| static constexpr uint8_t kInnerCredentialIDArray[] = {kInnerCredentialID}; |
| ASSERT_TRUE(virtual_device_factory->mutable_state()->InjectRegistration( |
| kOuterCredentialIDArray, kOuterHost)); |
| ASSERT_TRUE(virtual_device_factory->mutable_state()->InjectRegistration( |
| kInnerCredentialIDArray, kInnerHost)); |
| |
| static constexpr struct kTestCase { |
| // Whether the iframe loads from a different origin. |
| bool cross_origin; |
| bool create_should_work; |
| bool get_should_work; |
| // The contents of an "allow" attribute on the iframe. |
| const char allow_value[32]; |
| } kTestCases[] = { |
| // XO |Create|Get | Allow |
| {false, true, true, ""}, |
| {true, false, false, ""}, |
| {true, false, true, "publickey-credentials-get"}, |
| }; |
| |
| for (const auto& test : kTestCases) { |
| SCOPED_TRACE(test.allow_value); |
| SCOPED_TRACE(test.cross_origin); |
| |
| const std::string setAllowJS = base::StringPrintf( |
| "document.getElementById('test_iframe').setAttribute('allow', '%s'); " |
| "'OK';", |
| test.allow_value); |
| ASSERT_EQ("OK", EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| setAllowJS.c_str())); |
| |
| if (test.cross_origin) { |
| // Create a cross-origin iframe by loading it from notacme.com. |
| NavigateIframeToURL(shell()->web_contents(), "test_iframe", |
| GetHttpsURL(kInnerHost, "/title2.html")); |
| } else { |
| // Create a same-origin iframe by loading it from acme.com. |
| NavigateIframeToURL(shell()->web_contents(), "test_iframe", |
| GetHttpsURL(kOuterHost, "/title2.html")); |
| } |
| |
| RenderFrameHost* const iframe = ChildFrameAt(shell()->web_contents(), 0); |
| ASSERT_TRUE(iframe); |
| |
| CreateParameters create_parameters; |
| create_parameters.rp_id = test.cross_origin ? "notacme.com" : "acme.com"; |
| std::string result = |
| EvalJs(iframe, BuildCreateCallWithParameters(create_parameters), |
| EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| if (test.create_should_work) { |
| EXPECT_EQ(std::string(kOkMessage), result); |
| } else { |
| EXPECT_EQ(kCrossOriginAncestorMessage, result); |
| } |
| |
| GetParameters get_params; |
| const int credential_id = |
| test.cross_origin ? kInnerCredentialID : kOuterCredentialID; |
| get_params.allow_credentials = base::StringPrintf( |
| "[{ type: 'public-key'," |
| " id: new Uint8Array([%d])," |
| "}]", |
| credential_id); |
| result = EvalJs(iframe, BuildGetCallWithParameters(get_params), |
| EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractString(); |
| if (test.get_should_work) { |
| EXPECT_EQ(std::string(kOkMessage), result); |
| } else { |
| EXPECT_EQ(kGetPermissionsPolicyMissingMessage, result); |
| } |
| } |
| } |
| |
| // Tests that a credentials.create() call triggered by the main frame will |
| // successfully complete even if a subframe navigation takes place while the |
| // request is waiting for user consent. |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| NavigateSubframeDuringPress) { |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| bool prompt_callback_was_invoked = false; |
| virtual_device_factory->mutable_state()->simulate_press_callback = |
| base::BindLambdaForTesting([&](device::VirtualFidoDevice* device) { |
| prompt_callback_was_invoked = true; |
| NavigateIframeToURL(shell()->web_contents(), "test_iframe", |
| GURL("/title2.html")); |
| return true; |
| }); |
| |
| EXPECT_TRUE(NavigateToURL( |
| shell(), GetHttpsURL("www.acme.com", "/page_with_iframe.html"))); |
| |
| // The plain ExecuteScriptAndExtractString cannot be used because |
| // NavigateIframeToURL uses it internally and they get confused about which |
| // message is for whom. |
| absl::optional<std::string> result = ExecuteScriptAndExtractPrefixedString( |
| shell()->web_contents(), |
| BuildCreateCallWithParameters(CreateParameters()), "webauth: "); |
| ASSERT_TRUE(result); |
| ASSERT_EQ(kOkMessage, *result); |
| ASSERT_TRUE(prompt_callback_was_invoked); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| NavigateSubframeDuringAttestationPrompt) { |
| InjectVirtualFidoDeviceFactory(); |
| |
| for (auto behavior : kAllAttestationCallbackBehaviors) { |
| if (behavior == AttestationCallbackBehavior::IGNORE_CALLBACK) { |
| // If the callback is ignored, then the registration will not complete and |
| // that hangs the test. |
| continue; |
| } |
| |
| SCOPED_TRACE(AttestationCallbackBehaviorToString(behavior)); |
| |
| bool prompt_callback_was_invoked = false; |
| test_state()->attestation_prompt_callback_ = base::BindOnce( |
| [](WebContents* web_contents, bool* prompt_callback_was_invoked, |
| AttestationCallbackBehavior behavior, |
| base::OnceCallback<void(bool)> callback) { |
| *prompt_callback_was_invoked = true; |
| |
| if (behavior == AttestationCallbackBehavior::BEFORE_NAVIGATION) { |
| std::move(callback).Run(true); |
| // Silence the erroneous bugprone-use-after-move. |
| callback = base::OnceCallback<void(bool)>(); |
| } |
| // Can't use NavigateIframeToURL here because in the |
| // BEFORE_NAVIGATION case we are racing AuthenticatorImpl and |
| // NavigateIframeToURL can get confused by the "OK" message. |
| absl::optional<std::string> result = |
| ExecuteScriptAndExtractPrefixedString( |
| web_contents, |
| "document.getElementById('test_iframe').src = " |
| "'/title2.html'; " |
| "window.domAutomationController.send('iframe: done');", |
| "iframe: "); |
| CHECK(result); |
| CHECK_EQ("iframe: done", *result); |
| if (behavior == AttestationCallbackBehavior::AFTER_NAVIGATION) { |
| std::move(callback).Run(true); |
| } |
| }, |
| shell()->web_contents(), &prompt_callback_was_invoked, behavior); |
| |
| EXPECT_TRUE(NavigateToURL( |
| shell(), GetHttpsURL("www.acme.com", "/page_with_iframe.html"))); |
| |
| CreateParameters parameters; |
| parameters.attestation = "direct"; |
| // The plain ExecuteScriptAndExtractString cannot be used because |
| // NavigateIframeToURL uses it internally and they get confused about which |
| // message is for whom. |
| absl::optional<std::string> result = ExecuteScriptAndExtractPrefixedString( |
| shell()->web_contents(), BuildCreateCallWithParameters(parameters), |
| "webauth: "); |
| ASSERT_TRUE(result); |
| ASSERT_EQ(kOkMessage, *result); |
| ASSERT_TRUE(prompt_callback_was_invoked); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| BadCableExtensionVersions) { |
| // The caBLE extension should only contain v1 data. Test that nothing crashes |
| // if a site tries to set other versions. |
| |
| InjectVirtualFidoDeviceFactory(); |
| GetParameters parameters; |
| parameters.allow_credentials = |
| "[{ type: 'public-key'," |
| " id: new TextEncoder().encode('allowedCredential')," |
| " transports: ['cable']}]," |
| "extensions: {" |
| " cableAuthentication: [{" |
| " version: 1," |
| " clientEid: new Uint8Array(Array(16).fill(1))," |
| " authenticatorEid: new Uint8Array(Array(16).fill(2))," |
| " sessionPreKey: new Uint8Array(Array(32).fill(3))," |
| " },{" |
| " version: 2," |
| " clientEid: new Uint8Array(Array(16).fill(1))," |
| " authenticatorEid: new Uint8Array(Array(16).fill(2))," |
| " sessionPreKey: new Uint8Array(Array(32).fill(3))," |
| " },{" |
| " version: 3," |
| " clientEid: new Uint8Array(Array(16).fill(1))," |
| " authenticatorEid: new Uint8Array(Array(16).fill(2))," |
| " sessionPreKey: new Uint8Array(Array(32).fill(3))," |
| " }]" |
| "}"; |
| ASSERT_EQ(kNotAllowedErrorMessage, |
| EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), |
| BuildGetCallWithParameters(parameters), |
| EXECUTE_SCRIPT_USE_MANUAL_REPLY)); |
| } |
| |
| // VerifyDevicePublicKeyOutput checks the result of a DPK-enabled operation. |
| // The given Javascript result string contains the comma-separated, base64- |
| // encoded values of: |
| // 1. The authenticator data. |
| // 2. The clientDataJSON |
| // 3. The DPK authenticator output. |
| // 4. The DPK signature. |
| void VerifyDevicePublicKeyOutput(const std::string& output) { |
| const std::vector<std::string> base64_parts = base::SplitString( |
| output, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); |
| ASSERT_EQ(base64_parts.size(), 4u); |
| std::vector<uint8_t> authenticator_data = |
| *base::Base64Decode(base64_parts[0]); |
| std::vector<uint8_t> client_data_json = *base::Base64Decode(base64_parts[1]); |
| std::vector<uint8_t> dpk_output_bytes = *base::Base64Decode(base64_parts[2]); |
| std::vector<uint8_t> dpk_sig = *base::Base64Decode(base64_parts[3]); |
| |
| const device::DevicePublicKeyOutput dpk_output = |
| *device::DevicePublicKeyOutput::FromExtension( |
| cbor::Value(dpk_output_bytes)); |
| const std::unique_ptr<device::PublicKey> dpk_key = |
| device::P256PublicKey::ExtractFromCOSEKey( |
| static_cast<int32_t>(device::CoseAlgorithmIdentifier::kEs256), |
| base::span<const uint8_t>(), dpk_output.dpk.GetMap()); |
| |
| crypto::SignatureVerifier verifier; |
| verifier.VerifyInit( |
| crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256, dpk_sig, |
| dpk_key->der_bytes.value()); |
| verifier.VerifyUpdate(authenticator_data); |
| verifier.VerifyUpdate(crypto::SHA256Hash(client_data_json)); |
| EXPECT_TRUE(verifier.VerifyFinal()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| DevicePublicKeyMakeCredential) { |
| device::VirtualCtap2Device::Config config; |
| config.device_public_key_support = true; |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->SetCtap2Config(config); |
| |
| // self-attestation is needed because, otherwise, zeroing the AAGUID |
| // invalidates the DPK signature. |
| virtual_device_factory->mutable_state()->self_attestation = true; |
| |
| EXPECT_TRUE( |
| NavigateToURL(shell(), GetHttpsURL("www.acme.com", "/title1.html"))); |
| |
| constexpr char kJavascript[] = |
| "navigator.credentials.create({ publicKey: {" |
| " challenge: new TextEncoder().encode('abcd')," |
| " rp: { id: 'www.acme.com', name: 'name' }," |
| " user: { id: new Uint8Array([0]), name: 'name', displayName: 'dn' }," |
| " pubKeyCredParams: [{ type: 'public-key', alg: '-7' }]," |
| " extensions: { devicePubKey: {} }," |
| "}}).then(c => {" |
| " const e = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)));" |
| " window.domAutomationController.send(" |
| " 'webauth: ' + e(c.response.getAuthenticatorData())" |
| " + ',' + e(c.response.clientDataJSON)" |
| " + ',' + e(c.getClientExtensionResults()" |
| " .devicePubKey" |
| " .authenticatorOutput)" |
| " + ',' + e(c.getClientExtensionResults()" |
| " .devicePubKey" |
| " .signature));" |
| "}, e => window.domAutomationController.send(" |
| " 'webauth: ' + e.toString()));"; |
| absl::optional<std::string> result = ExecuteScriptAndExtractPrefixedString( |
| shell()->web_contents(), kJavascript, "webauth: "); |
| ASSERT_TRUE(result); |
| VerifyDevicePublicKeyOutput(result->substr(9)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| DevicePublicKeyGetAssertion) { |
| device::VirtualCtap2Device::Config config; |
| config.device_public_key_support = true; |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->SetCtap2Config(config); |
| constexpr uint8_t kCredentialId[] = {1}; |
| ASSERT_TRUE(virtual_device_factory->mutable_state()->InjectRegistration( |
| kCredentialId, "www.acme.com")); |
| |
| EXPECT_TRUE( |
| NavigateToURL(shell(), GetHttpsURL("www.acme.com", "/title1.html"))); |
| |
| constexpr char kJavascript[] = |
| "navigator.credentials.get({ publicKey: {" |
| " challenge: new TextEncoder().encode('abcd')," |
| " userVerification: 'discouraged'," |
| " allowCredentials: [{type: 'public-key', id: new Uint8Array([1])}]," |
| " extensions: { devicePubKey: {} }," |
| "}}).then(c => {" |
| " const e = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)));" |
| " window.domAutomationController.send(" |
| " 'webauth: ' + e(c.response.authenticatorData)" |
| " + ',' + e(c.response.clientDataJSON)" |
| " + ',' + e(c.getClientExtensionResults()" |
| " .devicePubKey" |
| " .authenticatorOutput)" |
| " + ',' + e(c.getClientExtensionResults()" |
| " .devicePubKey" |
| " .signature));" |
| "}, e => window.domAutomationController.send(" |
| " 'webauth: ' + e.toString()));"; |
| absl::optional<std::string> result = ExecuteScriptAndExtractPrefixedString( |
| shell()->web_contents(), kJavascript, "webauth: "); |
| ASSERT_TRUE(result); |
| VerifyDevicePublicKeyOutput(result->substr(9)); |
| } |
| |
| #if BUILDFLAG(IS_WIN) |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, WinMakeCredential) { |
| EXPECT_TRUE( |
| NavigateToURL(shell(), GetHttpsURL("www.acme.com", "/title1.html"))); |
| |
| device::FakeWinWebAuthnApi fake_api; |
| fake_api.set_is_uvpaa(true); |
| fake_api.set_hresult(S_OK); |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->set_win_webauthn_api(&fake_api); |
| |
| absl::optional<std::string> result = ExecuteScriptAndExtractPrefixedString( |
| shell()->web_contents(), |
| BuildCreateCallWithParameters(CreateParameters()), "webauth: "); |
| ASSERT_TRUE(result); |
| ASSERT_EQ(kOkMessage, *result); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| WinMakeCredentialReturnCodeFailure) { |
| EXPECT_TRUE( |
| NavigateToURL(shell(), GetHttpsURL("www.acme.com", "/title1.html"))); |
| device::FakeWinWebAuthnApi fake_api; |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->set_win_webauthn_api(&fake_api); |
| |
| // Errors documented for WebAuthNGetErrorName() in <webauthn.h>. |
| const std::map<HRESULT, std::string> errors{ |
| // NTE_EXISTS is the error for using an authenticator that matches the |
| // exclude list, which should result in "InvalidStateError". |
| {NTE_EXISTS, kInvalidStateErrorMessage}, |
| // All other errors should yield "NotAllowedError". |
| {HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), kNotAllowedErrorMessage}, |
| {NTE_TOKEN_KEYSET_STORAGE_FULL, kNotAllowedErrorMessage}, |
| {NTE_TOKEN_KEYSET_STORAGE_FULL, kNotAllowedErrorMessage}, |
| {NTE_INVALID_PARAMETER, kNotAllowedErrorMessage}, |
| {NTE_DEVICE_NOT_FOUND, kNotAllowedErrorMessage}, |
| {NTE_NOT_FOUND, kNotAllowedErrorMessage}, |
| {HRESULT_FROM_WIN32(ERROR_CANCELLED), kNotAllowedErrorMessage}, |
| {NTE_USER_CANCELLED, kNotAllowedErrorMessage}, |
| {HRESULT_FROM_WIN32(ERROR_TIMEOUT), kNotAllowedErrorMessage}, |
| // Undocumented errors should default to NOT_ALLOWED_ERROR. |
| {ERROR_FILE_NOT_FOUND, kNotAllowedErrorMessage}, |
| }; |
| |
| for (const auto& error : errors) { |
| fake_api.set_hresult(error.first); |
| |
| absl::optional<std::string> result = ExecuteScriptAndExtractPrefixedString( |
| shell()->web_contents(), |
| BuildCreateCallWithParameters(CreateParameters()), "webauth: "); |
| EXPECT_TRUE(result); |
| EXPECT_EQ(*result, error.second); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, WinGetAssertion) { |
| EXPECT_TRUE( |
| NavigateToURL(shell(), GetHttpsURL("www.acme.com", "/title1.html"))); |
| |
| constexpr uint8_t credential_id[] = {'A', 'A', 'A'}; |
| |
| device::FakeWinWebAuthnApi fake_api; |
| fake_api.InjectNonDiscoverableCredential(credential_id, "www.acme.com"); |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->set_win_webauthn_api(&fake_api); |
| |
| GetParameters get_parameters; |
| get_parameters.allow_credentials = |
| "[{ type: 'public-key', id: new TextEncoder().encode('AAA')}]"; |
| |
| absl::optional<std::string> result = ExecuteScriptAndExtractPrefixedString( |
| shell()->web_contents(), BuildGetCallWithParameters(get_parameters), |
| "webauth: "); |
| ASSERT_TRUE(result); |
| ASSERT_EQ(kOkMessage, *result); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| WinGetAssertionReturnCodeFailure) { |
| EXPECT_TRUE( |
| NavigateToURL(shell(), GetHttpsURL("www.acme.com", "/title1.html"))); |
| device::FakeWinWebAuthnApi fake_api; |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->set_win_webauthn_api(&fake_api); |
| |
| // Errors documented for WebAuthNGetErrorName() in <webauthn.h>. |
| const std::set<HRESULT> errors{ |
| // NTE_EXISTS -- should not be returned for WebAuthNGetAssertion(). |
| HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), NTE_TOKEN_KEYSET_STORAGE_FULL, |
| NTE_TOKEN_KEYSET_STORAGE_FULL, NTE_INVALID_PARAMETER, |
| NTE_DEVICE_NOT_FOUND, NTE_NOT_FOUND, HRESULT_FROM_WIN32(ERROR_CANCELLED), |
| NTE_USER_CANCELLED, HRESULT_FROM_WIN32(ERROR_TIMEOUT), |
| // Other errors should also result in NOT_ALLOWED_ERROR. |
| ERROR_FILE_NOT_FOUND}; |
| |
| for (const auto& error : errors) { |
| fake_api.set_hresult(error); |
| |
| absl::optional<std::string> result = ExecuteScriptAndExtractPrefixedString( |
| shell()->web_contents(), BuildGetCallWithParameters(GetParameters()), |
| "webauth: "); |
| ASSERT_EQ(*result, kNotAllowedErrorMessage); |
| } |
| } |
| #endif |
| |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| GetAssertionOversizedAllowList) { |
| EXPECT_TRUE( |
| NavigateToURL(shell(), GetHttpsURL("www.acme.com", "/title1.html"))); |
| |
| GetParameters get_parameters; |
| get_parameters.allow_credentials = |
| "Array(65).fill({ type: 'public-key', id: new " |
| "TextEncoder().encode('A')})"; |
| |
| absl::optional<std::string> result = ExecuteScriptAndExtractPrefixedString( |
| shell()->web_contents(), BuildGetCallWithParameters(get_parameters), |
| "webauth: "); |
| ASSERT_TRUE(result); |
| ASSERT_EQ(kAllowCredentialsRangeErrorMessage, *result); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| MakeCredentialOversizedExcludeList) { |
| EXPECT_TRUE( |
| NavigateToURL(shell(), GetHttpsURL("www.acme.com", "/title1.html"))); |
| |
| CreateParameters parameters; |
| parameters.exclude_credentials = |
| "Array(65).fill({type: 'public-key', id: new TextEncoder().encode('A')})"; |
| |
| absl::optional<std::string> result = ExecuteScriptAndExtractPrefixedString( |
| shell()->web_contents(), BuildCreateCallWithParameters(parameters), |
| "webauth: "); |
| ASSERT_TRUE(result); |
| ASSERT_EQ(kExcludeCredentialsRangeErrorMessage, *result); |
| } |
| |
| // No `kAndroidAccessory` on Windows. |
| #if !BUILDFLAG(IS_WIN) |
| IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, |
| DuplicateTransportStrings) { |
| // By setting the transport to `kAndroidAccessory`, and having explicit |
| // transports in the getInfo of ['hybrid', 'internal'], this test confirms |
| // that 'hybrid' doesn't get included twice. Since `kAndroidAccessory` and |
| // `kHybrid` are different values that both happen to get converted to the |
| // string "hybrid", this requires deduplication. |
| device::test::VirtualFidoDeviceFactory* virtual_device_factory = |
| InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->SetTransport( |
| device::FidoTransportProtocol::kAndroidAccessory); |
| device::VirtualCtap2Device::Config config; |
| config.transports_in_get_info = {device::FidoTransportProtocol::kHybrid, |
| device::FidoTransportProtocol::kInternal}; |
| config.include_transports_in_attestation_certificate = false; |
| virtual_device_factory->SetCtap2Config(config); |
| |
| EXPECT_TRUE( |
| NavigateToURL(shell(), GetHttpsURL("www.acme.com", "/title1.html"))); |
| |
| CreateParameters parameters; |
| constexpr char kJavascript[] = |
| "navigator.credentials.create({ publicKey: {" |
| " challenge: new TextEncoder().encode('climb a mountain')," |
| " rp: { id: 'www.acme.com', name: 'name' }," |
| " user: { id: new Uint8Array([0]), name: 'name', displayName: 'dn' }," |
| " pubKeyCredParams: [{ type: 'public-key', alg: '-7'}]," |
| "}}).then(c => window.domAutomationController.send(" |
| " 'webauth: ' + c.response.getTransports())," |
| " e => window.domAutomationController.send(" |
| " 'webauth: ' + e.toString()));"; |
| absl::optional<std::string> result = ExecuteScriptAndExtractPrefixedString( |
| shell()->web_contents(), kJavascript, "webauth: "); |
| ASSERT_TRUE(result); |
| EXPECT_EQ(result, "webauth: hybrid,internal"); |
| } |
| #endif |
| |
| class WebAuthLocalClientBackForwardCacheBrowserTest |
| : public WebAuthLocalClientBrowserTest { |
| protected: |
| BackForwardCacheDisabledTester tester_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(WebAuthLocalClientBackForwardCacheBrowserTest, |
| WebAuthDisablesBackForwardCache) { |
| // Initialisation of the test should disable bfcache. |
| EXPECT_TRUE(tester_.IsDisabledForFrameWithReason( |
| shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()->GetID(), |
| shell()->web_contents()->GetPrimaryMainFrame()->GetRoutingID(), |
| BackForwardCacheDisable::DisabledReason( |
| BackForwardCacheDisable::DisabledReasonId::kWebAuthenticationAPI))); |
| } |
| |
| // WebAuthBrowserCtapTest ---------------------------------------------- |
| |
| class WebAuthBrowserCtapTest : public WebAuthLocalClientBrowserTest { |
| public: |
| WebAuthBrowserCtapTest() = default; |
| |
| WebAuthBrowserCtapTest(const WebAuthBrowserCtapTest&) = delete; |
| WebAuthBrowserCtapTest& operator=(const WebAuthBrowserCtapTest&) = delete; |
| |
| ~WebAuthBrowserCtapTest() override = default; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(WebAuthBrowserCtapTest, TestMakeCredential) { |
| for (const auto protocol : kAllProtocols) { |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->SetSupportedProtocol(protocol); |
| |
| TestCreateCallbackReceiver create_callback_receiver; |
| authenticator()->MakeCredential(BuildBasicCreateOptions(), |
| create_callback_receiver.callback()); |
| |
| create_callback_receiver.WaitForCallback(); |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, create_callback_receiver.status()); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebAuthBrowserCtapTest, |
| TestMakeCredentialWithDuplicateKeyHandle) { |
| for (const auto protocol : kAllProtocols) { |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->SetSupportedProtocol(protocol); |
| auto make_credential_request = BuildBasicCreateOptions(); |
| device::PublicKeyCredentialDescriptor excluded_credential( |
| device::CredentialType::kPublicKey, |
| device::fido_parsing_utils::Materialize( |
| device::test_data::kCtap2MakeCredentialCredentialId), |
| std::vector<device::FidoTransportProtocol>{ |
| device::FidoTransportProtocol::kUsbHumanInterfaceDevice}); |
| make_credential_request->exclude_credentials.push_back(excluded_credential); |
| |
| ASSERT_TRUE(virtual_device_factory->mutable_state()->InjectRegistration( |
| device::fido_parsing_utils::Materialize( |
| device::test_data::kCtap2MakeCredentialCredentialId), |
| make_credential_request->relying_party.id)); |
| |
| TestCreateCallbackReceiver create_callback_receiver; |
| authenticator()->MakeCredential(std::move(make_credential_request), |
| create_callback_receiver.callback()); |
| |
| create_callback_receiver.WaitForCallback(); |
| EXPECT_EQ(AuthenticatorStatus::CREDENTIAL_EXCLUDED, |
| create_callback_receiver.status()); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebAuthBrowserCtapTest, TestGetAssertion) { |
| for (const auto protocol : kAllProtocols) { |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->SetSupportedProtocol(protocol); |
| auto get_assertion_request_params = BuildBasicGetOptions(); |
| ASSERT_TRUE(virtual_device_factory->mutable_state()->InjectRegistration( |
| device::fido_parsing_utils::Materialize( |
| device::test_data::kTestGetAssertionCredentialId), |
| get_assertion_request_params->relying_party_id)); |
| |
| TestGetCallbackReceiver get_callback_receiver; |
| authenticator()->GetAssertion(std::move(get_assertion_request_params), |
| get_callback_receiver.callback()); |
| get_callback_receiver.WaitForCallback(); |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, get_callback_receiver.status()); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebAuthBrowserCtapTest, |
| TestGetAssertionWithNoMatchingKeyHandles) { |
| for (const auto protocol : kAllProtocols) { |
| auto* virtual_device_factory = InjectVirtualFidoDeviceFactory(); |
| virtual_device_factory->SetSupportedProtocol(protocol); |
| auto get_assertion_request_params = BuildBasicGetOptions(); |
| |
| TestGetCallbackReceiver get_callback_receiver; |
| authenticator()->GetAssertion(std::move(get_assertion_request_params), |
| get_callback_receiver.callback()); |
| get_callback_receiver.WaitForCallback(); |
| EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, |
| get_callback_receiver.status()); |
| } |
| } |
| |
| } // namespace |
| |
| } // namespace content |