Files

323 lines
12 KiB
C++
Raw Permalink Normal View History

#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