Keep the colleague's microbu-esp32c5 tree in this repository
obu-firmware builds against vanetza-idf from microbu-esp32c5/external, but that tree was gitignored, so a clone of this repository could not build the firmware it ships. It is now committed here as ordinary files in its own folder, microbu-esp32c5/: the colleague's commit cf4b99f plus the V2X2MAP bridge's signature verification (--trust) used on the bench. Nothing is fetched from or pushed to the colleague's repository; this repository and its remotes carry everything. The folder's own .gitignore keeps build output, downloaded components and private key material out, as it did there; the committed file set is identical to that repository's tracked files. The ESP32-C5 is still flashed from obu-firmware/, which only takes vanetza-idf from microbu-esp32c5/, so the two stay separate folders. FLASHING.md says how to take a newer version of the colleague's tree (copy it over the folder, rebuild, test, commit).
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
#include "cpoc.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "certificate_storage.hpp"
|
||||
#include "certificate_trust_list.hpp"
|
||||
#include "exception.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "hashed_id8_validator.hpp"
|
||||
#include "http.hpp"
|
||||
#include "time.hpp"
|
||||
#include "validation.hpp"
|
||||
#include <boost/beast/http/field.hpp>
|
||||
#include <boost/date_time/posix_time/posix_time_io.hpp>
|
||||
#include <functional>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct Context
|
||||
{
|
||||
Context(const MainConfig& c) : cfg(c)
|
||||
{
|
||||
}
|
||||
|
||||
std::filesystem::path ectl_file() const
|
||||
{
|
||||
return cfg.data_path / "ectl.ctl";
|
||||
}
|
||||
|
||||
const MainConfig& cfg;
|
||||
std::string cpoc_url = "https://cpoc.jrc.ec.europa.eu/L0";
|
||||
HashedId8 hid8;
|
||||
std::function<void()> action;
|
||||
};
|
||||
|
||||
const HashedId8Validator hid8_validator;
|
||||
|
||||
Certificate fetch_tlm_certificate(const std::string& base_url, const HashedId8* id)
|
||||
{
|
||||
auto query = HttpQuery::from_url(base_url + "/gettlmcertificate/" + (id ? hexstring(*id) : ""));
|
||||
auto response = http_get(query);
|
||||
if (response.result() != boost::beast::http::status::ok) {
|
||||
throw HttpException("CPOC returned an unexpected HTTP status when fetching TLM certificate",
|
||||
std::move(response));
|
||||
} else if (response[boost::beast::http::field::content_type] != "application/octet-stream") {
|
||||
throw HttpException("did not receive bytes from CPOC when fetching TLM certificate");
|
||||
} else {
|
||||
Certificate cert;
|
||||
if (!cert.decode(response.body())) {
|
||||
throw DecodingFailure("decoding received TLM certificate failed");
|
||||
} else {
|
||||
return cert;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boost::optional<std::tuple<Certificate, HashedId8>> lookup_default_tlm_certificate(const Context& context)
|
||||
{
|
||||
const CertificateStorage& storage = *context.cfg.tlm;
|
||||
boost::optional<std::tuple<Certificate, HashedId8>> match;
|
||||
Clock::time_point now = current_time();
|
||||
|
||||
for (const HashedId8& hid8 : storage.list()) {
|
||||
boost::optional<Certificate> candidate = storage.fetch(hid8);
|
||||
if (candidate && is_currently_valid(*candidate, now)) {
|
||||
if (!match) {
|
||||
match = std::make_tuple(*candidate, hid8);
|
||||
} else if (std::get<0>(*match).valid_since() < candidate->valid_since()) {
|
||||
match = std::make_tuple(*candidate, hid8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
void list_tlm(Context& context)
|
||||
{
|
||||
const Clock::time_point now = current_time();
|
||||
std::cout << "Trusted TLM certificates are:\n";
|
||||
for (const HashedId8& hid8 : context.cfg.tlm->list()) {
|
||||
boost::optional<Certificate> cert = context.cfg.tlm->fetch(hid8);
|
||||
if (!cert) {
|
||||
std::cout << "- " << hexstring(hid8) << "\n";
|
||||
continue;
|
||||
}
|
||||
const std::string name = cert->get_name();
|
||||
std::cout << "- " << (name.empty() ? hexstring(hid8) : name + " (" + hexstring(hid8) + ")") << "\n";
|
||||
const char* status = "valid";
|
||||
if (now < cert->valid_since()) {
|
||||
status = "not yet valid";
|
||||
} else if (now > cert->valid_until()) {
|
||||
status = "expired";
|
||||
}
|
||||
std::cout << " |-> valid from " << Clock::at(cert->valid_since()) << " until "
|
||||
<< Clock::at(cert->valid_until()) << " [" << status << "]\n";
|
||||
}
|
||||
}
|
||||
|
||||
void fetch_tlm(Context& context, const HashedId8* id, bool dry_run)
|
||||
{
|
||||
Certificate tlm = fetch_tlm_certificate(context.cpoc_url, id);
|
||||
const HashedId8 tlm_hid8 = tlm.calculate_hashed_id8(*context.cfg.security);
|
||||
if (id && *id != tlm_hid8) {
|
||||
throw VerificationFailure("fetched TLM certificate's HashedId8 does not match requested HashedId8");
|
||||
}
|
||||
const std::string name = tlm.get_name();
|
||||
const std::string hex_hid8 = hexstring(tlm_hid8);
|
||||
if (dry_run) {
|
||||
std::cout << "CPOC can provide TLM certificate "
|
||||
<< (name.empty() ? hex_hid8 : "\"" + name + "\" (" + hex_hid8 + ")")
|
||||
<< ". Storage unchanged in this dry run.\n";
|
||||
return;
|
||||
}
|
||||
context.cfg.tlm->store(tlm);
|
||||
if (name.empty()) {
|
||||
std::cout << "Added TLM certificate (" << hex_hid8 << ")\n";
|
||||
} else {
|
||||
std::cout << "Added TLM certificate \"" << name << "\" (" << hex_hid8 << ")\n";
|
||||
}
|
||||
}
|
||||
|
||||
void discard_tlm(Context& context, const HashedId8& id)
|
||||
{
|
||||
if (context.cfg.tlm->erase(id)) {
|
||||
std::cout << "Removed " << hexstring(id) << " from trusted TLM certificates\n";
|
||||
} else {
|
||||
std::cout << "No TLM certificate with " << hexstring(id) << " found in local storage\n";
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<CLI::App> build_tlm_command(std::shared_ptr<Context> context)
|
||||
{
|
||||
auto app = std::make_shared<CLI::App>("Trust List Manager", "tlm");
|
||||
|
||||
auto list = app->add_subcommand("list", "list all trusted TLM certificates");
|
||||
list->callback([context]() { context->action = [context]() { list_tlm(*context); }; });
|
||||
|
||||
auto fetch = app->add_subcommand("fetch", "fetch a TLM certificate from CPOC (omit the id to fetch the latest)");
|
||||
auto fetch_id =
|
||||
fetch->add_option("hid8", context->hid8, "HashedId8 of the TLM certificate to fetch; omit for the latest")
|
||||
->check(hid8_validator);
|
||||
auto fetch_dry = fetch->add_flag("--dry-run,-n", "do not store the fetched certificate");
|
||||
fetch->callback([context, fetch_id, fetch_dry]() {
|
||||
const bool has_id = fetch_id->count() > 0;
|
||||
const bool dry_run = fetch_dry->as<bool>();
|
||||
context->action = [context, has_id, dry_run]() {
|
||||
fetch_tlm(*context, has_id ? &context->hid8 : nullptr, dry_run);
|
||||
};
|
||||
});
|
||||
|
||||
auto discard = app->add_subcommand("discard", "discard a TLM certificate (distrust it)");
|
||||
discard->add_option("hid8", context->hid8, "HashedId8 of the TLM certificate to discard")
|
||||
->required()
|
||||
->check(hid8_validator);
|
||||
discard->callback([context]() { context->action = [context]() { discard_tlm(*context, context->hid8); }; });
|
||||
|
||||
app->final_callback([context]() {
|
||||
if (!context->action) {
|
||||
context->action = [context]() { list_tlm(*context); };
|
||||
}
|
||||
context->action();
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
void list_ectl(Context& context)
|
||||
{
|
||||
CertificateTrustList ctl = CertificateTrustList::from_file(context.ectl_file());
|
||||
std::cout << "ECTL contains:\n";
|
||||
CtlListingVisitor visitor(*context.cfg.security);
|
||||
ctl.visit_tlm_ctl(visitor);
|
||||
}
|
||||
|
||||
// tlm_id == nullptr -> use the locally known latest TLM certificate.
|
||||
void fetch_ectl(Context& context, const HashedId8* tlm_id, bool dry_run)
|
||||
{
|
||||
CertificateStorage& storage = *context.cfg.tlm;
|
||||
|
||||
HashedId8 tlm_hid8;
|
||||
if (tlm_id) {
|
||||
tlm_hid8 = *tlm_id;
|
||||
} else {
|
||||
auto lookup = lookup_default_tlm_certificate(context);
|
||||
if (!lookup) {
|
||||
throw UsageError("no TLM certificate found", "run 'cpoc tlm fetch' first");
|
||||
}
|
||||
tlm_hid8 = std::get<1>(*lookup);
|
||||
}
|
||||
|
||||
struct UpdateVisitor : CtlVisitor
|
||||
{
|
||||
UpdateVisitor(std::shared_ptr<CertificateStorage> certs) : certificates(certs)
|
||||
{
|
||||
}
|
||||
|
||||
void add_root_ca(const Vanetza_Security_RootCaEntry_t& rca) override
|
||||
{
|
||||
Certificate root_ca { rca.selfsignedRootCa };
|
||||
certificates->store(root_ca);
|
||||
}
|
||||
|
||||
std::shared_ptr<CertificateStorage> certificates;
|
||||
};
|
||||
|
||||
auto query = HttpQuery::from_url(context.cpoc_url + "/getectl/" + hexstring(tlm_hid8));
|
||||
auto response = http_get(query);
|
||||
if (response.result() != boost::beast::http::status::ok) {
|
||||
throw HttpException("CPOC returned an unexpected HTTP status when fetching full ECTL",
|
||||
std::move(response));
|
||||
} else if (response[boost::beast::http::field::content_type] != "application/octet-stream") {
|
||||
throw HttpException("did not receive bytes from CPOC when fetching full ECTL");
|
||||
}
|
||||
|
||||
CertificateTrustList tlm_message;
|
||||
if (!tlm_message.decode(response.body())) {
|
||||
throw DecodingFailure("decoding received TLM certificate list message failed");
|
||||
} else if (const Vanetza_Security_SignedData_t* sdata = get_signed_data(tlm_message.raw())) {
|
||||
const Vanetza_Security_EtsiTs103097Certificate_t* ectl_certificate = nullptr;
|
||||
if (sdata->signer.present == Vanetza_Security_SignerIdentifier_PR_digest) {
|
||||
if (!equals(sdata->signer.choice.digest, tlm_hid8)) {
|
||||
throw VerificationFailure("expected a different HashedId8 digest in response message");
|
||||
}
|
||||
} else if (sdata->signer.present == Vanetza_Security_SignerIdentifier_PR_certificate) {
|
||||
const Vanetza_Security_SequenceOfCertificate& certlist = sdata->signer.choice.certificate;
|
||||
if (certlist.list.count >= 1) {
|
||||
ectl_certificate = certlist.list.array[0];
|
||||
HashedId8 cert_hid8 = calculate_hashed_id8(*context.cfg.security, *ectl_certificate);
|
||||
if (cert_hid8 != tlm_hid8) {
|
||||
throw VerificationFailure("signing certificate's digest does not match requested HashedId8");
|
||||
}
|
||||
} else {
|
||||
throw DecodingFailure("missing certificate used for signing");
|
||||
}
|
||||
} else {
|
||||
throw VerificationFailure("received ECTL message is not signed by expected TLM HashedId8");
|
||||
}
|
||||
|
||||
boost::optional<Certificate> stored_tlm = storage.fetch(tlm_hid8);
|
||||
if (!stored_tlm) {
|
||||
if (!ectl_certificate) {
|
||||
throw UsageError("missing TLM certificate to verify ECTL", "run 'cpoc tlm fetch' first");
|
||||
}
|
||||
stored_tlm = Certificate(*ectl_certificate);
|
||||
if (!dry_run) {
|
||||
storage.store(*stored_tlm);
|
||||
}
|
||||
}
|
||||
|
||||
if (!validate(*context.cfg.security, *sdata, stored_tlm->raw())) {
|
||||
throw VerificationFailure("signature verification of ECTL failed");
|
||||
}
|
||||
|
||||
if (dry_run) {
|
||||
std::cout << "ECTL signature verified. Storage unchanged in this dry run.\n";
|
||||
return;
|
||||
}
|
||||
|
||||
write(context.ectl_file(), ByteBuffer { response.body().begin(), response.body().end() });
|
||||
std::cout << "Stored ECTL\n";
|
||||
|
||||
UpdateVisitor visitor(context.cfg.root_ca);
|
||||
tlm_message.visit_tlm_ctl(visitor);
|
||||
} else {
|
||||
throw DecodingFailure("message contains no signed data");
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<CLI::App> build_ectl_command(std::shared_ptr<Context> context)
|
||||
{
|
||||
auto app = std::make_shared<CLI::App>("European Certificate Trust List", "ectl");
|
||||
|
||||
auto list = app->add_subcommand("list", "list trusted CAs from the locally stored ECTL");
|
||||
list->callback([context]() { context->action = [context]() { list_ectl(*context); }; });
|
||||
|
||||
auto fetch =
|
||||
app->add_subcommand("fetch", "fetch the full ECTL signed by the TLM (omit the id to use the latest known TLM)");
|
||||
auto fetch_id =
|
||||
fetch->add_option("hid8", context->hid8, "HashedId8 of the TLM; omit to use the latest known TLM certificate")
|
||||
->check(hid8_validator);
|
||||
auto fetch_dry = fetch->add_flag("--dry-run,-n", "fetch and verify only; do not store the ECTL");
|
||||
fetch->callback([context, fetch_id, fetch_dry]() {
|
||||
const bool has_id = fetch_id->count() > 0;
|
||||
const bool dry_run = fetch_dry->as<bool>();
|
||||
context->action = [context, has_id, dry_run]() {
|
||||
fetch_ectl(*context, has_id ? &context->hid8 : nullptr, dry_run);
|
||||
};
|
||||
});
|
||||
|
||||
app->final_callback([context]() {
|
||||
if (!context->action) {
|
||||
context->action = [context]() { list_ectl(*context); };
|
||||
}
|
||||
context->action();
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::shared_ptr<CLI::App> build_cpoc_command(const MainConfig& config)
|
||||
{
|
||||
auto ctx = std::make_shared<Context>(config);
|
||||
auto app = std::make_shared<CLI::App>("C-ITS Point of Contact Protocol", "cpoc");
|
||||
app->add_option("--url", ctx->cpoc_url, "CPOC base URL")->capture_default_str();
|
||||
|
||||
app->add_subcommand(build_ectl_command(ctx));
|
||||
app->add_subcommand(build_tlm_command(ctx));
|
||||
app->require_subcommand();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
Reference in New Issue
Block a user