blob: 18f7cabfe0be1db5f6664e62c4e9e5e6b1b8dd9a [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/browser_context.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 "mojo/public/cpp/bindings/self_owned_receiver.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 base::test::TestFuture;
using device::mojom::SmartCardConnection;
using device::mojom::SmartCardContext;
using device::mojom::SmartCardDisposition;
using device::mojom::SmartCardError;
using device::mojom::SmartCardProtocol;
using device::mojom::SmartCardReaderStateFlags;
using device::mojom::SmartCardReaderStateOut;
using device::mojom::SmartCardReaderStateOutPtr;
using device::mojom::SmartCardResult;
using device::mojom::SmartCardShareMode;
using device::mojom::SmartCardSuccess;
using ::testing::_;
using testing::Exactly;
using testing::InSequence;
namespace content {
namespace {
class MockSmartCardConnection : public device::mojom::SmartCardConnection {
public:
MOCK_METHOD(void,
Disconnect,
(SmartCardDisposition disposition, DisconnectCallback callback),
(override));
MOCK_METHOD(void,
Transmit,
(device::mojom::SmartCardProtocol protocol,
const std::vector<uint8_t>& data,
TransmitCallback callback),
(override));
MOCK_METHOD(void,
Control,
(uint32_t control_code,
const std::vector<uint8_t>& data,
ControlCallback callback),
(override));
MOCK_METHOD(void,
GetAttrib,
(uint32_t id, GetAttribCallback callback),
(override));
MOCK_METHOD(void,
SetAttrib,
(uint32_t id,
const std::vector<uint8_t>& data,
SetAttribCallback callback),
(override));
MOCK_METHOD(void, Status, (StatusCallback callback), (override));
};
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& GetFakeSmartCardDelegate() {
return *static_cast<FakeSmartCardDelegate*>(
test_client_->GetSmartCardDelegate(nullptr));
}
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>();
test_client_->SetSmartCardDelegate(
std::make_unique<FakeSmartCardDelegate>());
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, Disconnect) {
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
MockSmartCardContextFactory& mock_context_factory =
GetFakeSmartCardDelegate().mock_context_factory;
MockSmartCardConnection mock_connection;
mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);
{
InSequence s;
mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
EXPECT_CALL(mock_connection, Disconnect(SmartCardDisposition::kEject, _))
.WillOnce([](SmartCardDisposition disposition,
SmartCardConnection::DisconnectCallback callback) {
std::move(callback).Run(
SmartCardResult::NewSuccess(SmartCardSuccess::kOk));
});
}
EXPECT_EQ(
"second disconnect: InvalidStateError, Failed to execute 'disconnect' on "
"'SmartCardConnection': Is disconnected.",
EvalJs(shell(), R"(
(async () => {
let context = await navigator.smartCard.establishContext();
let connection = await context.connect("Fake reader", "shared", ["t1"]);
await connection.disconnect("eject");
// A second attempt should fail.
try {
await connection.disconnect("unpower");
} catch (e) {
return `second disconnect: ${e.name}, ${e.message}`;
}
return `second disconnect did not throw`;
})())"));
}
IN_PROC_BROWSER_TEST_F(SmartCardTest, ConcurrentDisconnect) {
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
MockSmartCardContextFactory& mock_context_factory =
GetFakeSmartCardDelegate().mock_context_factory;
MockSmartCardConnection mock_connection;
mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);
TestFuture<SmartCardConnection::DisconnectCallback> disconnect_future;
{
InSequence s;
mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
EXPECT_CALL(mock_connection, Disconnect(SmartCardDisposition::kEject, _))
.WillOnce([&disconnect_future](
SmartCardDisposition disposition,
SmartCardConnection::DisconnectCallback callback) {
// Ensure this disconnect() call doesn't finish before the second
// one is issued.
disconnect_future.SetValue(std::move(callback));
});
}
EXPECT_EQ(
"second disconnect: InvalidStateError, Failed to execute 'disconnect' on "
"'SmartCardConnection': An operation is in progress.",
EvalJs(shell(), R"(
(async () => {
let context = await navigator.smartCard.establishContext();
let connection = await context.connect("Fake reader", "shared", ["t1"]);
// This first disconnect() call will go through but won't be finished
// before the end of this script.
connection.disconnect("eject");
// A second attempt should fail since the first one is still ongoing.
try {
await connection.disconnect("unpower");
} catch (e) {
return `second disconnect: ${e.name}, ${e.message}`;
}
return `second disconnect did not throw`;
})())"));
// Let the first disconnect() finish.
disconnect_future.Take().Run(
SmartCardResult::NewSuccess(SmartCardSuccess::kOk));
}
IN_PROC_BROWSER_TEST_F(SmartCardTest, Transmit) {
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
MockSmartCardContextFactory& mock_context_factory =
GetFakeSmartCardDelegate().mock_context_factory;
MockSmartCardConnection mock_connection;
mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);
{
InSequence s;
mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
EXPECT_CALL(mock_connection, Transmit(SmartCardProtocol::kT1, _, _))
.WillOnce([](SmartCardProtocol protocol,
const std::vector<uint8_t>& data,
SmartCardConnection::TransmitCallback callback) {
EXPECT_EQ(data, std::vector<uint8_t>({3u, 2u, 1u}));
std::move(callback).Run(
device::mojom::SmartCardDataResult::NewData({12u, 34u}));
});
}
EXPECT_EQ("response: 12,34", EvalJs(shell(), R"(
(async () => {
let context = await navigator.smartCard.establishContext();
let connection = await context.connect("Fake reader", "shared", ["t1"]);
let apdu = new Uint8Array([0x03, 0x02, 0x01]);
let response = await connection.transmit(apdu);
let responseString = new Uint8Array(response).toString();
return `response: ${responseString}`;
})())"));
}
IN_PROC_BROWSER_TEST_F(SmartCardTest, Control) {
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
MockSmartCardContextFactory& mock_context_factory =
GetFakeSmartCardDelegate().mock_context_factory;
MockSmartCardConnection mock_connection;
mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);
{
InSequence s;
mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
EXPECT_CALL(mock_connection, Control(42, _, _))
.WillOnce([](uint32_t control_code, const std::vector<uint8_t>& data,
SmartCardConnection::ControlCallback callback) {
EXPECT_EQ(data, std::vector<uint8_t>({3u, 2u, 1u}));
std::move(callback).Run(
device::mojom::SmartCardDataResult::NewData({12u, 34u}));
});
}
EXPECT_EQ("response: 12,34", EvalJs(shell(), R"(
(async () => {
let context = await navigator.smartCard.establishContext();
let connection = await context.connect("Fake reader", "shared", ["t1"]);
let data = new Uint8Array([0x03, 0x02, 0x01]);
let response = await connection.control(42, data);
let responseString = new Uint8Array(response).toString();
return `response: ${responseString}`;
})())"));
}
IN_PROC_BROWSER_TEST_F(SmartCardTest, GetAttribute) {
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
MockSmartCardContextFactory& mock_context_factory =
GetFakeSmartCardDelegate().mock_context_factory;
MockSmartCardConnection mock_connection;
mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);
{
InSequence s;
mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
EXPECT_CALL(mock_connection, GetAttrib(42, _))
.WillOnce(
[](uint32_t tag, SmartCardConnection::GetAttribCallback callback) {
std::move(callback).Run(
device::mojom::SmartCardDataResult::NewData({12u, 34u}));
});
}
EXPECT_EQ("response: 12,34", EvalJs(shell(), R"(
(async () => {
let context = await navigator.smartCard.establishContext();
let connection = await context.connect("Fake reader", "shared", ["t1"]);
let response = await connection.getAttribute(42);
let responseString = new Uint8Array(response).toString();
return `response: ${responseString}`;
})())"));
}
IN_PROC_BROWSER_TEST_F(SmartCardTest, ListReaders) {
MockSmartCardContextFactory& mock_context_factory =
GetFakeSmartCardDelegate().mock_context_factory;
EXPECT_CALL(mock_context_factory, ListReaders(_))
.WillOnce([](SmartCardContext::ListReadersCallback callback) {
std::vector<std::string> readers{"Foo", "Bar"};
auto result =
device::mojom::SmartCardListReadersResult::NewReaders(readers);
std::move(callback).Run(std::move(result));
});
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
auto expected_reader_names =
base::Value(base::Value::List().Append("Foo").Append("Bar"));
EXPECT_EQ(expected_reader_names, EvalJs(shell(), R"((async () => {
let context = await navigator.smartCard.establishContext();
return await context.listReaders();
})())"));
}
/*
This test checks that in case there are no readers available, listReaders() call
will return an empty list of readers with no errors.
Note that internally we will receive a kNoReadersAvailable error from
SmartCardDelegate. However, we should not forward this error to Javascript.
*/
IN_PROC_BROWSER_TEST_F(SmartCardTest, ListReadersEmpty) {
MockSmartCardContextFactory& mock_context_factory =
GetFakeSmartCardDelegate().mock_context_factory;
EXPECT_CALL(mock_context_factory, ListReaders(_))
.WillOnce([](SmartCardContext::ListReadersCallback callback) {
auto result = device::mojom::SmartCardListReadersResult::NewError(
SmartCardError::kNoReadersAvailable);
std::move(callback).Run(std::move(result));
});
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
auto expected_reader_names = base::Value(base::Value::List());
EXPECT_EQ(expected_reader_names, EvalJs(shell(), R"((async () => {
let context = await navigator.smartCard.establishContext();
return await context.listReaders();
})())"));
}
IN_PROC_BROWSER_TEST_F(SmartCardTest, GetStatusChange) {
MockSmartCardContextFactory& mock_context_factory =
GetFakeSmartCardDelegate().mock_context_factory;
EXPECT_CALL(mock_context_factory,
GetStatusChange(base::TimeDelta::Max(), _, _))
.WillOnce(
[](base::TimeDelta timeout,
std::vector<device::mojom::SmartCardReaderStateInPtr> states_in,
SmartCardContext::GetStatusChangeCallback callback) {
ASSERT_EQ(states_in.size(), 1u);
ASSERT_EQ(states_in[0]->reader, "Fake Reader");
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_TRUE(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 = false;
state_flags->present = true;
state_flags->exclusive = false;
state_flags->inuse = true;
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));
});
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
EXPECT_EQ(
"Fake Reader, {unaware=false, ignore=false, changed=false, "
"unknown=false, unavailable=false, empty=false, present=true, "
"exclusive=false, inuse=true, mute=false, unpowered=false}, {1,2,3,4}",
EvalJs(shell(), R"((async () => {
let context = await navigator.smartCard.establishContext();
let readerStates = [{readerName: "Fake Reader",
currentState: {empty: true}}];
let statesOut = await context.getStatusChange(
readerStates,
AbortSignal.timeout(4321));
if (statesOut.length !== 1) {
return `states array has size ${statesOut.length}`;
}
let atrString = new Uint8Array(statesOut[0].answerToReset).toString();
let flags = statesOut[0].eventState;
let eventStateString = `unaware=${flags.unaware}`
+ `, ignore=${flags.ignore}`
+ `, changed=${flags.changed}`
+ `, unknown=${flags.unknown}`
+ `, unavailable=${flags.unavailable}`
+ `, empty=${flags.empty}`
+ `, present=${flags.present}`
+ `, exclusive=${flags.exclusive}`
+ `, inuse=${flags.inuse}`
+ `, mute=${flags.mute}`
+ `, unpowered=${flags.unpowered}`;
return `${statesOut[0].readerName}, {${eventStateString}}` +
`, {${atrString}}`;
})())"));
}
IN_PROC_BROWSER_TEST_F(SmartCardTest, GetStatusChangeAborted) {
MockSmartCardContextFactory& mock_context_factory =
GetFakeSmartCardDelegate().mock_context_factory;
base::test::TestFuture<SmartCardContext::GetStatusChangeCallback>
get_status_callback;
{
InSequence s;
EXPECT_CALL(mock_context_factory,
GetStatusChange(base::TimeDelta::Max(), _, _))
.WillOnce(
[&get_status_callback](
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_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_TRUE(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);
// Don't respond immediately.
get_status_callback.SetValue(std::move(callback));
});
// Aborting a blink context.getStatusChange() call means sending a Cancel()
// request down to device.mojom.
EXPECT_CALL(mock_context_factory, Cancel(_))
.WillOnce(
[&get_status_callback](SmartCardContext::CancelCallback callback) {
std::move(get_status_callback)
.Take()
.Run(device::mojom::SmartCardStatusChangeResult::NewError(
SmartCardError::kCancelled));
std::move(callback).Run(
SmartCardResult::NewSuccess(SmartCardSuccess::kOk));
});
}
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
EXPECT_EQ("Exception: AbortError", EvalJs(shell(), R"((async () => {
let context = await navigator.smartCard.establishContext();
let abortController = new AbortController();
let getStatusPromise = context.getStatusChange(
[{readerName: "Fake Reader", currentState: {empty: true}}],
abortController.signal);
abortController.abort();
try {
let result = await getStatusPromise;
return "Success";
} catch (e) {
return `Exception: ${e.name}`;
}
})())"));
}
IN_PROC_BROWSER_TEST_F(SmartCardTest, Connect) {
MockSmartCardContextFactory& mock_context_factory =
GetFakeSmartCardDelegate().mock_context_factory;
EXPECT_CALL(mock_context_factory,
Connect("Fake reader", SmartCardShareMode::kShared, _, _))
.WillOnce([](const std::string& reader,
device::mojom::SmartCardShareMode share_mode,
device::mojom::SmartCardProtocolsPtr preferred_protocols,
SmartCardContext::ConnectCallback callback) {
mojo::PendingRemote<device::mojom::SmartCardConnection> pending_remote;
EXPECT_TRUE(preferred_protocols->t0);
EXPECT_TRUE(preferred_protocols->t1);
EXPECT_FALSE(preferred_protocols->raw);
mojo::MakeSelfOwnedReceiver(
std::make_unique<MockSmartCardConnection>(),
pending_remote.InitWithNewPipeAndPassReceiver());
auto success = device::mojom::SmartCardConnectSuccess::New(
std::move(pending_remote), SmartCardProtocol::kT1);
std::move(callback).Run(
device::mojom::SmartCardConnectResult::NewSuccess(
std::move(success)));
});
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
auto expected_reader_names =
base::Value(base::Value::List().Append("Foo").Append("Bar"));
EXPECT_EQ("[object SmartCardConnection]", EvalJs(shell(), R"(
(async () => {
let context = await navigator.smartCard.establishContext();
let connection = await context.connect("Fake reader", "shared",
["t0", "t1"]);
return `${connection}`;
})())"));
}
} // namespace content