Commit f856c63c authored by Jun Choi's avatar Jun Choi Committed by Commit Bot

Implement KeepAlive logic for CTAP HID device

CTAP HID transport specification adds KEEP_ALIVE command. Add logic for
handling keep alive messages. That is, message from HID device should be
re-read after 100 milliseconds.

Change-Id: Iecb002f141d1a57f5753d25d728a5f1d851323db
Reviewed-on: https://chromium-review.googlesource.com/947193
Commit-Queue: Jun Choi <hongjunchoi@chromium.org>
Reviewed-by: default avatarJan Wilken Dörrie <jdoerrie@chromium.org>
Cr-Commit-Position: refs/heads/master@{#548626}
parent f1ef9d9b
......@@ -6,10 +6,19 @@
#include <utility>
#include "base/optional.h"
#include "device/fido/u2f_parsing_utils.h"
namespace device {
namespace {
MATCHER_P(IsCtapHidCommand, expected_command, "") {
return arg.size() >= 5 &&
arg[4] == (0x80 | static_cast<uint8_t>(expected_command));
}
} // namespace
MockHidConnection::MockHidConnection(
device::mojom::HidDeviceInfoPtr device,
device::mojom::HidConnectionRequest request,
......@@ -45,6 +54,31 @@ void MockHidConnection::SetNonce(base::span<uint8_t const> nonce) {
nonce_ = std::vector<uint8_t>(nonce.begin(), nonce.end());
}
void MockHidConnection::ExpectWriteHidInit() {
EXPECT_CALL(*this, WritePtr(::testing::_,
IsCtapHidCommand(FidoHidDeviceCommand::kInit),
::testing::_))
.WillOnce(::testing::Invoke(
[&](auto&&, const std::vector<uint8_t>& buffer,
device::mojom::HidConnection::WriteCallback* cb) {
ASSERT_EQ(64u, buffer.size());
// First 7 bytes are 4 bytes of channel id, one byte representing
// HID command, 2 bytes for payload length.
SetNonce(base::make_span(buffer).subspan(7, 8));
std::move(*cb).Run(true);
}));
}
void MockHidConnection::ExpectHidWriteWithCommand(FidoHidDeviceCommand cmd) {
EXPECT_CALL(*this,
WritePtr(::testing::_, IsCtapHidCommand(cmd), ::testing::_))
.WillOnce(::testing::Invoke(
[&](auto&&, const std::vector<uint8_t>& buffer,
device::mojom::HidConnection::WriteCallback* cb) {
std::move(*cb).Run(true);
}));
}
bool FakeHidConnection::mock_connection_error_ = false;
FakeHidConnection::FakeHidConnection(device::mojom::HidDeviceInfoPtr device)
......
......@@ -13,6 +13,7 @@
#include "base/containers/span.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "device/fido/fido_constants.h"
#include "mojo/public/cpp/bindings/binding_set.h"
#include "mojo/public/cpp/bindings/interface_ptr_set.h"
#include "mojo/public/cpp/bindings/strong_binding.h"
......@@ -47,6 +48,9 @@ class MockHidConnection : public device::mojom::HidConnection {
SendFeatureReportCallback callback) override;
void SetNonce(base::span<uint8_t const> nonce);
void ExpectWriteHidInit();
void ExpectHidWriteWithCommand(FidoHidDeviceCommand cmd);
const std::vector<uint8_t>& connection_channel_id() const {
return connection_channel_id_;
}
......
......@@ -69,7 +69,6 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoBleDevice : public FidoDevice {
void StopTimeout();
void OnTimeout();
State state_ = State::kInit;
base::OneShotTimer timer_;
std::unique_ptr<FidoBleConnection> connection_;
......
......@@ -51,6 +51,8 @@ const std::array<uint8_t, 6> kU2fVersionResponse = {'U', '2', 'F',
'_', 'V', '2'};
const base::TimeDelta kDeviceTimeout = base::TimeDelta::FromSeconds(3);
const base::TimeDelta kHidKeepAliveDelay =
base::TimeDelta::FromMilliseconds(100);
const char kFormatKey[] = "fmt";
const char kAttestationStatementKey[] = "attStmt";
......
......@@ -260,6 +260,11 @@ extern const std::array<uint8_t, 6> kU2fVersionResponse;
// Maximum wait time before client error outs on device.
COMPONENT_EXPORT(DEVICE_FIDO) extern const base::TimeDelta kDeviceTimeout;
// Interval wait time before retrying reading on HID connection when
// CTAPHID_KEEPALIVE message has been received.
// https://fidoalliance.org/specs/fido-v2.0-rd-20170927/fido-client-to-authenticator-protocol-v2.0-rd-20170927.html#ctaphid_keepalive-0x3b
COMPONENT_EXPORT(DEVICE_FIDO) extern const base::TimeDelta kHidKeepAliveDelay;
// String key values for attestation object as a response to MakeCredential
// request.
COMPONENT_EXPORT(DEVICE_FIDO) extern const char kFormatKey[];
......
......@@ -7,6 +7,7 @@
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/command_line.h"
#include "base/logging.h"
#include "base/threading/thread_task_runner_handle.h"
#include "crypto/random.h"
#include "device/fido/fido_hid_message.h"
......@@ -26,7 +27,6 @@ static constexpr uint8_t kReportId = 0x00;
FidoHidDevice::FidoHidDevice(device::mojom::HidDeviceInfoPtr device_info,
device::mojom::HidManager* hid_manager)
: FidoDevice(),
state_(State::kInit),
hid_manager_(hid_manager),
device_info_(std::move(device_info)),
weak_factory_(this) {}
......@@ -61,9 +61,11 @@ void FidoHidDevice::Transition(std::vector<uint8_t> command,
state_ = State::kBusy;
ArmTimeout(repeating_callback);
// Write message to the device.
const auto command_type = supported_protocol() == ProtocolVersion::kCtap
? FidoHidDeviceCommand::kCbor
: FidoHidDeviceCommand::kMsg;
WriteMessage(
FidoHidMessage::Create(channel_id_, FidoHidDeviceCommand::kMsg,
std::move(command)),
FidoHidMessage::Create(channel_id_, command_type, std::move(command)),
true,
base::BindOnce(&FidoHidDevice::MessageReceived,
weak_factory_.GetWeakPtr(), repeating_callback));
......@@ -273,13 +275,34 @@ void FidoHidDevice::MessageReceived(DeviceCallback callback,
std::unique_ptr<FidoHidMessage> message) {
if (state_ == State::kDeviceError)
return;
timeout_callback_.Cancel();
if (!success) {
if (!success || !message) {
state_ = State::kDeviceError;
Transition(std::vector<uint8_t>(), std::move(callback));
return;
}
const auto cmd = message->cmd();
// If received HID packet has keep_alive as command type, re-read after delay.
if (supported_protocol() == ProtocolVersion::kCtap &&
cmd == FidoHidDeviceCommand::kKeepAlive) {
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&FidoHidDevice::OnKeepAlive, weak_factory_.GetWeakPtr(),
std::move(callback)),
kHidKeepAliveDelay);
return;
}
if (cmd != FidoHidDeviceCommand::kMsg && cmd != FidoHidDeviceCommand::kCbor) {
DLOG(ERROR) << "Unexpected HID device command received.";
state_ = State::kDeviceError;
Transition(std::vector<uint8_t>(), std::move(callback));
return;
}
auto response = message->GetMessagePayload();
state_ = State::kReady;
base::WeakPtr<FidoHidDevice> self = weak_factory_.GetWeakPtr();
std::move(callback).Run(
......@@ -310,6 +333,15 @@ void FidoHidDevice::TryWink(WinkCallback callback) {
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void FidoHidDevice::OnKeepAlive(DeviceCallback callback) {
auto repeating_callback =
base::AdaptCallbackForRepeating(std::move(callback));
ArmTimeout(repeating_callback);
ReadMessage(base::BindOnce(&FidoHidDevice::MessageReceived,
weak_factory_.GetWeakPtr(),
std::move(repeating_callback)));
}
void FidoHidDevice::OnWink(WinkCallback callback,
bool success,
std::unique_ptr<FidoHidMessage> response) {
......
......@@ -92,16 +92,17 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoHidDevice : public FidoDevice {
bool success,
uint8_t report_id,
const base::Optional<std::vector<uint8_t>>& buf);
void OnKeepAlive(DeviceCallback callback);
void OnWink(WinkCallback callback,
bool success,
std::unique_ptr<FidoHidMessage> response);
void ArmTimeout(DeviceCallback callback);
void OnTimeout(DeviceCallback callback);
base::WeakPtr<FidoDevice> GetWeakPtr() override;
uint32_t channel_id_ = kBroadcastChannel;
uint8_t capabilities_ = 0;
State state_ = State::kInit;
base::CancelableOnceClosure timeout_callback_;
std::queue<std::pair<std::vector<uint8_t>, DeviceCallback>>
......
......@@ -16,6 +16,7 @@
#include "device/fido/fido_constants.h"
#include "device/fido/fido_hid_device.h"
#include "device/fido/test_callback_receiver.h"
#include "device/fido/u2f_parsing_utils.h"
#include "device/fido/u2f_request.h"
#include "mojo/public/cpp/bindings/binding.h"
#include "mojo/public/cpp/bindings/interface_request.h"
......@@ -28,45 +29,53 @@ namespace device {
using ::testing::_;
using ::testing::Invoke;
using ::testing::WithArg;
using ::testing::WithArgs;
namespace {
std::string HexEncode(base::span<const uint8_t> in) {
return base::HexEncode(in.data(), in.size());
}
// HID_MSG(83), followed by payload length(0008), followed by 'U2F_V2', followed
// by APDU response code(9000).
constexpr uint8_t kMockVersionResponseSuffix[] = {
0x83, 0x00, 0x08, 0x55, 0x32, 0x46, 0x5f, 0x56, 0x32, 0x90, 0x00};
std::vector<uint8_t> HexDecode(base::StringPiece in) {
std::vector<uint8_t> out;
bool result = base::HexStringToBytes(in.as_string(), &out);
DCHECK(result);
return out;
}
// HID_KEEP_ALIVE(bb), followed by payload length(0001), followed by
// status processing(01) byte.
constexpr uint8_t kMockKeepAliveResponseSuffix[] = {0xbb, 0x00, 0x01, 0x01};
// Converts hex encoded StringPiece to byte vector and pads zero to fit HID
// packet size.
std::vector<uint8_t> MakePacket(base::StringPiece hex) {
std::vector<uint8_t> out = HexDecode(hex);
out.resize(64);
return out;
}
// 4 byte broadcast channel id(ffffffff), followed by an HID_INIT command(86),
// followed by a fixed size payload length(11). 8 byte nonce and 4 byte channel
// ID must be appended to create a well formed HID_INIT packet.
constexpr uint8_t kInitResponsePrefix[] = {
0xff, 0xff, 0xff, 0xff, 0x86, 0x00, 0x11,
};
// Returns HID_INIT request to send to device with mock connection.
std::vector<uint8_t> CreateMockInitResponse(std::vector<uint8_t> nonce,
std::vector<uint8_t> channel_id) {
// 4 bytes of broadcast channel identifier(ffffffff), followed by
// HID_INIT command(86) and 2 byte payload length(11).
return MakePacket("ffffffff860011" + HexEncode(nonce) +
HexEncode(channel_id));
std::vector<uint8_t> CreateMockInitResponse(
base::span<const uint8_t> nonce,
base::span<const uint8_t> channel_id) {
auto init_response = u2f_parsing_utils::Materialize(kInitResponsePrefix);
u2f_parsing_utils::Append(&init_response, nonce);
u2f_parsing_utils::Append(&init_response, channel_id);
init_response.resize(64);
return init_response;
}
// Returns HID keep alive message encoded into HID packet format.
std::vector<uint8_t> GetKeepAliveHidMessage(
base::span<const uint8_t> channel_id) {
auto response = u2f_parsing_utils::Materialize(channel_id);
u2f_parsing_utils::Append(&response, kMockKeepAliveResponseSuffix);
response.resize(64);
return response;
}
// Returns "U2F_v2" as a mock response to version request with given channel id.
std::vector<uint8_t> CreateMockVersionResponse(
std::vector<uint8_t> channel_id) {
// HID_MSG command(83), followed by payload length(0008), followed by
// hex encoded "U2F_V2" and NO_ERROR response code(9000).
return MakePacket(HexEncode(channel_id) + "8300085532465f56329000");
std::vector<uint8_t> CreateMockResponse(
base::span<const uint8_t> channel_id,
base::span<const uint8_t> response_buffer) {
auto response = u2f_parsing_utils::Materialize(channel_id);
u2f_parsing_utils::Append(&response, response_buffer);
response.resize(64);
return response;
}
// Returns U2F_V2 version response formatted in APDU response encoding.
......@@ -127,19 +136,16 @@ using TestDeviceCallbackReceiver =
class FidoHidDeviceTest : public ::testing::Test {
public:
FidoHidDeviceTest()
: scoped_task_environment_(
base::test::ScopedTaskEnvironment::MainThreadType::UI) {}
void SetUp() override {
fake_hid_manager_ = std::make_unique<FakeHidManager>();
fake_hid_manager_->AddBinding2(mojo::MakeRequest(&hid_manager_));
}
protected:
base::test::ScopedTaskEnvironment scoped_task_environment_{
base::test::ScopedTaskEnvironment::MainThreadType::MOCK_TIME};
device::mojom::HidManagerPtr hid_manager_;
std::unique_ptr<FakeHidManager> fake_hid_manager_;
base::test::ScopedTaskEnvironment scoped_task_environment_;
};
TEST_F(FidoHidDeviceTest, TestConnectionFailure) {
......@@ -223,65 +229,55 @@ TEST_F(FidoHidDeviceTest, TestDeviceError) {
}
TEST_F(FidoHidDeviceTest, TestRetryChannelAllocation) {
const std::vector<uint8_t> kIncorrectNonce = {0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00};
constexpr uint8_t kIncorrectNonce[] = {0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00};
const std::vector<uint8_t> kChannelId = {0x01, 0x02, 0x03, 0x04};
constexpr uint8_t kChannelId[] = {0x01, 0x02, 0x03, 0x04};
auto hid_device = TestHidDevice();
// Replace device HID connection with custom client connection bound to mock
// server-side mojo connection.
device::mojom::HidConnectionPtr connection_client;
MockHidConnection mock_connection(
hid_device.Clone(), mojo::MakeRequest(&connection_client), kChannelId);
// Delegate custom functions to be invoked for mock hid connection.
EXPECT_CALL(mock_connection, WritePtr(_, _, _))
// HID_INIT request to authenticator for channel allocation.
.WillOnce(WithArgs<1, 2>(
Invoke([&](const std::vector<uint8_t>& buffer,
device::mojom::HidConnection::WriteCallback* cb) {
mock_connection.SetNonce(base::make_span(buffer).subspan(7, 8));
std::move(*cb).Run(true);
})))
// HID_MSG request to authenticator for version request.
.WillOnce(WithArgs<2>(
Invoke([](device::mojom::HidConnection::WriteCallback* cb) {
std::move(*cb).Run(true);
})));
MockHidConnection mock_connection(hid_device.Clone(),
mojo::MakeRequest(&connection_client),
u2f_parsing_utils::Materialize(kChannelId));
// Initial write for establishing a channel ID.
mock_connection.ExpectWriteHidInit();
// HID_MSG request to authenticator for version request.
mock_connection.ExpectHidWriteWithCommand(FidoHidDeviceCommand::kMsg);
EXPECT_CALL(mock_connection, ReadPtr(_))
// First response to HID_INIT request with incorrect nonce.
.WillOnce(WithArg<0>(
Invoke([kIncorrectNonce, &mock_connection](
device::mojom::HidConnection::ReadCallback* cb) {
// First response to HID_INIT request with an incorrect nonce.
.WillOnce(
Invoke([kIncorrectNonce, &mock_connection](auto* cb) {
std::move(*cb).Run(
true, 0,
CreateMockInitResponse(
kIncorrectNonce, mock_connection.connection_channel_id()));
})))
// Second response to HID_INIT request with correct nonce.
.WillOnce(WithArg<0>(Invoke(
}))
// Second response to HID_INIT request with a correct nonce.
.WillOnce(Invoke(
[&mock_connection](device::mojom::HidConnection::ReadCallback* cb) {
std::move(*cb).Run(true, 0,
CreateMockInitResponse(
mock_connection.nonce(),
mock_connection.connection_channel_id()));
})))
}))
// Version response from the authenticator.
.WillOnce(WithArg<0>(Invoke(
.WillOnce(Invoke(
[&mock_connection](device::mojom::HidConnection::ReadCallback* cb) {
std::move(*cb).Run(true, 0,
CreateMockVersionResponse(
mock_connection.connection_channel_id()));
})));
std::move(*cb).Run(
true, 0,
CreateMockResponse(mock_connection.connection_channel_id(),
kMockVersionResponseSuffix));
}));
// Add device and set mock connection to fake hid manager.
fake_hid_manager_->AddDeviceAndSetConnection(std::move(hid_device),
std::move(connection_client));
FidoDeviceEnumerateCallbackReceiver receiver(hid_manager_.get());
hid_manager_->GetDevices(receiver.callback());
receiver.WaitForCallback();
......@@ -301,4 +297,139 @@ TEST_F(FidoHidDeviceTest, TestRetryChannelAllocation) {
EXPECT_THAT(*result, testing::ElementsAreArray(GetValidU2fVersionResponse()));
}
TEST_F(FidoHidDeviceTest, TestKeepAliveMessage) {
constexpr uint8_t kChannelId[] = {0x01, 0x02, 0x03, 0x04};
auto hid_device = TestHidDevice();
// Replace device HID connection with custom client connection bound to mock
// server-side mojo connection.
device::mojom::HidConnectionPtr connection_client;
MockHidConnection mock_connection(hid_device.Clone(),
mojo::MakeRequest(&connection_client),
u2f_parsing_utils::Materialize(kChannelId));
// Initial write for establishing channel ID.
mock_connection.ExpectWriteHidInit();
// HID_CBOR request to authenticator.
mock_connection.ExpectHidWriteWithCommand(FidoHidDeviceCommand::kCbor);
EXPECT_CALL(mock_connection, ReadPtr(_))
// Response to HID_INIT request.
.WillOnce(Invoke([&](device::mojom::HidConnection::ReadCallback* cb) {
std::move(*cb).Run(
true, 0,
CreateMockInitResponse(mock_connection.nonce(),
mock_connection.connection_channel_id()));
}))
// // Keep alive message sent from the authenticator.
.WillOnce(Invoke([&](device::mojom::HidConnection::ReadCallback* cb) {
std::move(*cb).Run(
true, 0,
GetKeepAliveHidMessage(mock_connection.connection_channel_id()));
}))
// Repeated Read() invocation due to keep alive message. Sends a dummy
// response that corresponds to U2F version response.
.WillOnce(Invoke([&](device::mojom::HidConnection::ReadCallback* cb) {
auto almost_time_out =
kDeviceTimeout - base::TimeDelta::FromMicroseconds(1);
scoped_task_environment_.FastForwardBy(almost_time_out);
std::move(*cb).Run(
true, 0,
CreateMockResponse(mock_connection.connection_channel_id(),
kMockVersionResponseSuffix));
}));
// Add device and set mock connection to fake hid manager.
fake_hid_manager_->AddDeviceAndSetConnection(std::move(hid_device),
std::move(connection_client));
FidoDeviceEnumerateCallbackReceiver receiver(hid_manager_.get());
hid_manager_->GetDevices(receiver.callback());
receiver.WaitForCallback();
std::vector<std::unique_ptr<FidoHidDevice>> u2f_devices =
receiver.TakeReturnedDevicesFiltered();
ASSERT_EQ(1u, u2f_devices.size());
auto& device = u2f_devices.front();
// Keep alive message handling is only supported for CTAP HID device.
device->set_supported_protocol(ProtocolVersion::kCtap);
TestDeviceCallbackReceiver cb;
device->DeviceTransact(U2fRequest::GetU2fVersionApduCommand(false),
cb.callback());
cb.WaitForCallback();
const auto result = std::get<0>(*cb.result());
ASSERT_TRUE(result);
EXPECT_THAT(*result, testing::ElementsAreArray(GetValidU2fVersionResponse()));
}
TEST_F(FidoHidDeviceTest, TestDeviceTimeoutAfterKeepAliveMessage) {
constexpr uint8_t kChannelId[] = {0x01, 0x02, 0x03, 0x04};
auto hid_device = TestHidDevice();
// Replace device HID connection with custom client connection bound to mock
// server-side mojo connection.
device::mojom::HidConnectionPtr connection_client;
MockHidConnection mock_connection(hid_device.Clone(),
mojo::MakeRequest(&connection_client),
u2f_parsing_utils::Materialize(kChannelId));
// Initial write for establishing channel ID.
mock_connection.ExpectWriteHidInit();
// HID_CBOR request to authenticator.
mock_connection.ExpectHidWriteWithCommand(FidoHidDeviceCommand::kCbor);
EXPECT_CALL(mock_connection, ReadPtr(_))
// Response to HID_INIT request.
.WillOnce(Invoke([&](device::mojom::HidConnection::ReadCallback* cb) {
std::move(*cb).Run(
true, 0,
CreateMockInitResponse(mock_connection.nonce(),
mock_connection.connection_channel_id()));
}))
// // Keep alive message sent from the authenticator.
.WillOnce(Invoke([&](device::mojom::HidConnection::ReadCallback* cb) {
std::move(*cb).Run(
true, 0,
GetKeepAliveHidMessage(mock_connection.connection_channel_id()));
}))
// Repeated Read() invocation due to keep alive message. The callback
// is invoked only after 3 seconds, which should cause device to timeout.
.WillOnce(Invoke([&](device::mojom::HidConnection::ReadCallback* cb) {
scoped_task_environment_.FastForwardBy(kDeviceTimeout);
std::move(*cb).Run(
true, 0,
CreateMockResponse(mock_connection.connection_channel_id(),
kMockVersionResponseSuffix));
}));
// Add device and set mock connection to fake hid manager.
fake_hid_manager_->AddDeviceAndSetConnection(std::move(hid_device),
std::move(connection_client));
FidoDeviceEnumerateCallbackReceiver receiver(hid_manager_.get());
hid_manager_->GetDevices(receiver.callback());
receiver.WaitForCallback();
std::vector<std::unique_ptr<FidoHidDevice>> u2f_devices =
receiver.TakeReturnedDevicesFiltered();
ASSERT_EQ(1u, u2f_devices.size());
auto& device = u2f_devices.front();
// Keep alive message handling is only supported for CTAP HID device.
device->set_supported_protocol(ProtocolVersion::kCtap);
TestDeviceCallbackReceiver cb;
device->DeviceTransact(U2fRequest::GetU2fVersionApduCommand(false),
cb.callback());
cb.WaitForCallback();
const auto result = std::get<0>(*cb.result());
EXPECT_FALSE(result);
EXPECT_EQ(FidoDevice::State::kDeviceError, device->state());
}
} // namespace device
......@@ -52,6 +52,10 @@ void FidoRequestHandlerBase::DiscoveryStarted(FidoDiscovery* discovery,
void FidoRequestHandlerBase::DeviceAdded(FidoDiscovery* discovery,
FidoDevice* device) {
DCHECK(!base::ContainsKey(ongoing_tasks(), device->GetId()));
// All devices are initially assumed to support CTAP protocol and thus
// AuthenticatorGetInfo command is sent to all connected devices. If device
// errors out, then it is assumed to support U2F protocol.
device->set_supported_protocol(ProtocolVersion::kCtap);
ongoing_tasks_.emplace(device->GetId(), CreateTaskForNewDevice(device));
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment