|
5 | 5 | #include "tb_utils.h" |
6 | 6 | #include "test_constants.h" |
7 | 7 |
|
| 8 | +#include <pb_decode.h> |
| 9 | + |
8 | 10 | #include <algorithm> |
9 | 11 | #include <array> |
10 | 12 | #include <cstring> |
11 | 13 | #include <vector> |
12 | 14 |
|
13 | 15 | using TeslaBLE::Command; |
| 16 | +using TeslaBLE::CommandState; |
14 | 17 | using TeslaBLE::TeslaBLE_Status_E_OK; |
15 | 18 | using TeslaBLE::Vehicle; |
16 | 19 | using TeslaBLE::Client; |
@@ -277,6 +280,60 @@ std::vector<uint8_t> make_vcsec_session_info_key_not_on_whitelist(const pb_byte_ |
277 | 280 | sizeof(MOCK_VCSEC_MESSAGE), |
278 | 281 | Signatures_Session_Info_Status_SESSION_INFO_STATUS_KEY_NOT_ON_WHITELIST); |
279 | 282 | } |
| 283 | + |
| 284 | +bool parse_whitelist_message(const std::vector<uint8_t> &frame, VCSEC_PermissionChange *permissions, |
| 285 | + VCSEC_KeyFormFactor *form_factor, VCSEC_SignatureType *signature_type) { |
| 286 | + if (frame.size() <= 2) { |
| 287 | + return false; |
| 288 | + } |
| 289 | + |
| 290 | + VCSEC_ToVCSECMessage vcsec_message = VCSEC_ToVCSECMessage_init_default; |
| 291 | + pb_istream_t vcsec_stream = pb_istream_from_buffer(frame.data() + 2, frame.size() - 2); |
| 292 | + if (!pb_decode(&vcsec_stream, VCSEC_ToVCSECMessage_fields, &vcsec_message) || !vcsec_message.has_signedMessage) { |
| 293 | + return false; |
| 294 | + } |
| 295 | + |
| 296 | + if (signature_type) { |
| 297 | + *signature_type = vcsec_message.signedMessage.signatureType; |
| 298 | + } |
| 299 | + |
| 300 | + VCSEC_UnsignedMessage unsigned_message = VCSEC_UnsignedMessage_init_default; |
| 301 | + pb_istream_t payload_stream = pb_istream_from_buffer(vcsec_message.signedMessage.protobufMessageAsBytes.bytes, |
| 302 | + vcsec_message.signedMessage.protobufMessageAsBytes.size); |
| 303 | + if (!pb_decode(&payload_stream, VCSEC_UnsignedMessage_fields, &unsigned_message) || |
| 304 | + unsigned_message.which_sub_message != VCSEC_UnsignedMessage_WhitelistOperation_tag) { |
| 305 | + return false; |
| 306 | + } |
| 307 | + |
| 308 | + const auto &whitelist = unsigned_message.sub_message.WhitelistOperation; |
| 309 | + if (whitelist.which_sub_message != VCSEC_WhitelistOperation_addKeyToWhitelistAndAddPermissions_tag) { |
| 310 | + return false; |
| 311 | + } |
| 312 | + |
| 313 | + if (permissions) { |
| 314 | + *permissions = whitelist.sub_message.addKeyToWhitelistAndAddPermissions; |
| 315 | + } |
| 316 | + if (form_factor) { |
| 317 | + *form_factor = whitelist.metadataForKey.keyFormFactor; |
| 318 | + } |
| 319 | + |
| 320 | + return true; |
| 321 | +} |
| 322 | + |
| 323 | +std::vector<uint8_t> derive_public_key_from_private_key(const std::vector<uint8_t> &private_key) { |
| 324 | + TeslaBLE::CryptoContext crypto_context; |
| 325 | + if (crypto_context.load_private_key(private_key.data(), private_key.size()) != TeslaBLE_Status_E_OK) { |
| 326 | + return {}; |
| 327 | + } |
| 328 | + |
| 329 | + std::array<pb_byte_t, 128> public_key{}; |
| 330 | + size_t public_key_length = public_key.size(); |
| 331 | + if (crypto_context.generate_public_key(public_key.data(), &public_key_length) != TeslaBLE_Status_E_OK) { |
| 332 | + return {}; |
| 333 | + } |
| 334 | + |
| 335 | + return {public_key.begin(), public_key.begin() + public_key_length}; |
| 336 | +} |
280 | 337 | } // namespace |
281 | 338 |
|
282 | 339 | class VehicleTest : public ::testing::Test { |
@@ -854,6 +911,66 @@ TEST_F(VehicleTest, MultipleDisconnectsOnlyCallbackOnce) { |
854 | 911 | EXPECT_EQ(callback_count, 1) << "Callback should only be invoked once"; |
855 | 912 | } |
856 | 913 |
|
| 914 | +TEST(VehiclePairingTest, PairPersistsGeneratedKeyAndSendsDirectWhitelistRequest) { |
| 915 | + auto ble = std::make_shared<MockBleAdapter>(); |
| 916 | + auto storage = std::make_shared<MockStorageAdapter>(); |
| 917 | + auto vehicle = std::make_shared<Vehicle>(ble, storage); |
| 918 | + |
| 919 | + vehicle->set_vin(TEST_VIN); |
| 920 | + vehicle->set_connected(true); |
| 921 | + vehicle->pair(Keys_Role_ROLE_DRIVER); |
| 922 | + |
| 923 | + std::vector<uint8_t> stored_key; |
| 924 | + ASSERT_TRUE(storage->load("private_key", stored_key)) << "Pairing should persist the generated private key"; |
| 925 | + ASSERT_FALSE(stored_key.empty()) << "Persisted private key should not be empty"; |
| 926 | + |
| 927 | + auto expected_public_key = derive_public_key_from_private_key(stored_key); |
| 928 | + ASSERT_FALSE(expected_public_key.empty()) << "Persisted private key should produce a public key"; |
| 929 | + |
| 930 | + vehicle->loop(); |
| 931 | + vehicle->loop(); |
| 932 | + |
| 933 | + const auto &writes = ble->get_written_data(); |
| 934 | + ASSERT_EQ(writes.size(), 1U) << "Pairing should send one direct whitelist request without a session auth handshake"; |
| 935 | + |
| 936 | + VCSEC_PermissionChange permissions = VCSEC_PermissionChange_init_default; |
| 937 | + VCSEC_KeyFormFactor form_factor = VCSEC_KeyFormFactor_KEY_FORM_FACTOR_UNKNOWN; |
| 938 | + VCSEC_SignatureType signature_type = VCSEC_SignatureType_SIGNATURE_TYPE_NONE; |
| 939 | + ASSERT_TRUE(parse_whitelist_message(writes.front(), &permissions, &form_factor, &signature_type)) |
| 940 | + << "First pairing write should be a VCSEC whitelist command"; |
| 941 | + |
| 942 | + EXPECT_EQ(signature_type, VCSEC_SignatureType_SIGNATURE_TYPE_PRESENT_KEY); |
| 943 | + EXPECT_EQ(form_factor, VCSEC_KeyFormFactor_KEY_FORM_FACTOR_NFC_CARD); |
| 944 | + EXPECT_EQ(permissions.keyRole, Keys_Role_ROLE_DRIVER); |
| 945 | + ASSERT_EQ(permissions.key.PublicKeyRaw.size, expected_public_key.size()); |
| 946 | + EXPECT_TRUE(std::equal(expected_public_key.begin(), expected_public_key.end(), permissions.key.PublicKeyRaw.bytes)); |
| 947 | +} |
| 948 | + |
| 949 | +TEST_F(VehicleTest, PairingWhitelistRequestUsesCommandTimeout) { |
| 950 | + vehicle_->set_connected(true); |
| 951 | + vehicle_->pair(); |
| 952 | + vehicle_->loop(); |
| 953 | + |
| 954 | + auto &command_queue = const_cast<std::queue<std::shared_ptr<Command>> &>(vehicle_->get_command_queue_for_testing()); |
| 955 | + ASSERT_FALSE(command_queue.empty()) << "Pairing command should remain pending while waiting for a response"; |
| 956 | + |
| 957 | + auto command = command_queue.front(); |
| 958 | + ASSERT_NE(command, nullptr); |
| 959 | + ASSERT_EQ(command->name, "Whitelist Add Key"); |
| 960 | + |
| 961 | + command->state = CommandState::WAITING_FOR_RESPONSE; |
| 962 | + command->retry_count = 0; |
| 963 | + command->started_at = std::chrono::steady_clock::now() - Vehicle::CLOCK_SYNC_MAX_LATENCY - std::chrono::seconds(1); |
| 964 | + command->last_tx_at = command->started_at; |
| 965 | + |
| 966 | + mock_ble_->clear_written_data(); |
| 967 | + vehicle_->loop(); |
| 968 | + |
| 969 | + EXPECT_EQ(command->retry_count, 0) << "Whitelist pairing should not retry at the normal clock sync timeout"; |
| 970 | + EXPECT_EQ(command->state, CommandState::WAITING_FOR_RESPONSE); |
| 971 | + EXPECT_TRUE(mock_ble_->get_written_data().empty()) << "No retry write should occur before command timeout"; |
| 972 | +} |
| 973 | + |
857 | 974 | // ============================================================================ |
858 | 975 | // Command Struct Tests |
859 | 976 | // ============================================================================ |
|
0 commit comments