| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "device/fido/fido_cable_handshake_handler.h" |
| |
| #include <array> |
| #include <limits> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/optional.h" |
| #include "base/test/scoped_task_environment.h" |
| #include "base/threading/sequenced_task_runner_handle.h" |
| #include "components/cbor/cbor_reader.h" |
| #include "components/cbor/cbor_values.h" |
| #include "components/cbor/cbor_writer.h" |
| #include "crypto/hkdf.h" |
| #include "crypto/hmac.h" |
| #include "device/bluetooth/test/bluetooth_test.h" |
| #include "device/fido/fido_ble_frames.h" |
| #include "device/fido/fido_cable_device.h" |
| #include "device/fido/fido_constants.h" |
| #include "device/fido/fido_parsing_utils.h" |
| #include "device/fido/mock_fido_ble_connection.h" |
| #include "device/fido/test_callback_receiver.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace device { |
| |
| namespace { |
| |
| using ::testing::_; |
| using ::testing::Invoke; |
| using ::testing::Test; |
| using TestDeviceCallbackReceiver = |
| test::ValueCallbackReceiver<base::Optional<std::vector<uint8_t>>>; |
| |
| // Sufficiently large test control point length as we are not interested |
| // in testing fragmentations of BLE messages. All Cable messages are encrypted |
| // and decrypted per request frame, not fragment. |
| constexpr auto kControlPointLength = std::numeric_limits<uint16_t>::max(); |
| |
| constexpr std::array<uint8_t, 16> kAuthenticatorSessionRandom = {{ |
| 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, |
| 0x01, 0x02, 0x03, 0x04, |
| }}; |
| |
| constexpr std::array<uint8_t, 32> kTestSessionPreKey = {{ |
| 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
| 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
| 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, |
| }}; |
| |
| constexpr std::array<uint8_t, 32> kIncorrectSessionPreKey = {{ |
| 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, |
| 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, |
| 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, |
| }}; |
| |
| constexpr std::array<uint8_t, 8> kTestNonce = {{ |
| 0x15, 0x14, 0x13, 0x12, 0x11, 0x10, 0x09, 0x08, |
| }}; |
| |
| constexpr std::array<uint8_t, 8> kIncorrectNonce = {{ |
| 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, |
| }}; |
| |
| constexpr std::array<uint8_t, 50> kValidAuthenticatorHello = {{ |
| // Map(2) |
| 0xA2, |
| // Key(0) |
| 0x00, |
| // Text(28) |
| 0x78, 0x1C, |
| // "caBLE v1 authenticator hello" |
| 0x63, 0x61, 0x42, 0x4C, 0x45, 0x20, 0x76, 0x31, 0x20, 0x61, 0x75, 0x74, |
| 0x68, 0x65, 0x6E, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6F, 0x72, 0x20, 0x68, |
| 0x65, 0x6C, 0x6C, 0x6F, |
| // Key(1) |
| 0x01, |
| // Bytes(16) |
| 0x50, |
| // Authenticator random session |
| 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, |
| 0x01, 0x02, 0x03, 0x04, |
| }}; |
| |
| constexpr std::array<uint8_t, 43> kInvalidAuthenticatorHello = {{ |
| // Map(2) |
| 0xA2, |
| // Key(0) |
| 0x00, |
| // Text(21) |
| 0x75, |
| // "caBLE INVALID MESSAGE" |
| 0x63, 0x61, 0x42, 0x4C, 0x45, 0x20, 0x49, 0x4E, 0x56, 0x41, 0x4C, 0x49, |
| 0x44, 0x20, 0x4D, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45, |
| // Key(1) |
| 0x01, |
| // Bytes(16) |
| 0x50, |
| // Authenticator random session |
| 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, |
| 0x01, 0x02, 0x03, 0x04, |
| }}; |
| |
| constexpr char kIncorrectHandshakeKey[] = "INCORRECT_HANDSHAKE_KEY_12345678"; |
| |
| // Returns the expected encryption key that should be constructed given that |
| // the client random nonce is |client_random_nonce| and other determining |
| // factors (i.e. authenticator session random, session pre key, and nonce) are |
| // |kAuthenticatorSessionRandom|, |kTestSessionPreKey|, and |kTestNonce|, |
| // respectively. |
| std::string GetExpectedEncryptionKey( |
| base::span<const uint8_t> client_random_nonce) { |
| std::vector<uint8_t> nonce_message = |
| fido_parsing_utils::Materialize(kTestNonce); |
| fido_parsing_utils::Append(&nonce_message, client_random_nonce); |
| fido_parsing_utils::Append(&nonce_message, kAuthenticatorSessionRandom); |
| return crypto::HkdfSha256( |
| fido_parsing_utils::ConvertToStringPiece(kTestSessionPreKey), |
| fido_parsing_utils::ConvertToStringPiece( |
| fido_parsing_utils::CreateSHA256Hash( |
| fido_parsing_utils::ConvertToStringPiece(nonce_message))), |
| kCableDeviceEncryptionKeyInfo, 32); |
| } |
| |
| // Given a hello message and handshake key from the authenticator, construct |
| // a handshake message by concatenating hello message and its mac message |
| // derived from |handshake_key|. |
| std::vector<uint8_t> ConstructAuthenticatorHelloReply( |
| base::span<const uint8_t> hello_msg, |
| base::StringPiece handshake_key) { |
| auto reply = fido_parsing_utils::Materialize(hello_msg); |
| crypto::HMAC hmac(crypto::HMAC::SHA256); |
| if (!hmac.Init(handshake_key)) |
| return std::vector<uint8_t>(); |
| |
| std::array<uint8_t, 32> authenticator_hello_mac; |
| if (!hmac.Sign(fido_parsing_utils::ConvertToStringPiece(hello_msg), |
| authenticator_hello_mac.data(), |
| authenticator_hello_mac.size())) { |
| return std::vector<uint8_t>(); |
| } |
| |
| fido_parsing_utils::Append( |
| &reply, base::make_span(authenticator_hello_mac).first(16)); |
| return reply; |
| } |
| |
| // Constructs incoming handshake message from the authenticator into a BLE |
| // control fragment. |
| std::vector<uint8_t> ConstructSerializedOutgoingFragment( |
| base::span<const uint8_t> data) { |
| if (data.empty()) |
| return std::vector<uint8_t>(); |
| |
| FidoBleFrame response_frame(FidoBleDeviceCommand::kControl, |
| fido_parsing_utils::Materialize(data)); |
| const auto response_fragment = |
| std::get<0>(response_frame.ToFragments(kControlPointLength)); |
| |
| std::vector<uint8_t> outgoing_message; |
| response_fragment.Serialize(&outgoing_message); |
| return outgoing_message; |
| } |
| |
| // Authenticator abstraction that handles logic related to validating handshake |
| // messages from the client and sending rely handshake message back to the |
| // client. Session key and nonce are assumed to be |kTestSessionPreKey| and |
| // |kTestNonce| respectively. |
| class FakeCableAuthenticator { |
| public: |
| FakeCableAuthenticator() { |
| handshake_key_ = crypto::HkdfSha256( |
| fido_parsing_utils::ConvertToStringPiece(kTestSessionPreKey), |
| fido_parsing_utils::ConvertToStringPiece(kTestNonce), |
| kCableHandshakeKeyInfo, 32); |
| } |
| |
| // Receives handshake message from the client, check its validity and if the |
| // handshake message is valid, store |client_session_random| embedded in the |
| // handshake message. |
| bool ConfirmClientHandshakeMessage( |
| base::span<const uint8_t> handshake_message) { |
| if (handshake_message.size() <= 16) |
| return false; |
| |
| crypto::HMAC hmac(crypto::HMAC::SHA256); |
| if (!hmac.Init(handshake_key_)) |
| return false; |
| |
| // Handshake message from client should be concatenation of client hello |
| // message (42 bytes) with message authentication code (16 bytes). |
| if (handshake_message.size() != 58) |
| return false; |
| |
| const auto client_hello = handshake_message.first(42); |
| if (!hmac.VerifyTruncated( |
| fido_parsing_utils::ConvertToStringPiece(client_hello), |
| fido_parsing_utils::ConvertToStringPiece( |
| handshake_message.subspan(42)))) { |
| return false; |
| } |
| |
| const auto& client_hello_cbor = cbor::CBORReader::Read(client_hello); |
| if (!client_hello_cbor) |
| return false; |
| |
| const auto& message_map = client_hello_cbor->GetMap(); |
| auto hello_message_it = message_map.find(cbor::CBORValue(0)); |
| auto client_random_nonce_it = message_map.find(cbor::CBORValue(1)); |
| if (hello_message_it == message_map.end() || |
| client_random_nonce_it == message_map.end()) |
| return false; |
| |
| if (!hello_message_it->second.is_string() || |
| hello_message_it->second.GetString() != kCableClientHelloMessage) { |
| return false; |
| } |
| |
| if (!client_random_nonce_it->second.is_bytestring() || |
| client_random_nonce_it->second.GetBytestring().size() != 16) { |
| return false; |
| } |
| |
| client_session_random_ = |
| std::move(client_random_nonce_it->second.GetBytestring()); |
| return true; |
| } |
| |
| std::vector<uint8_t> RelyWithAuthenticatorHandShakeMessage( |
| base::span<const uint8_t> handshake_message) { |
| if (!ConfirmClientHandshakeMessage(handshake_message)) |
| return std::vector<uint8_t>(); |
| |
| return ConstructAuthenticatorHelloReply(kValidAuthenticatorHello, |
| handshake_key_); |
| } |
| |
| private: |
| std::string handshake_key_; |
| std::vector<uint8_t> client_session_random_; |
| std::vector<uint8_t> authenticator_session_random_ = |
| fido_parsing_utils::Materialize(kAuthenticatorSessionRandom); |
| }; |
| |
| } // namespace |
| |
| class FidoCableHandshakeHandlerTest : public Test { |
| public: |
| FidoCableHandshakeHandlerTest() { |
| auto connection = std::make_unique<MockFidoBleConnection>( |
| BluetoothTestBase::kTestDeviceAddress1); |
| connection_ = connection.get(); |
| device_ = std::make_unique<FidoCableDevice>(std::move(connection)); |
| |
| connection_->connection_status_callback() = |
| device_->GetConnectionStatusCallbackForTesting(); |
| connection_->read_callback() = device_->GetReadCallbackForTesting(); |
| } |
| |
| std::unique_ptr<FidoCableHandshakeHandler> CreateHandshakeHandler( |
| std::array<uint8_t, 8> nonce, |
| std::array<uint8_t, 32> session_pre_key) { |
| return std::make_unique<FidoCableHandshakeHandler>(device_.get(), nonce, |
| session_pre_key); |
| } |
| |
| void ConnectWithLength(uint16_t length) { |
| EXPECT_CALL(*connection(), Connect()).WillOnce(Invoke([this] { |
| connection()->connection_status_callback().Run(true); |
| })); |
| |
| EXPECT_CALL(*connection(), ReadControlPointLengthPtr(_)) |
| .WillOnce(Invoke([length](auto* cb) { std::move(*cb).Run(length); })); |
| |
| device()->Connect(); |
| } |
| |
| FidoCableDevice* device() { return device_.get(); } |
| MockFidoBleConnection* connection() { return connection_; } |
| FakeCableAuthenticator* authenticator() { return &authenticator_; } |
| TestDeviceCallbackReceiver& callback_receiver() { return callback_receiver_; } |
| |
| protected: |
| base::test::ScopedTaskEnvironment scoped_task_environment_; |
| |
| private: |
| FakeCableAuthenticator authenticator_; |
| MockFidoBleConnection* connection_; |
| std::unique_ptr<FidoCableDevice> device_; |
| TestDeviceCallbackReceiver callback_receiver_; |
| }; |
| |
| // Checks that outgoing handshake message from the client is a BLE frame with |
| // Control command type. |
| MATCHER(IsControlFrame, "") { |
| return !arg.empty() && |
| arg[0] == base::strict_cast<uint8_t>(FidoBleDeviceCommand::kControl); |
| } |
| |
| TEST_F(FidoCableHandshakeHandlerTest, HandShakeSuccess) { |
| ConnectWithLength(kControlPointLength); |
| |
| EXPECT_CALL(*connection(), WriteControlPointPtr(IsControlFrame(), _)) |
| .WillOnce(Invoke([this](const auto& data, auto* cb) { |
| base::SequencedTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(*cb), true)); |
| |
| const auto client_ble_handshake_message = |
| base::make_span(data).subspan(3); |
| base::SequencedTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| connection()->read_callback(), |
| ConstructSerializedOutgoingFragment( |
| authenticator()->RelyWithAuthenticatorHandShakeMessage( |
| client_ble_handshake_message)))); |
| })); |
| |
| auto handshake_handler = |
| CreateHandshakeHandler(kTestNonce, kTestSessionPreKey); |
| handshake_handler->InitiateCableHandshake(callback_receiver().callback()); |
| |
| callback_receiver().WaitForCallback(); |
| const auto& value = callback_receiver().value(); |
| ASSERT_TRUE(value); |
| EXPECT_TRUE(handshake_handler->ValidateAuthenticatorHandshakeMessage(*value)); |
| EXPECT_EQ(GetExpectedEncryptionKey(handshake_handler->client_session_random_), |
| handshake_handler->GetEncryptionKeyAfterSuccessfulHandshake( |
| kAuthenticatorSessionRandom)); |
| } |
| |
| TEST_F(FidoCableHandshakeHandlerTest, HandShakeWithIncorrectSessionPreKey) { |
| ConnectWithLength(kControlPointLength); |
| |
| EXPECT_CALL(*connection(), WriteControlPointPtr(IsControlFrame(), _)) |
| .WillOnce(Invoke([this](const auto& data, auto* cb) { |
| base::SequencedTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(*cb), true)); |
| |
| const auto client_ble_handshake_message = |
| base::make_span(data).subspan(3); |
| base::SequencedTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| connection()->read_callback(), |
| ConstructSerializedOutgoingFragment( |
| authenticator()->RelyWithAuthenticatorHandShakeMessage( |
| client_ble_handshake_message)))); |
| })); |
| |
| auto handshake_handler = |
| CreateHandshakeHandler(kTestNonce, kIncorrectSessionPreKey); |
| handshake_handler->InitiateCableHandshake(callback_receiver().callback()); |
| |
| callback_receiver().WaitForCallback(); |
| EXPECT_FALSE(callback_receiver().value()); |
| } |
| |
| TEST_F(FidoCableHandshakeHandlerTest, HandshakeFailWithIncorrectNonce) { |
| ConnectWithLength(kControlPointLength); |
| |
| EXPECT_CALL(*connection(), WriteControlPointPtr(IsControlFrame(), _)) |
| .WillOnce(Invoke([this](const auto& data, auto* cb) { |
| base::SequencedTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(*cb), true)); |
| |
| const auto client_ble_handshake_message = |
| base::make_span(data).subspan(3); |
| base::SequencedTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| connection()->read_callback(), |
| ConstructSerializedOutgoingFragment( |
| authenticator()->RelyWithAuthenticatorHandShakeMessage( |
| client_ble_handshake_message)))); |
| })); |
| |
| auto handshake_handler = |
| CreateHandshakeHandler(kIncorrectNonce, kTestSessionPreKey); |
| handshake_handler->InitiateCableHandshake(callback_receiver().callback()); |
| |
| callback_receiver().WaitForCallback(); |
| EXPECT_FALSE(callback_receiver().value()); |
| } |
| |
| TEST_F(FidoCableHandshakeHandlerTest, |
| HandshakeFailWithIncorrectAuthenticatorResponse) { |
| auto handshake_handler = |
| CreateHandshakeHandler(kTestNonce, kTestSessionPreKey); |
| |
| EXPECT_NE(kIncorrectHandshakeKey, handshake_handler->handshake_key_); |
| const auto authenticator_reply_with_invalid_key = |
| ConstructAuthenticatorHelloReply(kValidAuthenticatorHello, |
| kIncorrectHandshakeKey); |
| EXPECT_FALSE(handshake_handler->ValidateAuthenticatorHandshakeMessage( |
| authenticator_reply_with_invalid_key)); |
| |
| const auto authenticator_reply_with_invalid_hello_msg = |
| ConstructAuthenticatorHelloReply(kInvalidAuthenticatorHello, |
| handshake_handler->handshake_key_); |
| EXPECT_FALSE(handshake_handler->ValidateAuthenticatorHandshakeMessage( |
| authenticator_reply_with_invalid_hello_msg)); |
| } |
| |
| } // namespace device |