Files
MicrOBU/obu-firmware/external/vanetza-idf/tools/pki/enrolment.cpp
T
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

387 lines
15 KiB
C++

#include "enrolment.hpp"
#include "asn1.hpp"
#include "certificate.hpp"
#include "certificate_trust_list.hpp"
#include "ea_request.hpp"
#include "ea_response.hpp"
#include "encrypted_data.hpp"
#include "exception.hpp"
#include "filesystem.hpp"
#include "hex_string_validator.hpp"
#include "hexstring.hpp"
#include "http.hpp"
#include "pem.hpp"
#include "response_codes.hpp"
#include "time.hpp"
#include "validation.hpp"
#include <vanetza/common/its_aid.hpp>
#include <CLI/ExtraValidators.hpp>
#include <boost/date_time/posix_time/posix_time_io.hpp>
#include <stdexcept>
namespace vanetza
{
namespace pki
{
namespace
{
struct Context
{
Context(const MainConfig& c) : cfg(c)
{
}
std::string url(const EnrolmentAuthority& ea) const
{
// Some PKIs have EA entry which does not have ITS access point
const std::string& base = ea.its_access_point.empty() ? ea.aa_access_point : ea.its_access_point;
return resolve_url(base, url_override);
}
const MainConfig& cfg;
std::string url_override;
std::function<void()> action;
bool forced = false;
KeyType key_type = KeyType::BrainpoolP256r1;
HashAlgorithm hash_algo = HashAlgorithm::SHA256;
// Manual enrolment: EA certificate and endpoint supplied directly instead of CTL lookup
std::string ea_cert_file;
std::string ea_url;
// Check if manual enolrment mode is engaged
bool manual_enrolment() const { return !ea_cert_file.empty() || !ea_url.empty(); }
struct Bootstrap
{
PrivateKey private_key;
PublicKey public_key;
std::string identifier; // canonical identifier as stored/displayed (text or hex string)
bool identifier_hex = false; // interpret identifier as hex, decoded to bytes for itsId
// itsId bytes for the enrolment request: raw text, or hex decoded to bytes
std::string its_id() const
{
return identifier_hex ? parse_hexstring(identifier) : identifier;
}
};
Bootstrap bootstrap;
};
std::map<std::string, KeyType> key_type_map = { { "NistP256", KeyType::NistP256 },
{ "BrainpoolP256r1", KeyType::BrainpoolP256r1 }, { "BrainpoolP384r1", KeyType::BrainpoolP384r1 } };
std::map<std::string, HashAlgorithm> hash_algo_map = { { "SHA256", HashAlgorithm::SHA256 },
{ "SHA384", HashAlgorithm::SHA384 } };
void check_enrolment_status(Context&);
void perform_initial_enrolment(Context&);
void perform_enrolment_renewal(Context&);
} // namespace
std::shared_ptr<CLI::App> build_enrolment_command(const MainConfig& cfg)
{
auto ctx = std::make_shared<Context>(cfg);
auto app = std::make_shared<CLI::App>("enrolment of ITS station at PKI", "enrolment");
app->alias("enrol");
app->add_option("--url", ctx->url_override, "override the Enrolment Authority URL");
CLI::callback_t canonical_keyfile_cb = [ctx](const CLI::results_t& keyfiles) -> bool {
if (keyfiles.size() != 1) {
return false;
}
auto key = parse_pem_private_key(read(keyfiles[0]));
if (key) {
ctx->bootstrap.private_key = *key;
ctx->bootstrap.public_key = derive_public_key(*key);
return true;
} else {
return false;
}
};
// Manual enrolment options, shared by initial and renewal
auto add_manual_options = [ctx](CLI::App* sub) {
auto ea_cert = sub->add_option("--ea-certificate", ctx->ea_cert_file,
"manual enrolment: EA certificate (OER) to encrypt the request to")
->check(CLI::ExistingFile);
auto ea_url = sub->add_option("--ea-url", ctx->ea_url, "manual enrolment: EA enrolment endpoint URL");
// Manual enrolment needs both; let CLI11 reject a lone --ea-certificate / --ea-url.
ea_cert->needs(ea_url);
ea_url->needs(ea_cert);
};
auto initial = app->add_subcommand("initial", "enrol with initial credentials (bootstrap)");
initial->alias("init");
initial->fallthrough();
initial->add_flag("--force", ctx->forced, "enforce initial enrolment");
// Exactly one of --canonical-id (text) or --canonical-id-hex (hex, decoded to bytes for itsId).
auto canonical_id_group = initial->add_option_group("canonical-id");
canonical_id_group->fallthrough(false); // required because of CLI11 2.6.2 bug
canonical_id_group->add_option("--canonical-id", ctx->bootstrap.identifier, "canonical identifier (text)");
canonical_id_group->add_option("--canonical-id-hex", ctx->bootstrap.identifier,
"canonical identifier (hex, decoded to bytes for itsId)")
->check(HexStringValidator())
->each([ctx](const std::string&) { ctx->bootstrap.identifier_hex = true; });
canonical_id_group->require_option(1);
initial->add_option("--canonical-keyfile", canonical_keyfile_cb, "canonical key (PEM encoded file)")
->required()
->check(CLI::ExistingFile);
initial->add_option("--key-type", ctx->key_type, "type of generated verification key")
->default_val(KeyType::BrainpoolP256r1)
->capture_default_str()
->transform(CLI::CheckedTransformer(key_type_map, CLI::ignore_case));
initial->add_option("--hash-algorithm", ctx->hash_algo, "hash algorithm for signing")
->default_val(HashAlgorithm::SHA256)
->capture_default_str()
->transform(CLI::CheckedTransformer(hash_algo_map, CLI::ignore_case));
add_manual_options(initial);
initial->callback([ctx]() {
ctx->action = [ctx]() {
// will initial enrolment overwrite an existing EC identifier?
auto ec_id = ctx->cfg.station->get_ec_identifier();
if (!ec_id || ctx->forced) {
perform_initial_enrolment(*ctx);
} else {
throw UsageError("EC identifier is already set", "pass --force to overwrite");
}
};
});
auto renewal = app->add_subcommand("renewal", "renew enrolment credential (re-keying)");
renewal->alias("renew");
renewal->fallthrough();
renewal->add_option("--key-type", ctx->key_type, "type of generated verification key")
->default_val(KeyType::BrainpoolP256r1)
->capture_default_str()
->transform(CLI::CheckedTransformer(key_type_map, CLI::ignore_case));
renewal->add_option("--hash-algorithm", ctx->hash_algo, "hash algorithm for signing")
->default_val(HashAlgorithm::SHA256)
->capture_default_str()
->transform(CLI::CheckedTransformer(hash_algo_map, CLI::ignore_case));
add_manual_options(renewal);
renewal->callback([ctx]() { ctx->action = [ctx]() { perform_enrolment_renewal(*ctx); }; });
auto status = app->add_subcommand("status", "show enrolment status");
status->callback([ctx]() { ctx->action = [ctx]() { check_enrolment_status(*ctx); }; });
app->final_callback([ctx]() {
if (ctx->action) {
ctx->action();
} else {
check_enrolment_status(*ctx);
}
});
return app;
}
namespace
{
void check_enrolment_status(Context& ctx)
{
// has EC identifier set?
boost::optional<HashedId8> ec_id = ctx.cfg.station->get_ec_identifier();
// has EC certificate stored?
boost::optional<Certificate> ec_cert;
if (ec_id) {
ec_cert = ctx.cfg.enrolment_credentials->fetch(*ec_id);
}
// can station sign with EC certificate (i.e., is the private key available)?
bool can_sign = false;
if (ec_cert) {
can_sign = ctx.cfg.security->can_sign(ec_cert->get_public_key());
}
auto canonical_identifier = ctx.cfg.station->get_canonical_identifier();
std::cout << "Canonical identifier: " << (canonical_identifier.empty() ? "[unknown]" : canonical_identifier)
<< "\n";
std::cout << "EC identifier: " << (ec_id ? hexstring(*ec_id) : "[unknown]") << "\n";
std::cout << "EC certificate: " << (ec_cert ? (ec_cert->get_name() + " [available]") : "[unavailable]") << "\n";
if (ec_cert) {
Clock::time_point start = ec_cert->valid_since();
Clock::time_point stop = ec_cert->valid_until();
std::cout << "EC certificate validity: " << Clock::at(start) << " until " << Clock::at(stop);
Clock::time_point now = current_time();
if (now >= start && now <= stop) {
std::cout << " [valid now]\n";
if (now + std::chrono::hours(24 * 7 * 12) > stop) {
std::cout << "!! EC certificate expires in less than 12 weeks, you should renew it !!\n";
}
} else if (now < start) {
std::cout << " [not yet valid]\n";
} else {
std::cout << " [expired]\n";
}
}
std::cout << "EC suitable for signing: " << (can_sign ? "yes" : "no") << "\n";
}
EnrolmentAuthority lookup_enrolment_authority(Context& ctx)
{
std::cout << "Root CA " << hexstring(ctx.cfg.root_ca_hid8) << "\n";
auto processor = process_stored_ctl(*ctx.cfg.trust_lists, ctx.cfg.security, ctx.cfg.root_ca_hid8);
return require_enrolment_authority(processor, ctx.cfg.root_ca_hid8);
}
/**
* \brief Build the EnrolmentAuthority from the manually supplied --ea-certificate / --ea-url.
*
* Lets a station enrol against a PKI whose DC offers no getctl, where trust comes from an
* out-of-band bootstrap bundle rather than a stored CTL. The certificate must carry an encryption
* key because the request is ECIES-encrypted to it.
*/
EnrolmentAuthority build_enrolment_authority(Context& ctx)
{
Certificate cert;
if (!cert.decode(read(ctx.ea_cert_file))) {
throw DecodingFailure("could not decode EA certificate");
}
if (!cert.get_encryption_key()) {
throw UsageError("missing encryption key in EA certificate");
}
EnrolmentAuthority ea;
ea.certificate = std::move(cert);
ea.its_access_point = ctx.ea_url;
std::cout << "Manual EA " << hexstring(ea.certificate.calculate_hashed_id8(*ctx.cfg.security)) << " at "
<< ctx.ea_url << "\n";
return ea;
}
/**
* \brief Shared request/response round-trip for initial enrolment and renewal.
*
* Builds the EA-encrypted request, POSTs it, decrypts/parses the response,
* stores the returned EC, sets the station's active EC identifier, and commits
* the new verification key pair.
* \throws on any failure along the way
*/
void perform_enrolment(Context& ctx, const EnrolmentRequestParameters& params, const EnrolmentAuthority& ea,
ScopedKeyPair& scoped_verification_key)
{
HashedId8 ea_cert_hid8 = ea.certificate.calculate_hashed_id8(*ctx.cfg.security);
std::cout << "Enroling against EA certificate: " << hexstring(ea_cert_hid8) << "\n";
EncryptedData encrypted_data = build_enrolment_request(*ctx.cfg.security, params, ea.certificate);
const std::string ea_url = ctx.url(ea);
std::cout << "Enroling at EA URL: " << ea_url << "\n";
auto query = HttpQuery::from_url(ea_url);
auto encoded_encrypted_data = encrypted_data.encode();
auto sha256_encrypted_data =
ctx.cfg.security->calculate_sha256_hash(encoded_encrypted_data.data(), encoded_encrypted_data.size());
auto response = http_post(query, "application/x-its-request", encoded_encrypted_data);
if (response.result() != boost::beast::http::status::ok) {
throw HttpException("EA returned an unexpected HTTP status for the enrolment request",
std::move(response));
} else if (response[boost::beast::http::field::content_type] != "application/x-its-response") {
throw HttpException("expected application/x-its-response");
}
if (!encrypted_data.decode(response.body().data(), response.body().size())) {
throw DecodingFailure("decoding encrypted response failed");
}
ByteBuffer dec_resp = encrypted_data.decrypt();
EnrolmentResponse ec_resp = parse_enrolment_response(*ctx.cfg.security, dec_resp, ea.certificate);
if (!check_request_hash(sha256_encrypted_data, ec_resp.request_hash)) {
throw VerificationFailure("mismatch between request and response hash");
}
if (ec_resp.code != Vanetza_Security_EnrolmentResponseCode_ok) {
throw std::runtime_error("inner EC response code: " + vanetza::pki::to_string(ec_resp.code));
}
Certificate& ec = *ec_resp.certificate;
auto ec_hid8 = ec.calculate_hashed_id8(*ctx.cfg.security);
scoped_verification_key.commit();
ctx.cfg.enrolment_credentials->store(ec);
ctx.cfg.station->set_ec_identifier(ec_hid8);
std::cout << "Stored new EC with HashedId8 = " << hexstring(ec_hid8) << "\n";
// safety check
if (ctx.cfg.security->can_sign(ec.get_public_key())) {
std::cout << "Station can sign with received EC\n";
} else {
std::cerr << "Station cannot sign with received EC\n";
}
}
void perform_initial_enrolment(Context& ctx)
{
EnrolmentAuthority ea = ctx.manual_enrolment() ? build_enrolment_authority(ctx) : lookup_enrolment_authority(ctx);
// security module stores created private key in its credential storage
ScopedKeyPair scoped_verification_key(*ctx.cfg.security, ctx.key_type);
// set permanent canonical identifier
ctx.cfg.station->set_canonical_identifier(ctx.bootstrap.identifier);
std::cout << "Canonical identifier: " << ctx.bootstrap.identifier << "\n";
// load bootstrap keys (only for this scope)
ScopedCredential scoped_bootstrap_keys { *ctx.cfg.credentials, ctx.bootstrap.public_key,
ctx.bootstrap.private_key };
std::cout << "Canonical public key X=" << hexstring(ctx.bootstrap.public_key.x);
if (!ctx.bootstrap.public_key.y.empty()) {
std::cout << " Y=" << hexstring(ctx.bootstrap.public_key.y);
}
std::cout << "\n";
EnrolmentRequestParameters params;
params.its_id = ctx.bootstrap.its_id();
params.verification_key = scoped_verification_key.public_key();
params.outer_signer_key = ctx.bootstrap.public_key;
params.hash_algo = ctx.hash_algo;
perform_enrolment(ctx, params, ea, scoped_verification_key);
}
void perform_enrolment_renewal(Context& ctx)
{
auto ec_id = ctx.cfg.station->get_ec_identifier();
if (!ec_id) {
throw UsageError("no EC to renew", "run 'enrolment initial' first");
}
auto ec_cert = ctx.cfg.enrolment_credentials->fetch(*ec_id);
if (!ec_cert) {
throw UsageError("EC certificate missing from storage", "re-run 'enrolment initial'");
}
PublicKey ec_key = ec_cert->get_public_key();
if (!ctx.cfg.security->can_sign(ec_key)) {
throw UsageError("no EC private key available", "re-run 'enrolment initial'");
}
EnrolmentAuthority ea = ctx.manual_enrolment() ? build_enrolment_authority(ctx) : lookup_enrolment_authority(ctx);
// Fresh verification key for the new EC
// TS 102 941 §6.2.3.2.1 mandates a new key pair on every enrolment request).
ScopedKeyPair scoped_verification_key(*ctx.cfg.security, ctx.key_type);
std::cout << "Renewing EC " << hexstring(*ec_id) << "\n";
EnrolmentRequestParameters params;
// TS 102 941 §6.2.3.2.1 itsId carries the raw 8 bytes of HashedId8(current EC).
params.its_id.assign(ec_id->octets.begin(), ec_id->octets.end());
params.verification_key = scoped_verification_key.public_key();
params.outer_signer_key = ec_key;
params.outer_signer_certificate = &ec_cert.value();
params.hash_algo = ctx.hash_algo;
perform_enrolment(ctx, params, ea, scoped_verification_key);
}
} // namespace
} // namespace pki
} // namespace vanetza