blob: 7f8f2b4b7e5ec8057647e7f0e6827c8cd8027cba [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <vector>
#include "base/base_switches.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "content/browser/smart_card/mock_smart_card_context_factory.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/smart_card_delegate.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_content_browser_client.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 "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/default_handlers.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/device/public/mojom/smart_card.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/mojom/smart_card/smart_card.mojom.h"
using device::mojom::SmartCardContext;
using device::mojom::SmartCardError;
using device::mojom::SmartCardReaderStateFlags;
using device::mojom::SmartCardReaderStateOut;
using device::mojom::SmartCardReaderStateOutPtr;
using ::testing::_;
using testing::InSequence;
namespace content {
namespace {
class FakeSmartCardDelegate : public SmartCardDelegate {
public:
FakeSmartCardDelegate() = default;
// SmartCardDelegate overrides:
mojo::PendingRemote<device::mojom::SmartCardContextFactory>
GetSmartCardContextFactory(BrowserContext& browser_context) override;
bool SupportsReaderAddedRemovedNotifications() const override { return true; }
MockSmartCardContextFactory mock_context_factory;
};
class SmartCardTestContentBrowserClient
: public ContentBrowserTestContentBrowserClient {
public:
SmartCardTestContentBrowserClient();
SmartCardTestContentBrowserClient(SmartCardTestContentBrowserClient&) =
delete;
SmartCardTestContentBrowserClient& operator=(
SmartCardTestContentBrowserClient&) = delete;
~SmartCardTestContentBrowserClient() override;
void SetSmartCardDelegate(std::unique_ptr<SmartCardDelegate>);
// ContentBrowserClient:
SmartCardDelegate* GetSmartCardDelegate(
content::BrowserContext* browser_context) override;
bool ShouldUrlUseApplicationIsolationLevel(BrowserContext* browser_context,
const GURL& url) override;
absl::optional<blink::ParsedPermissionsPolicy>
GetPermissionsPolicyForIsolatedWebApp(
content::BrowserContext* browser_context,
const url::Origin& app_origin) override;
private:
std::unique_ptr<SmartCardDelegate> delegate_;
};
class SmartCardTest : public ContentBrowserTest {
public:
GURL GetIsolatedContextUrl() {
return https_server_.GetURL(
"a.com",
"/set-header?Cross-Origin-Opener-Policy: same-origin&"
"Cross-Origin-Embedder-Policy: require-corp&"
"Permissions-Policy: smart-card%3D(self)");
}
FakeSmartCardDelegate* CreateFakeSmartCardDelegate() {
auto unique_delegate = std::make_unique<FakeSmartCardDelegate>();
FakeSmartCardDelegate* delegate = unique_delegate.get();
test_client_->SetSmartCardDelegate(std::move(unique_delegate));
return delegate;
}
private:
void SetUpCommandLine(base::CommandLine* command_line) override {
ContentBrowserTest::SetUpCommandLine(command_line);
mock_cert_verifier_.SetUpCommandLine(command_line);
}
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
test_client_ = std::make_unique<SmartCardTestContentBrowserClient>();
mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
// Serve a.com (and any other domain).
host_resolver()->AddRule("*", "127.0.0.1");
// Add a handler for the "/set-header" page (among others)
https_server_.AddDefaultHandlers(GetTestDataFilePath());
ASSERT_TRUE(https_server_.Start());
}
void SetUpInProcessBrowserTestFixture() override {
ContentBrowserTest::SetUpInProcessBrowserTestFixture();
mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
}
void TearDownInProcessBrowserTestFixture() override {
ContentBrowserTest::TearDownInProcessBrowserTestFixture();
mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
}
void TearDown() override {
ASSERT_TRUE(https_server_.ShutdownAndWaitUntilComplete());
ContentBrowserTest::TearDown();
}
std::unique_ptr<SmartCardTestContentBrowserClient> test_client_;
// Need a mock CertVerifier for HTTPS connections to succeed with the test
// server.
ContentMockCertVerifier mock_cert_verifier_;
net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
base::test::ScopedFeatureList scoped_feature_list_{
blink::features::kSmartCard};
};
} // namespace
SmartCardTestContentBrowserClient::SmartCardTestContentBrowserClient() =
default;
SmartCardTestContentBrowserClient::~SmartCardTestContentBrowserClient() =
default;
SmartCardDelegate* SmartCardTestContentBrowserClient::GetSmartCardDelegate(
content::BrowserContext* browser_context) {
return delegate_.get();
}
void SmartCardTestContentBrowserClient::SetSmartCardDelegate(
std::unique_ptr<SmartCardDelegate> delegate) {
delegate_ = std::move(delegate);
}
bool SmartCardTestContentBrowserClient::ShouldUrlUseApplicationIsolationLevel(
BrowserContext* browser_context,
const GURL& url) {
return true;
}
absl::optional<blink::ParsedPermissionsPolicy>
SmartCardTestContentBrowserClient::GetPermissionsPolicyForIsolatedWebApp(
content::BrowserContext* browser_context,
const url::Origin& app_origin) {
blink::ParsedPermissionsPolicy out;
blink::ParsedPermissionsPolicyDeclaration decl(
blink::mojom::PermissionsPolicyFeature::kSmartCard,
/*allowed_origins=*/{},
/*self_if_matches=*/app_origin, /*matches_all_origins=*/false,
/*matches_opaque_src=*/false);
out.push_back(decl);
return out;
}
mojo::PendingRemote<device::mojom::SmartCardContextFactory>
FakeSmartCardDelegate::GetSmartCardContextFactory(
BrowserContext& browser_context) {
return mock_context_factory.GetRemote();
}
IN_PROC_BROWSER_TEST_F(SmartCardTest, GetReaders) {
FakeSmartCardDelegate* delegate = CreateFakeSmartCardDelegate();
MockSmartCardContextFactory& mock_context_factory =
delegate->mock_context_factory;
{
InSequence s;
// Request what readers are currently available.
EXPECT_CALL(mock_context_factory, ListReaders(_))
.WillOnce([](SmartCardContext::ListReadersCallback callback) {
std::vector<std::string> readers{"Fake Reader"};
auto result =
device::mojom::SmartCardListReadersResult::NewReaders(readers);
std::move(callback).Run(std::move(result));
});
// Request the state of each of those readers.
EXPECT_CALL(mock_context_factory, GetStatusChange(_, _, _))
.WillOnce(
[](base::TimeDelta timeout,
std::vector<device::mojom::SmartCardReaderStateInPtr> states_in,
SmartCardContext::GetStatusChangeCallback callback) {
ASSERT_EQ(states_in.size(), size_t(1));
ASSERT_EQ(states_in[0]->reader, "Fake Reader");
EXPECT_TRUE(states_in[0]->current_state->unaware);
EXPECT_FALSE(states_in[0]->current_state->ignore);
EXPECT_FALSE(states_in[0]->current_state->changed);
EXPECT_FALSE(states_in[0]->current_state->unknown);
EXPECT_FALSE(states_in[0]->current_state->unavailable);
EXPECT_FALSE(states_in[0]->current_state->empty);
EXPECT_FALSE(states_in[0]->current_state->present);
EXPECT_FALSE(states_in[0]->current_state->exclusive);
EXPECT_FALSE(states_in[0]->current_state->inuse);
EXPECT_FALSE(states_in[0]->current_state->mute);
EXPECT_FALSE(states_in[0]->current_state->unpowered);
auto state_flags = SmartCardReaderStateFlags::New();
state_flags->unaware = false;
state_flags->ignore = false;
state_flags->changed = false;
state_flags->unknown = false;
state_flags->unavailable = false;
state_flags->empty = true;
state_flags->present = false;
state_flags->exclusive = false;
state_flags->inuse = false;
state_flags->mute = false;
state_flags->unpowered = false;
std::vector<SmartCardReaderStateOutPtr> states_out;
states_out.push_back(SmartCardReaderStateOut::New(
"Fake Reader", std::move(state_flags),
std::vector<uint8_t>({1u, 2u, 3u, 4u})));
auto result =
device::mojom::SmartCardStatusChangeResult::NewReaderStates(
std::move(states_out));
std::move(callback).Run(std::move(result));
});
// Request to be notified of state changes on those readers and on the
// addition of a new reader.
EXPECT_CALL(mock_context_factory, GetStatusChange(_, _, _))
.WillOnce(
[](base::TimeDelta timeout,
std::vector<device::mojom::SmartCardReaderStateInPtr> states_in,
SmartCardContext::GetStatusChangeCallback callback) {
ASSERT_EQ(states_in.size(), size_t(2));
EXPECT_EQ(states_in[0]->reader, R"(\\?PnP?\Notification)");
EXPECT_FALSE(states_in[0]->current_state->unaware);
EXPECT_FALSE(states_in[0]->current_state->ignore);
EXPECT_FALSE(states_in[0]->current_state->changed);
EXPECT_FALSE(states_in[0]->current_state->unknown);
EXPECT_FALSE(states_in[0]->current_state->unavailable);
EXPECT_FALSE(states_in[0]->current_state->empty);
EXPECT_FALSE(states_in[0]->current_state->present);
EXPECT_FALSE(states_in[0]->current_state->exclusive);
EXPECT_FALSE(states_in[0]->current_state->inuse);
EXPECT_FALSE(states_in[0]->current_state->mute);
EXPECT_FALSE(states_in[0]->current_state->unpowered);
EXPECT_EQ(states_in[1]->reader, "Fake Reader");
EXPECT_FALSE(states_in[1]->current_state->unaware);
EXPECT_FALSE(states_in[1]->current_state->ignore);
EXPECT_FALSE(states_in[1]->current_state->changed);
EXPECT_FALSE(states_in[1]->current_state->unknown);
EXPECT_FALSE(states_in[1]->current_state->unavailable);
EXPECT_TRUE(states_in[1]->current_state->empty);
EXPECT_FALSE(states_in[1]->current_state->present);
EXPECT_FALSE(states_in[1]->current_state->exclusive);
EXPECT_FALSE(states_in[1]->current_state->inuse);
EXPECT_FALSE(states_in[1]->current_state->mute);
EXPECT_FALSE(states_in[1]->current_state->unpowered);
// Fail so that SmartCardReaderTracker stops requesting.
std::move(callback).Run(
device::mojom::SmartCardStatusChangeResult::NewError(
SmartCardError::kNoService));
});
}
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
EXPECT_EQ(true, EvalJs(shell(),
R"((async () => {
let readers = await navigator.smartCard.getReaders();
return readers.length == 1 && readers[0].name == "Fake Reader";
})())"));
}
IN_PROC_BROWSER_TEST_F(SmartCardTest, ReaderAdd) {
FakeSmartCardDelegate* delegate = CreateFakeSmartCardDelegate();
MockSmartCardContextFactory& mock_context_factory =
delegate->mock_context_factory;
base::test::TestFuture<SmartCardContext::GetStatusChangeCallback>
first_get_status_callback;
{
InSequence s;
// Request what readers are currently available.
// There are none.
EXPECT_CALL(mock_context_factory, ListReaders(_))
.WillOnce([](SmartCardContext::ListReadersCallback callback) {
auto result = device::mojom::SmartCardListReadersResult::NewReaders(
std::vector<std::string>());
std::move(callback).Run(std::move(result));
});
// Request to be notified of on the addition of a new reader.
// Don't respond immediately.
EXPECT_CALL(mock_context_factory, GetStatusChange(_, _, _))
.WillOnce(
[&first_get_status_callback](
base::TimeDelta timeout,
std::vector<device::mojom::SmartCardReaderStateInPtr> states_in,
SmartCardContext::GetStatusChangeCallback callback) {
ASSERT_EQ(states_in.size(), size_t(1));
EXPECT_EQ(states_in[0]->reader, R"(\\?PnP?\Notification)");
EXPECT_FALSE(states_in[0]->current_state->unaware);
EXPECT_FALSE(states_in[0]->current_state->ignore);
EXPECT_FALSE(states_in[0]->current_state->changed);
EXPECT_FALSE(states_in[0]->current_state->unknown);
EXPECT_FALSE(states_in[0]->current_state->unavailable);
EXPECT_FALSE(states_in[0]->current_state->empty);
EXPECT_FALSE(states_in[0]->current_state->present);
EXPECT_FALSE(states_in[0]->current_state->exclusive);
EXPECT_FALSE(states_in[0]->current_state->inuse);
EXPECT_FALSE(states_in[0]->current_state->mute);
EXPECT_FALSE(states_in[0]->current_state->unpowered);
first_get_status_callback.SetValue(std::move(callback));
});
// Once the previous GetStatusChange is answered, the SmartCardReaderTracker
// will start over from ListReader again.
// Fail so that it stops requesting.
EXPECT_CALL(mock_context_factory, ListReaders(_))
.WillOnce([](SmartCardContext::ListReadersCallback callback) {
auto result = device::mojom::SmartCardListReadersResult::NewError(
SmartCardError::kNoService);
std::move(callback).Run(std::move(result));
});
}
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
EXPECT_TRUE(ExecJs(shell(), R"((async () => {
let observer = await navigator.smartCard.watchForReaders();
window.promise = new Promise((resolve) => {
observer.addEventListener('readeradd', (e) => {
resolve(e.reader.name);
}, { once: true });
});
})())"));
// Now that a listener to 'readeradd' has been set, notify that a new reader
// was added.
{
auto state_flags = SmartCardReaderStateFlags::New();
state_flags->empty = true;
std::vector<SmartCardReaderStateOutPtr> states_out;
states_out.push_back(
SmartCardReaderStateOut::New("New Fake Reader", std::move(state_flags),
std::vector<uint8_t>({1u, 2u, 3u, 4u})));
auto result = device::mojom::SmartCardStatusChangeResult::NewReaderStates(
std::move(states_out));
first_get_status_callback.Take().Run(std::move(result));
}
EXPECT_EQ("New Fake Reader", EvalJs(shell(), "window.promise"));
}
IN_PROC_BROWSER_TEST_F(SmartCardTest, ReaderRemove) {
FakeSmartCardDelegate* delegate = CreateFakeSmartCardDelegate();
MockSmartCardContextFactory& mock_context_factory =
delegate->mock_context_factory;
base::test::TestFuture<SmartCardContext::GetStatusChangeCallback>
first_get_status_callback;
const std::string reader_name = "Fake Reader";
{
InSequence s;
// Request what readers are currently available.
// There is already one.
EXPECT_CALL(mock_context_factory, ListReaders(_))
.WillOnce([&reader_name](
SmartCardContext::ListReadersCallback callback) {
std::vector<std::string> readers{reader_name};
auto result =
device::mojom::SmartCardListReadersResult::NewReaders(readers);
std::move(callback).Run(std::move(result));
});
// Request the state of each of those readers.
EXPECT_CALL(mock_context_factory, GetStatusChange(_, _, _))
.WillOnce(
[&reader_name](
base::TimeDelta timeout,
std::vector<device::mojom::SmartCardReaderStateInPtr> states_in,
SmartCardContext::GetStatusChangeCallback callback) {
ASSERT_EQ(states_in.size(), size_t(1));
ASSERT_EQ(states_in[0]->reader, reader_name);
EXPECT_TRUE(states_in[0]->current_state->unaware);
EXPECT_FALSE(states_in[0]->current_state->ignore);
EXPECT_FALSE(states_in[0]->current_state->changed);
EXPECT_FALSE(states_in[0]->current_state->unknown);
EXPECT_FALSE(states_in[0]->current_state->unavailable);
EXPECT_FALSE(states_in[0]->current_state->empty);
EXPECT_FALSE(states_in[0]->current_state->present);
EXPECT_FALSE(states_in[0]->current_state->exclusive);
EXPECT_FALSE(states_in[0]->current_state->inuse);
EXPECT_FALSE(states_in[0]->current_state->mute);
EXPECT_FALSE(states_in[0]->current_state->unpowered);
auto state_flags = SmartCardReaderStateFlags::New();
state_flags->unaware = false;
state_flags->ignore = false;
state_flags->changed = false;
state_flags->unknown = false;
state_flags->unavailable = false;
state_flags->empty = true;
state_flags->present = false;
state_flags->exclusive = false;
state_flags->inuse = false;
state_flags->mute = false;
state_flags->unpowered = false;
std::vector<SmartCardReaderStateOutPtr> states_out;
states_out.push_back(SmartCardReaderStateOut::New(
reader_name, std::move(state_flags),
std::vector<uint8_t>({1u, 2u, 3u, 4u})));
auto result =
device::mojom::SmartCardStatusChangeResult::NewReaderStates(
std::move(states_out));
std::move(callback).Run(std::move(result));
});
// Request to be notified of on the addition of a new reader.
// Don't respond immediately.
EXPECT_CALL(mock_context_factory, GetStatusChange(_, _, _))
.WillOnce(
[&first_get_status_callback, &reader_name](
base::TimeDelta timeout,
std::vector<device::mojom::SmartCardReaderStateInPtr> states_in,
SmartCardContext::GetStatusChangeCallback callback) {
ASSERT_EQ(states_in.size(), size_t(2));
EXPECT_EQ(states_in[0]->reader, R"(\\?PnP?\Notification)");
EXPECT_FALSE(states_in[0]->current_state->unaware);
EXPECT_FALSE(states_in[0]->current_state->ignore);
EXPECT_FALSE(states_in[0]->current_state->changed);
EXPECT_FALSE(states_in[0]->current_state->unknown);
EXPECT_FALSE(states_in[0]->current_state->unavailable);
EXPECT_FALSE(states_in[0]->current_state->empty);
EXPECT_FALSE(states_in[0]->current_state->present);
EXPECT_FALSE(states_in[0]->current_state->exclusive);
EXPECT_FALSE(states_in[0]->current_state->inuse);
EXPECT_FALSE(states_in[0]->current_state->mute);
EXPECT_FALSE(states_in[0]->current_state->unpowered);
EXPECT_EQ(states_in[1]->reader, reader_name);
EXPECT_FALSE(states_in[1]->current_state->unaware);
EXPECT_FALSE(states_in[1]->current_state->ignore);
EXPECT_FALSE(states_in[1]->current_state->changed);
EXPECT_FALSE(states_in[1]->current_state->unknown);
EXPECT_FALSE(states_in[1]->current_state->unavailable);
EXPECT_TRUE(states_in[1]->current_state->empty);
EXPECT_FALSE(states_in[1]->current_state->present);
EXPECT_FALSE(states_in[1]->current_state->exclusive);
EXPECT_FALSE(states_in[1]->current_state->inuse);
EXPECT_FALSE(states_in[1]->current_state->mute);
EXPECT_FALSE(states_in[1]->current_state->unpowered);
// Don't respond immediately.
first_get_status_callback.SetValue(std::move(callback));
});
// Once the previous GetStatusChange is answered, the SmartCardReaderTracker
// will start over from ListReader again.
// Fail so that it stops requesting.
EXPECT_CALL(mock_context_factory, ListReaders(_))
.WillOnce([](SmartCardContext::ListReadersCallback callback) {
auto result = device::mojom::SmartCardListReadersResult::NewError(
SmartCardError::kNoService);
std::move(callback).Run(std::move(result));
});
}
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
EXPECT_TRUE(ExecJs(shell(), R"((async () => {
let observer = await navigator.smartCard.watchForReaders();
window.promise = new Promise((resolve) => {
observer.addEventListener('readerremove', (e) => {
resolve(e.reader.name);
}, { once: true });
});
})())"));
// Now that a listener to 'readerremove' has been set, notify about removal.
{
auto state_flags = SmartCardReaderStateFlags::New();
state_flags->unaware = false;
state_flags->ignore = true;
state_flags->changed = false;
state_flags->unknown = true;
state_flags->unavailable = false;
state_flags->empty = false;
state_flags->present = false;
state_flags->exclusive = false;
state_flags->inuse = false;
state_flags->mute = false;
state_flags->unpowered = false;
std::vector<SmartCardReaderStateOutPtr> states_out;
states_out.push_back(SmartCardReaderStateOut::New(
reader_name, std::move(state_flags), std::vector<uint8_t>()));
auto result = device::mojom::SmartCardStatusChangeResult::NewReaderStates(
std::move(states_out));
first_get_status_callback.Take().Run(std::move(result));
}
EXPECT_EQ(reader_name, EvalJs(shell(), "window.promise"));
}
IN_PROC_BROWSER_TEST_F(SmartCardTest, GetReadersFails) {
FakeSmartCardDelegate* delegate = CreateFakeSmartCardDelegate();
MockSmartCardContextFactory& mock_context_factory =
delegate->mock_context_factory;
EXPECT_CALL(mock_context_factory, ListReaders(_))
.WillOnce([](SmartCardContext::ListReadersCallback callback) {
std::move(callback).Run(
device::mojom::SmartCardListReadersResult::NewError(
SmartCardError::kNoService));
});
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
EXPECT_EQ("SmartCardError: no-service", EvalJs(shell(), R"(
(async () => {
try {
let readers = await navigator.smartCard.getReaders();
} catch (e) {
return `${e.name}: ${e.responseCode}`;
}
})()
)"));
}
} // namespace content