Files
MicrOBU/microbu-esp32c5/external/vanetza-idf/tools/pki/certificate.cpp
T
Ashin Walpola 0e9525162d 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).
2026-09-23 17:46:40 +02:00

515 lines
17 KiB
C++

#include "certificate.hpp"
#include "asn1.hpp"
#include "security_module.hpp"
#include <vanetza/common/its_aid.hpp>
namespace vanetza
{
namespace pki
{
namespace
{
bool is_compressed(const Vanetza_Security_EccP256CurvePoint& point)
{
switch (point.present) {
case Vanetza_Security_EccP256CurvePoint_PR_compressed_y_0:
case Vanetza_Security_EccP256CurvePoint_PR_compressed_y_1:
return true;
default:
return false;
}
}
bool is_compressed(const Vanetza_Security_EccP384CurvePoint& point)
{
switch (point.present) {
case Vanetza_Security_EccP384CurvePoint_PR_compressed_y_0:
case Vanetza_Security_EccP384CurvePoint_PR_compressed_y_1:
return true;
default:
return false;
}
}
bool is_signature_x_only(const Vanetza_Security_Signature_t& sig)
{
switch (sig.present) {
case Vanetza_Security_Signature_PR_ecdsaNistP256Signature:
return sig.choice.ecdsaNistP256Signature.rSig.present == Vanetza_Security_EccP256CurvePoint_PR_x_only;
case Vanetza_Security_Signature_PR_ecdsaBrainpoolP256r1Signature:
return sig.choice.ecdsaBrainpoolP256r1Signature.rSig.present ==
Vanetza_Security_EccP256CurvePoint_PR_x_only;
case Vanetza_Security_Signature_PR_ecdsaBrainpoolP384r1Signature:
return sig.choice.ecdsaBrainpoolP384r1Signature.rSig.present ==
Vanetza_Security_EccP384CurvePoint_PR_x_only;
default:
return true; // not an ECDSA signature at all
}
}
void copy_coordinates(const Vanetza_Security_EccP256CurvePoint_t& point, PublicKey& key)
{
switch (point.present) {
case Vanetza_Security_EccP256CurvePoint_PR_uncompressedP256:
key.compression = KeyCompression::NoCompression;
pki::copy(point.choice.uncompressedP256.x, key.x);
pki::copy(point.choice.uncompressedP256.y, key.y);
break;
case Vanetza_Security_EccP256CurvePoint_PR_compressed_y_0:
key.compression = KeyCompression::Y0;
pki::copy(point.choice.compressed_y_0, key.x);
break;
case Vanetza_Security_EccP256CurvePoint_PR_compressed_y_1:
key.compression = KeyCompression::Y1;
pki::copy(point.choice.compressed_y_1, key.x);
break;
default:
throw std::runtime_error("unsupported curve point type");
break;
}
}
template<KeyType> PublicKey make_public_key(const Vanetza_Security_EccP256CurvePoint_t& point);
template<> PublicKey make_public_key<KeyType::NistP256>(const Vanetza_Security_EccP256CurvePoint_t& point)
{
PublicKey pub;
pub.type = KeyType::NistP256;
copy_coordinates(point, pub);
return pub;
}
template<> PublicKey make_public_key<KeyType::BrainpoolP256r1>(const Vanetza_Security_EccP256CurvePoint_t& point)
{
PublicKey pub;
pub.type = KeyType::BrainpoolP256r1;
copy_coordinates(point, pub);
return pub;
}
} // namespace
Certificate::Certificate() : m_asn1(asn_DEF_Vanetza_Security_EtsiTs103097Certificate)
{
}
Certificate::Certificate(const Vanetza_Security_EtsiTs103097Certificate_t& src) :
m_asn1(asn_DEF_Vanetza_Security_EtsiTs103097Certificate, &src)
{
}
std::string Certificate::get_name() const
{
return pki::get_name(*m_asn1);
}
PublicKey Certificate::get_public_key() const
{
return pki::get_public_key(*m_asn1);
}
boost::optional<PublicKey> Certificate::get_encryption_key() const
{
if (m_asn1->toBeSigned.encryptionKey) {
const Vanetza_Security_PublicEncryptionKey_t& enckey = *m_asn1->toBeSigned.encryptionKey;
switch (enckey.publicKey.present) {
case Vanetza_Security_BasePublicEncryptionKey_PR_eciesNistP256:
return make_public_key<KeyType::NistP256>(enckey.publicKey.choice.eciesNistP256);
break;
case Vanetza_Security_BasePublicEncryptionKey_PR_eciesBrainpoolP256r1:
return make_public_key<KeyType::BrainpoolP256r1>(enckey.publicKey.choice.eciesBrainpoolP256r1);
break;
default:
throw std::runtime_error("unsupported encryption key type");
break;
}
} else {
return boost::none;
}
}
Clock::time_point Certificate::valid_since() const
{
return Clock::time_point { std::chrono::seconds(m_asn1->toBeSigned.validityPeriod.start) };
}
Clock::time_point Certificate::valid_until() const
{
Clock::duration d;
const Vanetza_Security_Duration& asn1_d = m_asn1->toBeSigned.validityPeriod.duration;
switch (asn1_d.present) {
case Vanetza_Security_Duration_PR_years:
// IEEE 1609.2: "A year is considered to be 31556952 seconds"
d = asn1_d.choice.years * std::chrono::seconds(31556952);
break;
case Vanetza_Security_Duration_PR_sixtyHours:
d = std::chrono::hours(60 * asn1_d.choice.sixtyHours);
break;
case Vanetza_Security_Duration_PR_hours:
d = std::chrono::hours(asn1_d.choice.hours);
break;
case Vanetza_Security_Duration_PR_minutes:
d = std::chrono::minutes(asn1_d.choice.minutes);
break;
case Vanetza_Security_Duration_PR_seconds:
d = std::chrono::seconds(asn1_d.choice.seconds);
break;
case Vanetza_Security_Duration_PR_milliseconds:
d = std::chrono::milliseconds(asn1_d.choice.milliseconds);
break;
case Vanetza_Security_Duration_PR_microseconds:
d = std::chrono::microseconds(asn1_d.choice.microseconds);
break;
default:
// no validity duration as safe fallback
d = std::chrono::seconds(0);
break;
}
return valid_since() + d;
}
bool Certificate::decode(const char* data, std::size_t length)
{
return m_asn1.decode(data, length);
}
bool Certificate::decode(const std::string& buffer)
{
return m_asn1.decode(buffer.data(), buffer.size());
}
bool Certificate::decode(const ByteBuffer& buffer)
{
return m_asn1.decode(buffer.data(), buffer.size());
}
ByteBuffer Certificate::encode() const
{
return m_asn1.encode();
}
void Certificate::print() const
{
xer_fprint(stdout, &asn_DEF_Vanetza_Security_EtsiTs103097Certificate, &*m_asn1);
}
bool Certificate::is_canonical() const
{
return vanetza::pki::is_canonical(*m_asn1);
}
HashedId8 Certificate::calculate_hashed_id8(SecurityModule& sec) const
{
return vanetza::pki::calculate_hashed_id8(sec, *m_asn1);
}
Sha256Hash calculate_sha256_hash(SecurityModule& sec, const Certificate& cert)
{
ByteBuffer buffer = cert.encode();
return sec.calculate_sha256_hash(buffer.data(), buffer.size());
}
Sha384Hash calculate_sha384_hash(SecurityModule& sec, const Certificate& cert)
{
ByteBuffer buffer = cert.encode();
return sec.calculate_sha384_hash(buffer.data(), buffer.size());
}
bool is_currently_valid(const Certificate& cert, Clock::time_point t)
{
return cert.valid_since() <= t && cert.valid_until() >= t;
}
bool is_root_ca(const Certificate& cert)
{
const auto& issuer = cert.raw().issuer;
const bool self_issued = (issuer.present == Vanetza_Security_IssuerIdentifier_PR_self);
if (!self_issued) {
return false;
}
const auto& tbs = cert.raw().toBeSigned;
const bool can_issue = tbs.certIssuePermissions && tbs.certIssuePermissions->list.count > 0;
if (!can_issue) {
return false;
}
if (tbs.appPermissions) {
bool sign_crl = false;
bool sign_ctl = false;
for (int i = 0; i < tbs.appPermissions->list.count; ++i) {
const Vanetza_Security_PsidSsp_t* permission = tbs.appPermissions->list.array[i];
if (!permission) {
continue;
}
switch (permission->psid) {
case aid::CTL:
sign_ctl = true;
break;
case aid::CRL:
sign_crl = true;
break;
default:
// no op
break;
}
}
if (!sign_ctl || !sign_crl) {
return false;
}
} else {
return false;
}
return true;
}
namespace
{
bool issuing_scope_contains(const Certificate& cert, ItsAid target)
{
const auto* cip = cert.raw().toBeSigned.certIssuePermissions;
if (!cip) {
return false;
}
for (int i = 0; i < cip->list.count; ++i) {
const auto* group = cip->list.array[i];
if (!group) {
continue;
}
const auto& sp = group->subjectPermissions;
if (sp.present == Vanetza_Security_SubjectPermissions_PR_all) {
return true;
}
if (sp.present == Vanetza_Security_SubjectPermissions_PR_explicit) {
const auto& ranges = sp.choice.Explicit.list;
for (int j = 0; j < ranges.count; ++j) {
if (ranges.array[j] && ranges.array[j]->psid == static_cast<long>(target)) {
return true;
}
}
}
}
return false;
}
bool app_permissions_contains(const Certificate& cert, ItsAid target)
{
const auto* ap = cert.raw().toBeSigned.appPermissions;
if (!ap) {
return false;
}
for (int i = 0; i < ap->list.count; ++i) {
if (ap->list.array[i] && ap->list.array[i]->psid == static_cast<long>(target)) {
return true;
}
}
return false;
}
} // namespace
CertificateRole certificate_role(const Certificate& cert)
{
if (is_root_ca(cert)) {
return CertificateRole::RootCa;
}
const auto& tbs = cert.raw().toBeSigned;
const bool self_issued = (cert.raw().issuer.present == Vanetza_Security_IssuerIdentifier_PR_self);
const bool has_cert_issue = tbs.certIssuePermissions && tbs.certIssuePermissions->list.count > 0;
if (self_issued) {
// TS 103 097 §7.2.5: TLM is self-signed, signs the CTL, no certIssuePermissions.
if (!has_cert_issue && app_permissions_contains(cert, aid::CTL)) {
return CertificateRole::Tlm;
}
return CertificateRole::Unknown;
}
if (has_cert_issue) {
// TS 103 097 §7.2.4 subordinate CA. Per TS 102 941 Table 1 the EA issues
// enrolment credentials (SCR in scope); the AA issues authorization tickets.
return issuing_scope_contains(cert, aid::SCR) ? CertificateRole::EnrolmentAuthority :
CertificateRole::AuthorizationAuthority;
}
// End entity: §7.2.2 EC uses CertificateId name, §7.2.1 AT uses CertificateId none.
switch (tbs.id.present) {
case Vanetza_Security_CertificateId_PR_name:
return CertificateRole::EnrolmentCredential;
case Vanetza_Security_CertificateId_PR_none:
return CertificateRole::AuthorizationTicket;
default:
return CertificateRole::Unknown;
}
}
bool is_canonical(const Vanetza_Security_EtsiTs103097Certificate_t& cert)
{
bool compressed_point = true;
const Vanetza_Security_VerificationKeyIndicator& indicator = cert.toBeSigned.verifyKeyIndicator;
if (indicator.present == Vanetza_Security_VerificationKeyIndicator_PR_verificationKey) {
const Vanetza_Security_PublicVerificationKey& pubkey = indicator.choice.verificationKey;
switch (pubkey.present) {
case Vanetza_Security_PublicVerificationKey_PR_ecdsaNistP256:
compressed_point = is_compressed(pubkey.choice.ecdsaNistP256);
break;
case Vanetza_Security_PublicVerificationKey_PR_ecdsaBrainpoolP256r1:
compressed_point = is_compressed(pubkey.choice.ecdsaBrainpoolP256r1);
break;
case Vanetza_Security_PublicVerificationKey_PR_ecdsaBrainpoolP384r1:
compressed_point = is_compressed(pubkey.choice.ecdsaBrainpoolP384r1);
break;
default:
break;
}
} else if (indicator.present == Vanetza_Security_VerificationKeyIndicator_PR_reconstructionValue) {
compressed_point = is_compressed(indicator.choice.reconstructionValue);
}
if (!compressed_point) {
return false;
} else if (cert.signature && !is_signature_x_only(*cert.signature)) {
return false;
} else {
return true;
}
}
Sha256Hash calculate_sha256_hash(SecurityModule& security, const Vanetza_Security_Certificate_t& cert)
{
ByteBuffer buffer = asn1::encode_oer(asn_DEF_Vanetza_Security_Certificate, &cert);
return security.calculate_sha256_hash(buffer.data(), buffer.size());
}
Sha384Hash calculate_sha384_hash(SecurityModule& sec, const Vanetza_Security_Certificate_t& cert)
{
ByteBuffer buffer = asn1::encode_oer(asn_DEF_Vanetza_Security_Certificate, &cert);
return sec.calculate_sha384_hash(buffer.data(), buffer.size());
}
HashedId8 calculate_hashed_id8(SecurityModule& sec, const Vanetza_Security_Certificate_t& cert)
{
if (!is_canonical(cert)) {
throw std::runtime_error("HashedId8 can only be calculated for canonical certificates");
}
// all explicit certificates possess an verification key
const Vanetza_Security_VerificationKeyIndicator& indicator = cert.toBeSigned.verifyKeyIndicator;
if (indicator.present == Vanetza_Security_VerificationKeyIndicator_PR_verificationKey) {
switch (indicator.choice.verificationKey.present) {
case Vanetza_Security_PublicVerificationKey_PR_ecdsaNistP256:
case Vanetza_Security_PublicVerificationKey_PR_ecdsaBrainpoolP256r1:
return HashedId8 { calculate_sha256_hash(sec, cert) };
case Vanetza_Security_PublicVerificationKey_PR_ecdsaBrainpoolP384r1:
return HashedId8 { calculate_sha384_hash(sec, cert) };
default:
throw std::runtime_error("do not know how to hash the certificate");
break;
}
} else {
// fall back to SHA-256
return HashedId8 { calculate_sha256_hash(sec, cert) };
}
}
PublicKey make_public_key(KeyType t, const Vanetza_Security_EccP256CurvePoint_t& point)
{
PublicKey pub;
pub.type = t;
switch (point.present) {
case Vanetza_Security_EccP256CurvePoint_PR_compressed_y_0:
pub.compression = KeyCompression::Y0;
pub.x = copy(point.choice.compressed_y_0);
break;
case Vanetza_Security_EccP256CurvePoint_PR_compressed_y_1:
pub.compression = KeyCompression::Y1;
pub.x = copy(point.choice.compressed_y_1);
break;
case Vanetza_Security_EccP256CurvePoint_PR_uncompressedP256:
pub.compression = KeyCompression::NoCompression;
pub.x = copy(point.choice.uncompressedP256.x);
pub.y = copy(point.choice.uncompressedP256.y);
break;
default:
throw std::runtime_error("cannot create public key from given curve point");
break;
}
return pub;
}
PublicKey make_public_key(KeyType t, const Vanetza_Security_EccP384CurvePoint_t& point)
{
PublicKey pub;
pub.type = t;
switch (point.present) {
case Vanetza_Security_EccP384CurvePoint_PR_compressed_y_0:
pub.compression = KeyCompression::Y0;
pub.x = copy(point.choice.compressed_y_0);
break;
case Vanetza_Security_EccP384CurvePoint_PR_compressed_y_1:
pub.compression = KeyCompression::Y1;
pub.x = copy(point.choice.compressed_y_1);
break;
case Vanetza_Security_EccP384CurvePoint_PR_uncompressedP384:
pub.compression = KeyCompression::NoCompression;
pub.x = copy(point.choice.uncompressedP384.x);
pub.y = copy(point.choice.uncompressedP384.y);
break;
default:
throw std::runtime_error("cannot create public key from given curve point");
break;
}
return pub;
}
PublicKey get_public_key(const Vanetza_Security_PublicVerificationKey_t& input)
{
switch (input.present) {
case Vanetza_Security_PublicVerificationKey_PR_ecdsaNistP256:
return make_public_key(KeyType::NistP256, input.choice.ecdsaNistP256);
break;
case Vanetza_Security_PublicVerificationKey_PR_ecdsaBrainpoolP256r1:
return make_public_key(KeyType::BrainpoolP256r1, input.choice.ecdsaBrainpoolP256r1);
break;
case Vanetza_Security_PublicVerificationKey_PR_ecdsaBrainpoolP384r1:
return make_public_key(KeyType::BrainpoolP384r1, input.choice.ecdsaBrainpoolP384r1);
break;
default:
throw std::runtime_error("unknown public verification key type");
break;
}
}
PublicKey get_public_key(const Vanetza_Security_Certificate_t& cert)
{
switch (cert.toBeSigned.verifyKeyIndicator.present) {
case Vanetza_Security_VerificationKeyIndicator_PR_verificationKey:
return get_public_key(cert.toBeSigned.verifyKeyIndicator.choice.verificationKey);
break;
default:
throw std::runtime_error("unable to fetch public key from certificate");
}
}
std::string get_name(const Vanetza_Security_Certificate_t& cert)
{
std::string name;
if (cert.toBeSigned.id.present == Vanetza_Security_CertificateId_PR_name) {
const UTF8String_t& hostname = cert.toBeSigned.id.choice.name;
name = std::string { hostname.buf, hostname.buf + hostname.size };
}
return name;
}
} // namespace pki
} // namespace vanetza