Files
Ashin Walpola d107534eb2 Keep vanetza-idf in obu-firmware, so a plain clone builds the firmware
obu-firmware builds against the vanetza-idf C-ITS library, which until now
came from the colleague's microbu-esp32c5 tree beside the repository and was
not tracked here, so a clone of this repository could not build the firmware
it ships. The library alone is now part of obu-firmware, as
obu-firmware/external/vanetza-idf: their external/vanetza-idf at commit
cf4b99f, unchanged (9775 files; see its PROVENANCE.md). CMake takes it from
there by default; -DVANETZA_IDF_DIR still points the build elsewhere.

The rest of the colleague's tree (their own VAM firmware, PKI tooling,
station-link Python tools, the V2X2MAP bridge) stays out of this repository
and gitignored; nothing is pushed to their repository. NOTES.md, docs/06,
TODO.md and the pcap verifier's usage line point at the new location.
2026-09-24 10:56:05 +02:00

264 lines
8.1 KiB
C++

#include "prune_command.hpp"
#include "certificate.hpp"
#include "certificate_filesystem_storage.hpp"
#include "hashed_id8.hpp"
#include "mock_credential_storage.hpp"
#include "openssl_security_module.hpp"
#include "stub_certificate.hpp"
#include "stub_station_configuration.hpp"
#include <vanetza/security/public_key.hpp>
#include <gtest/gtest.h>
#include <chrono>
#include <memory>
#include <string>
#include <unordered_set>
#include <vector>
using vanetza::Clock;
namespace vanetza
{
namespace pki
{
namespace
{
PublicKey make_random_pubkey()
{
// SecurityModule::create_key generates a fresh key pair, stores the
// private half in credential storage, and returns the public half.
// Tests derive the canonical hex from the returned public key so test data
// stays self-consistent.
static auto credentials = std::make_shared<MockCredentialStorage>();
static OpenSslSecurityModule sm(credentials);
return sm.create_key(KeyType::BrainpoolP256r1);
}
class PruneTestFixture : public ::testing::Test
{
protected:
void SetUp() override
{
root = std::filesystem::temp_directory_path() / "pki_unit_test_prune";
std::filesystem::remove_all(root);
cfg.credentials = std::make_shared<MockCredentialStorage>();
cfg.security = std::make_shared<OpenSslSecurityModule>(cfg.credentials);
const auto certs_dir = root / "certs";
cfg.root_ca = std::make_shared<CertificateFilesystemStorage>(cfg.security, certs_dir, ".rca");
cfg.enrolment_credentials = std::make_shared<CertificateFilesystemStorage>(cfg.security, certs_dir, ".ec");
cfg.tlm = std::make_shared<CertificateFilesystemStorage>(cfg.security, certs_dir, ".tlm");
cfg.tickets = std::make_shared<CertificateFilesystemStorage>(cfg.security, certs_dir, ".at");
cfg.station = std::make_shared<StubStationConfiguration>();
}
void TearDown() override
{
std::filesystem::remove_all(root);
}
// Typed accessor for the StubStationConfiguration held in cfg.station.
StubStationConfiguration& station()
{
return static_cast<StubStationConfiguration&>(*cfg.station);
}
std::filesystem::path root;
MainConfig cfg;
};
constexpr auto NOW = std::chrono::seconds(1'700'000'000);
Clock::time_point at_offset(std::chrono::seconds offset)
{
return Clock::time_point(NOW + offset);
}
struct ExpiredRecord
{
std::string label;
HashedId8 id;
std::string name;
Clock::time_point expires_at;
std::string pubkey_hex; // empty for skipped entries
bool skipped;
};
class RecordingExpiredVisitor : public PruneExpiredVisitor
{
public:
void on_deletable(CertificateStorage&, const std::string& label, const HashedId8& id, const Certificate& c,
const std::string& pk) override
{
records.push_back({ label, id, c.get_name(), c.valid_until(), pk, false });
}
void on_skipped(const std::string& label, const HashedId8& id, const Certificate& c) override
{
records.push_back({ label, id, c.get_name(), c.valid_until(), {}, true });
}
void on_summary() override
{
++summary_count;
}
std::vector<ExpiredRecord> records;
std::size_t summary_count = 0;
};
class RecordingOrphansVisitor : public PruneOrphansVisitor
{
public:
void on_orphan(const std::string& hex) override
{
orphans.push_back(hex);
}
void on_summary() override
{
++summary_count;
}
std::vector<std::string> orphans;
std::size_t summary_count = 0;
};
} // anonymous namespace
} // namespace pki
} // namespace vanetza
using namespace vanetza::pki;
TEST_F(PruneTestFixture, prune_expired_picks_certs_with_valid_until_in_past)
{
PublicKey live = make_random_pubkey();
PublicKey dead = make_random_pubkey();
cfg.enrolment_credentials->store(build_stub_certificate(live, nullptr, at_offset(std::chrono::seconds(0)),
std::chrono::hours(1)));
cfg.enrolment_credentials->store(build_stub_certificate(dead, nullptr, at_offset(std::chrono::seconds(-86400)),
std::chrono::hours(1)));
RecordingExpiredVisitor v;
prune_expired(cfg, Clock::time_point(NOW), false, v);
ASSERT_EQ(1u, v.records.size());
EXPECT_FALSE(v.records[0].skipped);
EXPECT_EQ("EC", v.records[0].label);
EXPECT_EQ(vanetza::security::canonical_hexstring(dead), v.records[0].pubkey_hex);
EXPECT_EQ(1u, v.summary_count);
}
TEST_F(PruneTestFixture, prune_expired_distinguishes_store_labels)
{
PublicKey expired_ec = make_random_pubkey();
PublicKey expired_at = make_random_pubkey();
cfg.enrolment_credentials->store(build_stub_certificate(expired_ec, nullptr,
at_offset(std::chrono::seconds(-86400)), std::chrono::hours(1)));
cfg.tickets->store(build_stub_certificate(expired_at, nullptr, at_offset(std::chrono::seconds(-86400)),
std::chrono::hours(1)));
RecordingExpiredVisitor v;
prune_expired(cfg, Clock::time_point(NOW), false, v);
ASSERT_EQ(2u, v.records.size());
std::unordered_set<std::string> labels;
for (const auto& r : v.records) {
labels.insert(r.label);
}
EXPECT_EQ(2u, labels.size());
EXPECT_TRUE(labels.count("EC"));
EXPECT_TRUE(labels.count("AT"));
}
TEST_F(PruneTestFixture, prune_expired_skips_station_referenced_without_force)
{
PublicKey ec_key = make_random_pubkey();
auto ec = build_stub_certificate(ec_key, nullptr, at_offset(std::chrono::seconds(-86400)), std::chrono::hours(1));
HashedId8 ec_id = ec.calculate_hashed_id8(*cfg.security);
cfg.enrolment_credentials->store(ec);
station().set_ec_identifier(ec_id);
RecordingExpiredVisitor v;
prune_expired(cfg, Clock::time_point(NOW), false, v);
ASSERT_EQ(1u, v.records.size());
EXPECT_TRUE(v.records[0].skipped);
EXPECT_EQ(ec_id, v.records[0].id);
}
TEST_F(PruneTestFixture, prune_expired_force_removes_station_referenced)
{
PublicKey ec_key = make_random_pubkey();
auto ec = build_stub_certificate(ec_key, nullptr, at_offset(std::chrono::seconds(-86400)), std::chrono::hours(1));
HashedId8 ec_id = ec.calculate_hashed_id8(*cfg.security);
cfg.enrolment_credentials->store(ec);
station().set_ec_identifier(ec_id);
RecordingExpiredVisitor v;
prune_expired(cfg, Clock::time_point(NOW), false, v); // without force: skipped
ASSERT_EQ(1u, v.records.size());
EXPECT_TRUE(v.records[0].skipped);
RecordingExpiredVisitor v2;
prune_expired(cfg, Clock::time_point(NOW), true, v2); // with force: deletable
ASSERT_EQ(1u, v2.records.size());
EXPECT_FALSE(v2.records[0].skipped);
EXPECT_EQ(ec_id, v2.records[0].id);
}
TEST_F(PruneTestFixture, prune_expired_keeps_not_yet_valid_certs)
{
PublicKey future = make_random_pubkey();
cfg.enrolment_credentials->store(build_stub_certificate(future, nullptr, at_offset(std::chrono::seconds(86400)),
std::chrono::hours(1)));
RecordingExpiredVisitor v;
prune_expired(cfg, Clock::time_point(NOW), false, v);
EXPECT_TRUE(v.records.empty());
EXPECT_EQ(1u, v.summary_count);
}
TEST_F(PruneTestFixture, prune_orphans_emits_only_unreferenced_credentials)
{
PublicKey ec_key = make_random_pubkey();
PublicKey orphan_key = make_random_pubkey();
// EC cert references ec_key; orphan_key has no cert
cfg.enrolment_credentials->store(build_stub_certificate(ec_key));
// Both private keys are in credential storage
cfg.credentials->store(ec_key, PrivateKey {});
cfg.credentials->store(orphan_key, PrivateKey {});
RecordingOrphansVisitor v;
prune_orphans(cfg, v);
ASSERT_EQ(1u, v.orphans.size());
EXPECT_EQ(vanetza::security::canonical_hexstring(orphan_key), v.orphans[0]);
EXPECT_EQ(1u, v.summary_count);
}
TEST_F(PruneTestFixture, prune_orphans_empty_when_all_referenced)
{
PublicKey k = make_random_pubkey();
cfg.enrolment_credentials->store(build_stub_certificate(k));
cfg.credentials->store(k, PrivateKey {});
RecordingOrphansVisitor v;
prune_orphans(cfg, v);
EXPECT_TRUE(v.orphans.empty());
EXPECT_EQ(1u, v.summary_count);
}
TEST_F(PruneTestFixture, prune_orphans_handles_empty_stores)
{
RecordingOrphansVisitor v;
prune_orphans(cfg, v);
EXPECT_TRUE(v.orphans.empty());
EXPECT_EQ(1u, v.summary_count);
}