Skip to content

Commit d469fc9

Browse files
authored
tests: cover BLE pairing flow regressions (#62)
Verify pairing persists a generated key and sends the direct whitelist request with the longer response timeout. This locks in the PR 61 fixes that fail on main.
1 parent dcb4332 commit d469fc9

1 file changed

Lines changed: 117 additions & 0 deletions

File tree

tests/test_vehicle.cpp

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
#include "tb_utils.h"
66
#include "test_constants.h"
77

8+
#include <pb_decode.h>
9+
810
#include <algorithm>
911
#include <array>
1012
#include <cstring>
1113
#include <vector>
1214

1315
using TeslaBLE::Command;
16+
using TeslaBLE::CommandState;
1417
using TeslaBLE::TeslaBLE_Status_E_OK;
1518
using TeslaBLE::Vehicle;
1619
using TeslaBLE::Client;
@@ -277,6 +280,60 @@ std::vector<uint8_t> make_vcsec_session_info_key_not_on_whitelist(const pb_byte_
277280
sizeof(MOCK_VCSEC_MESSAGE),
278281
Signatures_Session_Info_Status_SESSION_INFO_STATUS_KEY_NOT_ON_WHITELIST);
279282
}
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+
}
280337
} // namespace
281338

282339
class VehicleTest : public ::testing::Test {
@@ -854,6 +911,66 @@ TEST_F(VehicleTest, MultipleDisconnectsOnlyCallbackOnce) {
854911
EXPECT_EQ(callback_count, 1) << "Callback should only be invoked once";
855912
}
856913

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+
857974
// ============================================================================
858975
// Command Struct Tests
859976
// ============================================================================

0 commit comments

Comments
 (0)