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

424 lines
17 KiB
C++

#include "certificate_builder.hpp"
#include "files.hpp"
#include <vanetza/common/manual_runtime.hpp>
#include <vanetza/security/backend.hpp>
#include <vanetza/security/pqc/fndsa512.hpp>
#include <vanetza/security/pqc/hybrid_certificate.hpp>
#include <vanetza/security/pqc/hybrid_certificate_validator.hpp>
#include <vanetza/security/v3/issuer_memory_lookup.hpp>
#include <vanetza/security/v3/trust_store.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/program_options.hpp>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
namespace po = boost::program_options;
using namespace vanetza;
using namespace vanetza::security;
using namespace vanetza::security::v3;
namespace builder = vanetza::security::pqc::certificate_builder;
namespace
{
enum class CertificateProfile
{
Ecc,
Hybrid
};
CertificateProfile parse_profile(const std::string& profile)
{
if (profile == "ecc") {
return CertificateProfile::Ecc;
}
if (profile == "hybrid") {
return CertificateProfile::Hybrid;
}
throw std::invalid_argument("unknown certificate profile: " + profile);
}
void validate_pqc_argument(
CertificateProfile profile, const std::string& value, const char* option)
{
if (profile == CertificateProfile::Hybrid && value.empty()) {
throw std::invalid_argument(std::string(option) + " is required for the hybrid profile");
}
if (profile == CertificateProfile::Ecc && !value.empty()) {
throw std::invalid_argument(std::string(option) + " is not valid for the ECC profile");
}
}
Clock::time_point now()
{
return Clock::at(boost::posix_time::microsec_clock::universal_time());
}
std::vector<ItsAid> convert_aids(const std::vector<unsigned>& input)
{
return std::vector<ItsAid>(input.begin(), input.end());
}
std::vector<std::string> command_arguments(int argc, const char* argv[])
{
return std::vector<std::string>(argv + 2, argv + argc);
}
po::variables_map parse(
const std::vector<std::string>& arguments, const po::options_description& options,
const po::positional_options_description& positional = {})
{
po::variables_map variables;
po::store(po::command_line_parser(arguments)
.options(options).positional(positional).run(), variables);
if (!variables.count("help")) {
po::notify(variables);
}
return variables;
}
int generate_key(const std::vector<std::string>& arguments)
{
std::string output;
po::options_description options("Generate an FN-DSA-512 key pair");
options.add_options()
("help,h", "Show this help")
("output", po::value<std::string>(&output)->required(),
"Output base path; .pqc.key and .pqc.pub are appended");
po::positional_options_description positional;
positional.add("output", 1);
const auto variables = parse(arguments, options, positional);
if (variables.count("help")) {
std::cout << options << '\n';
return 0;
}
auto backend = pqc::create_fndsa512_backend();
save_pqc_key_pair(output, backend->generate_key_pair());
std::cout << "Wrote " << output << ".pqc.key and " << output << ".pqc.pub\n";
return 0;
}
builder::CertificateParameters certificate_parameters(
const std::string& name, int days, const std::vector<unsigned>& aids)
{
builder::CertificateParameters parameters;
parameters.subject_name = name;
parameters.validity_days = days;
parameters.application_ids = convert_aids(aids);
return parameters;
}
int generate_root(const std::vector<std::string>& arguments)
{
std::string output;
std::string subject_key;
std::string subject_pqc_key;
std::string profile_name = "hybrid";
std::string subject_name = "Hello World Root-CA";
int days = 365;
std::vector<unsigned> aids;
po::options_description options("Generate a V3 Root CA certificate");
options.add_options()
("help,h", "Show this help")
("output,o", po::value<std::string>(&output)->required(), "Output certificate")
("subject-key", po::value<std::string>(&subject_key)->required(), "ECC private key")
("profile", po::value<std::string>(&profile_name)->default_value("hybrid"),
"Certificate profile: ecc or hybrid")
("subject-pqc-key", po::value<std::string>(&subject_pqc_key),
"FN-DSA key base path")
("subject-name", po::value<std::string>(&subject_name), "Certificate subject name")
("days", po::value<int>(&days), "Validity in days")
("aid", po::value<std::vector<unsigned>>(&aids)->multitoken(), "Permitted ITS-AIDs");
const auto variables = parse(arguments, options);
if (variables.count("help")) {
std::cout << options << '\n';
return 0;
}
const auto profile = parse_profile(profile_name);
validate_pqc_argument(profile, subject_pqc_key, "--subject-pqc-key");
auto ecc_backend = create_backend_or_throw("default");
const auto subject_ecc = load_ecc_key_pair(subject_key);
Certificate certificate;
if (profile == CertificateProfile::Hybrid) {
auto pqc_backend = pqc::create_fndsa512_backend();
certificate = builder::build_hybrid_root_certificate(
*ecc_backend, *pqc_backend, subject_ecc, load_pqc_key_pair(subject_pqc_key),
certificate_parameters(subject_name, days, aids), now());
} else {
certificate = builder::build_ecc_root_certificate(
*ecc_backend, subject_ecc,
certificate_parameters(subject_name, days, aids), now());
}
save_v3_certificate(output, certificate);
std::cout << "Wrote " << profile_name << " V3 Root CA certificate " << output << '\n';
return 0;
}
int generate_aa(const std::vector<std::string>& arguments)
{
std::string output;
std::string sign_key;
std::string sign_cert;
std::string sign_pqc_key;
std::string subject_key;
std::string subject_pqc_key;
std::string profile_name = "hybrid";
std::string subject_name = "Hello World Auth-CA";
int days = 180;
std::vector<unsigned> aids;
po::options_description options("Generate a V3 Authorization Authority certificate");
options.add_options()
("help,h", "Show this help")
("output,o", po::value<std::string>(&output)->required(), "Output certificate")
("sign-key", po::value<std::string>(&sign_key)->required(), "Issuer ECC private key")
("sign-cert", po::value<std::string>(&sign_cert)->required(), "Issuer certificate")
("profile", po::value<std::string>(&profile_name)->default_value("hybrid"),
"Certificate profile: ecc or hybrid")
("sign-pqc-key", po::value<std::string>(&sign_pqc_key),
"Issuer FN-DSA key base path")
("subject-key", po::value<std::string>(&subject_key)->required(), "Subject ECC private key")
("subject-pqc-key", po::value<std::string>(&subject_pqc_key),
"Subject FN-DSA key base path")
("subject-name", po::value<std::string>(&subject_name), "Certificate subject name")
("days", po::value<int>(&days), "Validity in days")
("aid", po::value<std::vector<unsigned>>(&aids)->multitoken(), "Permitted ITS-AIDs");
const auto variables = parse(arguments, options);
if (variables.count("help")) {
std::cout << options << '\n';
return 0;
}
const auto profile = parse_profile(profile_name);
validate_pqc_argument(profile, sign_pqc_key, "--sign-pqc-key");
validate_pqc_argument(profile, subject_pqc_key, "--subject-pqc-key");
auto ecc_backend = create_backend_or_throw("default");
const auto subject_ecc = load_ecc_key_pair(subject_key);
const auto issuer_ecc = load_ecc_key_pair(sign_key);
const auto issuer_certificate = load_v3_certificate(sign_cert);
Certificate certificate;
if (profile == CertificateProfile::Hybrid) {
auto pqc_backend = pqc::create_fndsa512_backend();
const auto subject_pqc = load_pqc_key_pair(subject_pqc_key);
certificate = builder::build_hybrid_authorization_authority_certificate(
*ecc_backend, *pqc_backend, issuer_ecc,
load_pqc_private_key(sign_pqc_key), issuer_certificate,
subject_ecc.public_key, subject_pqc.public_key,
certificate_parameters(subject_name, days, aids), now());
} else {
certificate = builder::build_ecc_authorization_authority_certificate(
*ecc_backend, issuer_ecc, issuer_certificate, subject_ecc.public_key,
certificate_parameters(subject_name, days, aids), now());
}
save_v3_certificate(output, certificate);
std::cout << "Wrote " << profile_name
<< " V3 Authorization Authority certificate " << output << '\n';
return 0;
}
int generate_ticket(const std::vector<std::string>& arguments)
{
std::string output;
std::string sign_key;
std::string sign_cert;
std::string sign_pqc_key;
std::string subject_key;
std::string profile_name = "hybrid";
int days = 7;
std::vector<unsigned> aids;
po::options_description options("Generate a V3 Authorization Ticket");
options.add_options()
("help,h", "Show this help")
("output,o", po::value<std::string>(&output)->required(), "Output certificate")
("sign-key", po::value<std::string>(&sign_key)->required(), "Issuer ECC private key")
("sign-cert", po::value<std::string>(&sign_cert)->required(), "Issuer certificate")
("profile", po::value<std::string>(&profile_name)->default_value("hybrid"),
"Certificate profile: ecc or hybrid")
("sign-pqc-key", po::value<std::string>(&sign_pqc_key),
"Issuer FN-DSA key base path")
("subject-key", po::value<std::string>(&subject_key)->required(), "Subject ECC private key")
("days", po::value<int>(&days), "Validity in days")
("aid", po::value<std::vector<unsigned>>(&aids)->multitoken(), "Permitted ITS-AIDs");
const auto variables = parse(arguments, options);
if (variables.count("help")) {
std::cout << options << '\n';
return 0;
}
const auto profile = parse_profile(profile_name);
validate_pqc_argument(profile, sign_pqc_key, "--sign-pqc-key");
auto ecc_backend = create_backend_or_throw("default");
const auto issuer_ecc = load_ecc_key_pair(sign_key);
const auto issuer_certificate = load_v3_certificate(sign_cert);
const auto subject_ecc = load_ecc_key_pair(subject_key);
Certificate certificate;
if (profile == CertificateProfile::Hybrid) {
auto pqc_backend = pqc::create_fndsa512_backend();
certificate = builder::build_hybrid_authorization_ticket(
*ecc_backend, *pqc_backend, issuer_ecc,
load_pqc_private_key(sign_pqc_key), issuer_certificate,
subject_ecc.public_key, certificate_parameters({}, days, aids), now());
} else {
certificate = builder::build_ecc_authorization_ticket(
*ecc_backend, issuer_ecc, issuer_certificate, subject_ecc.public_key,
certificate_parameters({}, days, aids), now());
}
save_v3_certificate(output, certificate);
std::cout << "Wrote " << profile_name << " V3 Authorization Ticket " << output << '\n';
return 0;
}
const char* material_state(pqc::MaterialState state)
{
switch (state) {
case pqc::MaterialState::None: return "none";
case pqc::MaterialState::Authority: return "authority (key + signature)";
case pqc::MaterialState::EndEntity: return "end entity (signature only)";
case pqc::MaterialState::Inconsistent: return "inconsistent";
}
return "unknown";
}
int show_certificate(const std::vector<std::string>& arguments)
{
std::string input;
po::options_description options("Show V3 certificate information");
options.add_options()
("help,h", "Show this help")
("certificate", po::value<std::string>(&input)->required(), "Certificate file");
po::positional_options_description positional;
positional.add("certificate", 1);
const auto variables = parse(arguments, options, positional);
if (variables.count("help")) {
std::cout << options << '\n';
return 0;
}
const auto certificate = load_v3_certificate(input);
const auto key = pqc::get_alternative_public_key(certificate);
const auto signature = pqc::get_alternative_signature(certificate);
std::cout << "Encoded size: " << certificate.encode().size() << " bytes\n"
<< "Alternative material: "
<< material_state(pqc::alternative_material_state(certificate)) << "\n"
<< "FN-DSA-512 public key: " << (key ? key->bytes.size() : 0) << " bytes\n"
<< "FN-DSA-512 signature: " << (signature ? signature->bytes.size() : 0)
<< " bytes\n";
return 0;
}
int verify_chain(const std::vector<std::string>& arguments)
{
std::string root_path;
std::string aa_path;
std::string ticket_path;
std::string profile_name = "hybrid";
unsigned application_id = aid::CA;
po::options_description options("Verify a V3 Root -> AA -> AT chain");
options.add_options()
("help,h", "Show this help")
("root", po::value<std::string>(&root_path)->required(), "Trusted Root certificate")
("aa", po::value<std::string>(&aa_path)->required(), "Authorization Authority certificate")
("ticket", po::value<std::string>(&ticket_path)->required(), "Authorization Ticket certificate")
("profile", po::value<std::string>(&profile_name)->default_value("hybrid"),
"Expected profile: ecc or hybrid")
("aid", po::value<unsigned>(&application_id), "ITS-AID to validate (default: 36)");
const auto variables = parse(arguments, options);
if (variables.count("help")) {
std::cout << options << '\n';
return 0;
}
const auto root = load_v3_certificate(root_path);
const auto aa = load_v3_certificate(aa_path);
const auto ticket = load_v3_certificate(ticket_path);
const auto profile = parse_profile(profile_name);
if (profile == CertificateProfile::Ecc &&
(pqc::alternative_material_state(root) != pqc::MaterialState::None ||
pqc::alternative_material_state(aa) != pqc::MaterialState::None ||
pqc::alternative_material_state(ticket) != pqc::MaterialState::None)) {
throw std::invalid_argument("ECC profile chain contains alternative PQC material");
}
auto ecc_backend = create_backend_or_throw("default");
auto pqc_backend = pqc::create_fndsa512_backend();
TrustStore trust_store;
trust_store.insert(root);
IssuerMemoryLookup issuer_lookup;
if (!issuer_lookup.insert(root) || !issuer_lookup.insert(aa)) {
throw std::runtime_error("Root or AA cannot be used as an issuer certificate");
}
ManualRuntime runtime(now());
pqc::HybridCertificateValidator validator;
validator.use_runtime(&runtime);
validator.disable_location_checks(true);
validator.use_issuer_lookup(&issuer_lookup);
validator.use_trust_store(&trust_store);
validator.use_backends(ecc_backend.get(), pqc_backend.get());
validator.use_verification_policy(profile == CertificateProfile::Hybrid ?
pqc::HybridCertificateValidator::VerificationPolicy::HybridRequired :
pqc::HybridCertificateValidator::VerificationPolicy::HybridIfPresent);
const auto verdict = validator.valid_for_signing(ticket, application_id);
if (verdict != CertificateValidator::Verdict::Valid) {
std::cerr << profile_name << " chain verification failed (verdict "
<< static_cast<int>(verdict) << ")\n";
return 1;
}
std::cout << profile_name << " V3 Root -> AA -> AT chain is valid\n";
return 0;
}
void print_usage(const char* executable)
{
std::cout << "Usage: " << executable << " COMMAND [OPTIONS]\n\n"
<< "Commands:\n"
<< " generate-key Generate an FN-DSA-512 key pair\n"
<< " generate-root Generate a V3 Root CA\n"
<< " generate-aa Generate a V3 Authorization Authority\n"
<< " generate-ticket Generate a V3 Authorization Ticket\n"
<< " show Show V3 certificate material\n"
<< " verify-chain Verify a Root -> AA -> AT chain\n";
}
} // namespace
int main(int argc, const char* argv[])
{
try {
if (argc < 2 || std::string(argv[1]) == "--help" || std::string(argv[1]) == "-h") {
print_usage(argv[0]);
return argc < 2 ? 1 : 0;
}
const std::string command = argv[1];
const auto arguments = command_arguments(argc, argv);
if (command == "generate-key") {
return generate_key(arguments);
}
if (command == "generate-root") {
return generate_root(arguments);
}
if (command == "generate-aa") {
return generate_aa(arguments);
}
if (command == "generate-ticket") {
return generate_ticket(arguments);
}
if (command == "show") {
return show_certificate(arguments);
}
if (command == "verify-chain") {
return verify_chain(arguments);
}
throw std::invalid_argument("unknown command: " + command);
} catch (const std::exception& error) {
std::cerr << "Error: " << error.what() << '\n';
return 1;
}
}