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.
This commit is contained in:
Ashin Walpola
2026-09-24 10:56:05 +02:00
parent 2f60623e18
commit d107534eb2
9781 changed files with 1560475 additions and 17 deletions
@@ -0,0 +1,62 @@
#pragma once
#include <vanetza/common/byte_buffer.hpp>
#include <cstddef>
#include <memory>
namespace vanetza
{
namespace security
{
namespace pqc
{
constexpr std::size_t fndsa512_public_key_size = 897;
constexpr std::size_t fndsa512_private_key_size = 1281;
constexpr std::size_t fndsa512_signature_size = 666;
struct PublicKey
{
ByteBuffer bytes;
};
struct PrivateKey
{
ByteBuffer bytes;
};
struct Signature
{
ByteBuffer bytes;
};
struct KeyPair
{
PublicKey public_key;
PrivateKey private_key;
};
/**
* Cryptographic operations for the experimental FN-DSA-512 profile.
*
* The interface is intentionally separate from security::Backend. Enabling
* the experimental profile therefore does not alter the established ECC
* backend contract or its key types.
*/
class Backend
{
public:
virtual KeyPair generate_key_pair() = 0;
virtual Signature sign(const PrivateKey&, const ByteBuffer& data) = 0;
virtual bool verify(const PublicKey&, const ByteBuffer& data, const Signature&) = 0;
virtual ~Backend() = default;
};
/**
* Create the liboqs-backed implementation used by the experimental profile.
*/
std::unique_ptr<Backend> create_fndsa512_backend();
} // namespace pqc
} // namespace security
} // namespace vanetza
@@ -0,0 +1,99 @@
#include <vanetza/security/pqc/fndsa512.hpp>
#include <oqs/oqs.h>
#include <memory>
#include <stdexcept>
namespace vanetza
{
namespace security
{
namespace pqc
{
namespace
{
using OqsSignature = std::unique_ptr<OQS_SIG, decltype(&OQS_SIG_free)>;
OqsSignature create_signature_context()
{
OqsSignature context { OQS_SIG_new(OQS_SIG_alg_falcon_padded_512), OQS_SIG_free };
if (!context) {
throw std::runtime_error("liboqs does not provide Falcon-padded-512");
}
if (context->length_public_key != fndsa512_public_key_size ||
context->length_secret_key != fndsa512_private_key_size ||
context->length_signature != fndsa512_signature_size) {
throw std::runtime_error("liboqs Falcon-padded-512 parameters do not match the ASN.1 profile");
}
return context;
}
class OqsBackend final : public Backend
{
public:
KeyPair generate_key_pair() override
{
auto context = create_signature_context();
KeyPair key_pair;
key_pair.public_key.bytes.resize(fndsa512_public_key_size);
key_pair.private_key.bytes.resize(fndsa512_private_key_size);
const auto result = OQS_SIG_keypair(
context.get(), key_pair.public_key.bytes.data(), key_pair.private_key.bytes.data());
if (result != OQS_SUCCESS) {
throw std::runtime_error("liboqs failed to generate an FN-DSA-512 key pair");
}
return key_pair;
}
Signature sign(const PrivateKey& private_key, const ByteBuffer& data) override
{
if (private_key.bytes.size() != fndsa512_private_key_size) {
throw std::invalid_argument("FN-DSA-512 private key has an invalid size");
}
auto context = create_signature_context();
Signature signature;
signature.bytes.resize(fndsa512_signature_size);
std::size_t signature_size = 0;
const auto result = OQS_SIG_sign(
context.get(), signature.bytes.data(), &signature_size,
data.data(), data.size(), private_key.bytes.data());
if (result != OQS_SUCCESS) {
throw std::runtime_error("liboqs failed to create an FN-DSA-512 signature");
}
if (signature_size != fndsa512_signature_size) {
throw std::runtime_error("liboqs returned a non-padded FN-DSA-512 signature");
}
return signature;
}
bool verify(const PublicKey& public_key, const ByteBuffer& data, const Signature& signature) override
{
if (public_key.bytes.size() != fndsa512_public_key_size ||
signature.bytes.size() != fndsa512_signature_size) {
return false;
}
auto context = create_signature_context();
return OQS_SIG_verify(
context.get(), data.data(), data.size(), signature.bytes.data(),
signature.bytes.size(), public_key.bytes.data()) == OQS_SUCCESS;
}
};
} // namespace
std::unique_ptr<Backend> create_fndsa512_backend()
{
return std::unique_ptr<Backend> { new OqsBackend() };
}
} // namespace pqc
} // namespace security
} // namespace vanetza
@@ -0,0 +1,386 @@
#include <vanetza/security/pqc/hybrid_certificate.hpp>
#include <vanetza/asn1/asn1c_wrapper.hpp>
#include <vanetza/asn1/security_profile.hpp>
#include VANETZA_ASN1_SECURITY_HEADER(PublicVerificationKey.h)
#include VANETZA_ASN1_SECURITY_HEADER(Signature.h)
#include VANETZA_ASN1_SECURITY_HEADER(ToBeSignedCertificate.h)
#include <vanetza/security/backend.hpp>
#include <vanetza/security/private_key.hpp>
#include <vanetza/security/v3/asn1_conversions.hpp>
#include <vanetza/security/v3/certificate.hpp>
#include <vanetza/security/v3/hash.hpp>
#include <limits>
#include <stdexcept>
#include <utility>
namespace vanetza
{
namespace security
{
namespace pqc
{
using v3::Certificate;
using v3::CertificateView;
namespace
{
boost::optional<Certificate> copy_certificate(const CertificateView& view)
{
try {
Certificate certificate;
if (certificate.decode(view.encode())) {
return certificate;
}
} catch (const std::exception&) {
// A malformed or empty view cannot expose hybrid certificate data.
}
return boost::none;
}
Certificate copy_certificate_or_throw(const CertificateView& view)
{
auto certificate = copy_certificate(view);
if (!certificate) {
throw std::invalid_argument("certificate cannot be encoded and decoded");
}
return std::move(*certificate);
}
ByteBuffer copy_octets(const OCTET_STRING_t& value)
{
if (!value.buf || value.size == 0) {
return {};
}
return ByteBuffer(value.buf, value.buf + value.size);
}
boost::optional<PublicKey> extract_alternative_public_key(const Certificate& certificate)
{
const auto* key = certificate->toBeSigned.altVerificationKey;
if (!key || key->present != Vanetza_Security_PublicVerificationKey_PR_fnDsa512) {
return boost::none;
}
PublicKey result { copy_octets(key->choice.fnDsa512) };
return result.bytes.size() == fndsa512_public_key_size ?
boost::optional<PublicKey> { std::move(result) } : boost::none;
}
boost::optional<Signature> extract_alternative_signature(const Certificate& certificate)
{
const auto* signature = certificate->toBeSigned.altSignatureValue;
if (!signature || signature->present != Vanetza_Security_Signature_PR_fnDsa512Signature) {
return boost::none;
}
Signature result { copy_octets(signature->choice.fnDsa512Signature) };
return result.bytes.size() == fndsa512_signature_size ?
boost::optional<Signature> { std::move(result) } : boost::none;
}
void assign_octets(OCTET_STRING_t& destination, const ByteBuffer& source)
{
if (source.size() > static_cast<std::size_t>(std::numeric_limits<int>::max()) ||
OCTET_STRING_fromBuf(&destination,
reinterpret_cast<const char*>(source.data()),
static_cast<int>(source.size())) != 0) {
throw std::runtime_error("cannot allocate ASN.1 octet string");
}
}
void set_primary_signature(
Certificate& certificate, const ::vanetza::security::Signature& signature)
{
if (signature.r.size() != key_length(signature.type) ||
signature.s.size() != key_length(signature.type)) {
throw std::invalid_argument("ECC certificate signature has an invalid size");
}
if (certificate->signature) {
vanetza::asn1::free(asn_DEF_Vanetza_Security_Signature, certificate->signature);
}
certificate->signature = vanetza::asn1::allocate<v3::asn1::Signature>();
v3::asn1::EccP256CurvePoint* r256 = nullptr;
v3::asn1::EccP384CurvePoint* r384 = nullptr;
OCTET_STRING_t* s = nullptr;
switch (signature.type) {
case KeyType::NistP256:
certificate->signature->present = Vanetza_Security_Signature_PR_ecdsaNistP256Signature;
r256 = &certificate->signature->choice.ecdsaNistP256Signature.rSig;
s = &certificate->signature->choice.ecdsaNistP256Signature.sSig;
break;
case KeyType::BrainpoolP256r1:
certificate->signature->present = Vanetza_Security_Signature_PR_ecdsaBrainpoolP256r1Signature;
r256 = &certificate->signature->choice.ecdsaBrainpoolP256r1Signature.rSig;
s = &certificate->signature->choice.ecdsaBrainpoolP256r1Signature.sSig;
break;
case KeyType::BrainpoolP384r1:
certificate->signature->present = Vanetza_Security_Signature_PR_ecdsaBrainpoolP384r1Signature;
r384 = &certificate->signature->choice.ecdsaBrainpoolP384r1Signature.rSig;
s = &certificate->signature->choice.ecdsaBrainpoolP384r1Signature.sSig;
break;
default:
throw std::invalid_argument("unsupported ECC certificate signature type");
}
if (r256) {
r256->present = Vanetza_Security_EccP256CurvePoint_PR_x_only;
assign_octets(r256->choice.x_only, signature.r);
} else {
r384->present = Vanetza_Security_EccP384CurvePoint_PR_x_only;
assign_octets(r384->choice.x_only, signature.r);
}
assign_octets(*s, signature.s);
}
const CertificateView& signing_certificate(
const CertificateView& subject, const CertificateView* issuer)
{
if (subject.issuer_is_self()) {
return subject;
}
if (!issuer) {
throw std::invalid_argument("issuer certificate is required for a non-self-signed certificate");
}
return *issuer;
}
ByteBuffer canonical_tbs(const CertificateView& subject, SignatureLayer layer)
{
Certificate certificate = copy_certificate_or_throw(subject);
if (layer == SignatureLayer::Alternative) {
clear_alternative_signature(certificate);
}
auto canonical = certificate.canonicalize();
if (!canonical) {
throw std::invalid_argument("certificate cannot be canonicalized");
}
return vanetza::asn1::encode_oer(
asn_DEF_Vanetza_Security_ToBeSignedCertificate, &canonical->content()->toBeSigned);
}
ByteBuffer canonical_issuer(const CertificateView& subject, const CertificateView* issuer)
{
if (subject.issuer_is_self()) {
return {};
}
if (!issuer) {
throw std::invalid_argument("issuer certificate is required for a non-self-signed certificate");
}
const auto expected_digest = subject.issuer_digest();
const auto actual_digest = issuer->calculate_digest();
if (!expected_digest || !actual_digest || *expected_digest != *actual_digest) {
throw std::invalid_argument("issuer certificate does not match the subject issuer identifier");
}
auto canonical = issuer->canonicalize();
if (!canonical) {
throw std::invalid_argument("issuer certificate cannot be canonicalized");
}
return canonical->encode();
}
} // namespace
boost::optional<PublicKey> get_alternative_public_key(const CertificateView& view)
{
auto certificate = copy_certificate(view);
return certificate ? extract_alternative_public_key(*certificate) : boost::none;
}
boost::optional<Signature> get_alternative_signature(const CertificateView& view)
{
auto certificate = copy_certificate(view);
return certificate ? extract_alternative_signature(*certificate) : boost::none;
}
MaterialState alternative_material_state(const CertificateView& view)
{
auto certificate = copy_certificate(view);
if (!certificate) {
return MaterialState::Inconsistent;
}
const bool key_present = certificate->content()->toBeSigned.altVerificationKey;
const bool signature_present = certificate->content()->toBeSigned.altSignatureValue;
const bool has_key = static_cast<bool>(extract_alternative_public_key(*certificate));
const bool has_signature = static_cast<bool>(extract_alternative_signature(*certificate));
if (!key_present && !signature_present) {
return MaterialState::None;
}
if (key_present != has_key || signature_present != has_signature) {
return MaterialState::Inconsistent;
}
if (view.is_at_certificate()) {
return !has_key && has_signature ? MaterialState::EndEntity : MaterialState::Inconsistent;
}
if (view.issuer_is_self() || view.is_ca_certificate()) {
return has_key && has_signature ? MaterialState::Authority : MaterialState::Inconsistent;
}
return MaterialState::Inconsistent;
}
void set_alternative_public_key(Certificate& certificate, const PublicKey& key)
{
if (key.bytes.size() != fndsa512_public_key_size) {
throw std::invalid_argument("FN-DSA-512 public key has an invalid size");
}
clear_alternative_public_key(certificate);
certificate->toBeSigned.altVerificationKey =
vanetza::asn1::allocate<v3::asn1::PublicVerificationKey>();
certificate->toBeSigned.altVerificationKey->present =
Vanetza_Security_PublicVerificationKey_PR_fnDsa512;
assign_octets(certificate->toBeSigned.altVerificationKey->choice.fnDsa512, key.bytes);
}
void set_alternative_signature(Certificate& certificate, const Signature& signature)
{
if (signature.bytes.size() != fndsa512_signature_size) {
throw std::invalid_argument("FN-DSA-512 signature has an invalid size");
}
clear_alternative_signature(certificate);
certificate->toBeSigned.altSignatureValue =
vanetza::asn1::allocate<v3::asn1::Signature>();
certificate->toBeSigned.altSignatureValue->present =
Vanetza_Security_Signature_PR_fnDsa512Signature;
assign_octets(
certificate->toBeSigned.altSignatureValue->choice.fnDsa512Signature,
signature.bytes);
}
void clear_alternative_public_key(Certificate& certificate)
{
if (certificate->toBeSigned.altVerificationKey) {
vanetza::asn1::free(
asn_DEF_Vanetza_Security_PublicVerificationKey,
certificate->toBeSigned.altVerificationKey);
certificate->toBeSigned.altVerificationKey = nullptr;
}
}
void clear_alternative_signature(Certificate& certificate)
{
if (certificate->toBeSigned.altSignatureValue) {
vanetza::asn1::free(
asn_DEF_Vanetza_Security_Signature,
certificate->toBeSigned.altSignatureValue);
certificate->toBeSigned.altSignatureValue = nullptr;
}
}
ByteBuffer calculate_certificate_hash(
::vanetza::security::Backend& backend, HashAlgorithm algorithm,
const CertificateView& subject,
const CertificateView* issuer, SignatureLayer layer)
{
if (algorithm == HashAlgorithm::Unspecified) {
throw std::invalid_argument("certificate hash algorithm is unspecified");
}
const ByteBuffer data_input = canonical_tbs(subject, layer);
const ByteBuffer signer_input = canonical_issuer(subject, issuer);
const ByteBuffer data_hash = backend.calculate_hash(algorithm, data_input);
const ByteBuffer signer_hash = backend.calculate_hash(algorithm, signer_input);
ByteBuffer concatenated;
concatenated.reserve(data_hash.size() + signer_hash.size());
concatenated.insert(concatenated.end(), data_hash.begin(), data_hash.end());
concatenated.insert(concatenated.end(), signer_hash.begin(), signer_hash.end());
return backend.calculate_hash(algorithm, concatenated);
}
void sign_primary_certificate(
Certificate& subject, const CertificateView* issuer,
::vanetza::security::Backend& backend,
const ::vanetza::security::PrivateKey& issuer_key)
{
const CertificateView& signer = signing_certificate(subject, issuer);
const auto public_key = v3::get_public_key(*copy_certificate_or_throw(signer).content());
if (!public_key || public_key->type != issuer_key.type) {
throw std::invalid_argument("ECC private key does not match the issuer certificate key type");
}
const HashAlgorithm algorithm = v3::specified_hash_algorithm(issuer_key.type);
const ByteBuffer digest = calculate_certificate_hash(
backend, algorithm, subject, issuer, SignatureLayer::Primary);
const auto signature = backend.sign_digest(issuer_key, digest);
if (!backend.verify_digest(*public_key, digest, signature)) {
throw std::invalid_argument(
"ECC private key does not match the issuer certificate public key");
}
set_primary_signature(subject, signature);
}
bool verify_primary_certificate(
const CertificateView& subject, const CertificateView* issuer,
::vanetza::security::Backend& backend)
{
try {
const CertificateView& signer = signing_certificate(subject, issuer);
Certificate signer_copy = copy_certificate_or_throw(signer);
Certificate subject_copy = copy_certificate_or_throw(subject);
const auto public_key = v3::get_public_key(*signer_copy.content());
const auto signature = v3::get_signature(*subject_copy.content());
if (!public_key || !signature || public_key->type != signature->type) {
return false;
}
const HashAlgorithm algorithm = v3::specified_hash_algorithm(public_key->type);
const ByteBuffer digest = calculate_certificate_hash(
backend, algorithm, subject, issuer, SignatureLayer::Primary);
return backend.verify_digest(*public_key, digest, *signature);
} catch (const std::exception&) {
return false;
}
}
void sign_alternative_certificate(
Certificate& subject, const CertificateView* issuer,
::vanetza::security::Backend& hash_backend, Backend& backend,
const PrivateKey& issuer_key)
{
const CertificateView& signer = signing_certificate(subject, issuer);
const auto public_key = get_alternative_public_key(signer);
if (!public_key) {
throw std::invalid_argument("issuer certificate has no FN-DSA-512 alternative key");
}
const ByteBuffer digest = calculate_certificate_hash(
hash_backend, HashAlgorithm::SHA256, subject, issuer, SignatureLayer::Alternative);
const auto signature = backend.sign(issuer_key, digest);
if (!backend.verify(*public_key, digest, signature)) {
throw std::invalid_argument(
"FN-DSA-512 private key does not match the issuer certificate public key");
}
set_alternative_signature(subject, signature);
}
bool verify_alternative_certificate(
const CertificateView& subject, const CertificateView* issuer,
::vanetza::security::Backend& hash_backend, Backend& backend)
{
try {
const CertificateView& signer = signing_certificate(subject, issuer);
const auto public_key = get_alternative_public_key(signer);
const auto signature = get_alternative_signature(subject);
if (!public_key || !signature) {
return false;
}
const ByteBuffer digest = calculate_certificate_hash(
hash_backend, HashAlgorithm::SHA256, subject, issuer, SignatureLayer::Alternative);
return backend.verify(*public_key, digest, *signature);
} catch (const std::exception&) {
return false;
}
}
} // namespace pqc
} // namespace security
} // namespace vanetza
@@ -0,0 +1,81 @@
#pragma once
#include <vanetza/common/byte_buffer.hpp>
#include <vanetza/security/hash_algorithm.hpp>
#include <vanetza/security/pqc/fndsa512.hpp>
#include <boost/optional/optional.hpp>
namespace vanetza
{
namespace security
{
class Backend;
struct PrivateKey;
namespace v3
{
class Certificate;
class CertificateView;
} // namespace v3
namespace pqc
{
/** The two nested certificate signatures used by the experimental profile. */
enum class SignatureLayer
{
Alternative,
Primary,
};
/** Shape of the optional alternative material carried by a certificate. */
enum class MaterialState
{
None,
Authority,
EndEntity,
Inconsistent,
};
boost::optional<PublicKey> get_alternative_public_key(const v3::CertificateView&);
boost::optional<Signature> get_alternative_signature(const v3::CertificateView&);
MaterialState alternative_material_state(const v3::CertificateView&);
void set_alternative_public_key(v3::Certificate&, const PublicKey&);
void set_alternative_signature(v3::Certificate&, const Signature&);
void clear_alternative_public_key(v3::Certificate&);
void clear_alternative_signature(v3::Certificate&);
/**
* Calculate the certificate signature hash.
*
* The construction follows the IEEE 1609.2 certificate signature input:
* H(H(COER(toBeSigned)) || H(COER(canonical issuer certificate))). For a
* self-signed certificate the issuer input is empty. The alternative layer
* omits altSignatureValue from toBeSigned; the primary layer includes it.
*/
ByteBuffer calculate_certificate_hash(
::vanetza::security::Backend&, HashAlgorithm, const v3::CertificateView& subject,
const v3::CertificateView* issuer, SignatureLayer);
/** Sign or verify the outer, classical certificate signature. */
void sign_primary_certificate(
v3::Certificate&, const v3::CertificateView* issuer,
::vanetza::security::Backend&, const ::vanetza::security::PrivateKey& issuer_key);
bool verify_primary_certificate(
const v3::CertificateView&, const v3::CertificateView* issuer,
::vanetza::security::Backend&);
/** Sign or verify the inner FN-DSA-512 certificate signature. */
void sign_alternative_certificate(
v3::Certificate&, const v3::CertificateView* issuer,
::vanetza::security::Backend& hash_backend, Backend&,
const PrivateKey& issuer_key);
bool verify_alternative_certificate(
const v3::CertificateView&, const v3::CertificateView* issuer,
::vanetza::security::Backend&, Backend&);
} // namespace pqc
} // namespace security
} // namespace vanetza
@@ -0,0 +1,153 @@
#include <vanetza/security/pqc/hybrid_certificate_validator.hpp>
#include <vanetza/security/pqc/hybrid_certificate.hpp>
#include <vanetza/security/backend.hpp>
#include <vanetza/security/v3/certificate.hpp>
#include <vanetza/security/v3/issuer_lookup.hpp>
#include <vanetza/security/v3/trust_store.hpp>
namespace vanetza
{
namespace security
{
namespace pqc
{
auto HybridCertificateValidator::valid_for_signing(
const v3::CertificateView& certificate, ItsAid aid) -> Verdict
{
const Verdict policy_verdict = m_policy_validator.valid_for_signing(certificate, aid);
if (policy_verdict != Verdict::Valid) {
return policy_verdict;
}
if (!m_issuer_lookup || !m_trust_store || !m_ecc_backend ||
(m_verification_policy != VerificationPolicy::ClassicalOnly && !m_pqc_backend)) {
return Verdict::Misconfiguration;
}
return chain_is_authentic(certificate) ? Verdict::Valid : Verdict::Untrusted;
}
void HybridCertificateValidator::use_runtime(const Runtime* runtime)
{
m_policy_validator.use_runtime(runtime);
}
void HybridCertificateValidator::use_position_provider(PositionProvider* provider)
{
m_policy_validator.use_position_provider(provider);
}
void HybridCertificateValidator::use_issuer_lookup(const v3::IssuerLookup* lookup)
{
m_issuer_lookup = lookup;
m_policy_validator.use_issuer_lookup(lookup);
}
void HybridCertificateValidator::use_location_checker(const v3::LocationChecker* checker)
{
m_policy_validator.use_location_checker(checker);
}
void HybridCertificateValidator::use_revocation_lookup(const v3::RevocationLookup* lookup)
{
m_policy_validator.use_revocation_lookup(lookup);
}
void HybridCertificateValidator::use_trust_store(const v3::TrustStore* store)
{
m_trust_store = store;
m_policy_validator.use_trust_store(store);
}
void HybridCertificateValidator::use_backends(
::vanetza::security::Backend* ecc, Backend* pqc_backend)
{
m_ecc_backend = ecc;
m_pqc_backend = pqc_backend;
}
void HybridCertificateValidator::use_verification_policy(VerificationPolicy policy)
{
m_verification_policy = policy;
}
void HybridCertificateValidator::disable_time_checks(bool disable)
{
m_policy_validator.disable_time_checks(disable);
}
void HybridCertificateValidator::disable_location_checks(bool disable)
{
m_policy_validator.disable_location_checks(disable);
}
void HybridCertificateValidator::disable_chain_consistency_checks(bool disable)
{
m_policy_validator.disable_chain_consistency_checks(disable);
}
void HybridCertificateValidator::disable_region_consistency_checks(bool disable)
{
m_policy_validator.disable_region_consistency_checks(disable);
}
bool HybridCertificateValidator::alternative_signature_is_accepted(
const v3::CertificateView& subject, const v3::CertificateView* issuer) const
{
if (m_verification_policy == VerificationPolicy::ClassicalOnly) {
return true;
}
const auto state = alternative_material_state(subject);
if (state == MaterialState::Inconsistent) {
return false;
}
if (state == MaterialState::None) {
return m_verification_policy == VerificationPolicy::HybridIfPresent;
}
return m_pqc_backend && verify_alternative_certificate(
subject, issuer, *m_ecc_backend, *m_pqc_backend);
}
bool HybridCertificateValidator::chain_is_authentic(
const v3::CertificateView& signing_certificate) const
{
constexpr int maximum_chain_depth = 8;
const v3::CertificateView* subject = &signing_certificate;
for (int depth = 0; depth < maximum_chain_depth; ++depth) {
if (subject->issuer_is_self()) {
const auto root_digest = subject->calculate_digest();
if (!root_digest || m_trust_store->lookup(*root_digest).empty()) {
return false;
}
return verify_primary_certificate(*subject, nullptr, *m_ecc_backend) &&
alternative_signature_is_accepted(*subject, nullptr);
}
const auto expected_issuer_digest = subject->issuer_digest();
if (!expected_issuer_digest) {
return false;
}
const v3::Certificate* issuer = m_issuer_lookup->find_issuer(*expected_issuer_digest);
if (!issuer) {
return false;
}
const auto actual_issuer_digest = issuer->calculate_digest();
if (!actual_issuer_digest || *actual_issuer_digest != *expected_issuer_digest) {
return false;
}
if (!verify_primary_certificate(*subject, issuer, *m_ecc_backend) ||
!alternative_signature_is_accepted(*subject, issuer)) {
return false;
}
subject = issuer;
}
return false;
}
} // namespace pqc
} // namespace security
} // namespace vanetza
@@ -0,0 +1,77 @@
#pragma once
#include <vanetza/security/v3/certificate_validator.hpp>
#include <vanetza/security/pqc/fndsa512.hpp>
namespace vanetza
{
class PositionProvider;
class Runtime;
namespace security
{
class Backend;
namespace v3
{
class CertificateView;
class IssuerLookup;
class LocationChecker;
class RevocationLookup;
class TrustStore;
} // namespace v3
namespace pqc
{
/**
* Default V3 policy checks plus cryptographic certificate-chain validation.
*
* This class is compiled only for the experimental profile. Message
* signatures remain classical ECC; this validator concerns the nested
* signatures on certificates in the Root -> AA -> AT chain.
*/
class HybridCertificateValidator : public v3::CertificateValidator
{
public:
enum class VerificationPolicy
{
ClassicalOnly,
HybridIfPresent,
HybridRequired,
};
Verdict valid_for_signing(const v3::CertificateView&, ItsAid) override;
void use_runtime(const Runtime*);
void use_position_provider(PositionProvider*);
void use_issuer_lookup(const v3::IssuerLookup*);
void use_location_checker(const v3::LocationChecker*);
void use_revocation_lookup(const v3::RevocationLookup*);
void use_trust_store(const v3::TrustStore*);
void use_backends(::vanetza::security::Backend*, Backend*);
void use_verification_policy(VerificationPolicy);
void disable_time_checks(bool);
void disable_location_checks(bool);
void disable_chain_consistency_checks(bool);
void disable_region_consistency_checks(bool);
private:
bool chain_is_authentic(const v3::CertificateView&) const;
bool alternative_signature_is_accepted(
const v3::CertificateView& subject, const v3::CertificateView* issuer) const;
v3::DefaultCertificateValidator m_policy_validator;
const v3::IssuerLookup* m_issuer_lookup = nullptr;
const v3::TrustStore* m_trust_store = nullptr;
::vanetza::security::Backend* m_ecc_backend = nullptr;
Backend* m_pqc_backend = nullptr;
VerificationPolicy m_verification_policy = VerificationPolicy::HybridRequired;
};
} // namespace pqc
} // namespace security
} // namespace vanetza
@@ -0,0 +1,4 @@
include(UseGTest)
configure_gtest_directory(LINK_LIBRARIES geodesy security
COMPILE_DEFINITIONS ASSET_DIR="${SECURITY_TEST_ASSET_DIR}")
add_gtest(HybridPqcCertificate hybrid_certificate.cpp)
@@ -0,0 +1,441 @@
#include <vanetza/asn1/security_profile.hpp>
#include VANETZA_ASN1_SECURITY_HEADER(Certificate.h)
#include VANETZA_ASN1_SECURITY_HEADER(PsidGroupPermissions.h)
#include <vanetza/common/its_aid.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/asn1_conversions.hpp>
#include <vanetza/security/v3/certificate.hpp>
#include <vanetza/security/v3/issuer_memory_lookup.hpp>
#include <vanetza/security/v3/trust_store.hpp>
#include <gtest/gtest.h>
#include <array>
#include <stdexcept>
using namespace vanetza;
using namespace vanetza::security;
using namespace vanetza::security::v3;
namespace
{
PrivateKey to_private_key(const ecdsa256::PrivateKey& input)
{
PrivateKey output;
output.type = KeyType::NistP256;
output.key.assign(input.key.begin(), input.key.end());
return output;
}
void set_verification_key(Certificate& certificate, const ecdsa256::PublicKey& key)
{
auto& indicator = certificate->toBeSigned.verifyKeyIndicator;
indicator.present = Vanetza_Security_VerificationKeyIndicator_PR_verificationKey;
auto& verification_key = indicator.choice.verificationKey;
verification_key.present = Vanetza_Security_PublicVerificationKey_PR_ecdsaNistP256;
auto& point = verification_key.choice.ecdsaNistP256;
point.present = Vanetza_Security_EccP256CurvePoint_PR_uncompressedP256;
OCTET_STRING_fromBuf(
&point.choice.uncompressedP256.x,
reinterpret_cast<const char*>(key.x.data()), key.x.size());
OCTET_STRING_fromBuf(
&point.choice.uncompressedP256.y,
reinterpret_cast<const char*>(key.y.data()), key.y.size());
}
void set_issuer(Certificate& certificate, const Certificate* issuer)
{
if (!issuer) {
certificate->issuer.present = Vanetza_Security_IssuerIdentifier_PR_self;
certificate->issuer.choice.self = Vanetza_Security_HashAlgorithm_sha256;
return;
}
const auto digest = issuer->calculate_digest();
if (!digest) {
throw std::runtime_error("issuer certificate has no digest");
}
certificate->issuer.present = Vanetza_Security_IssuerIdentifier_PR_sha256AndDigest;
OCTET_STRING_fromBuf(
&certificate->issuer.choice.sha256AndDigest,
reinterpret_cast<const char*>(digest->data()), digest->size());
}
void add_all_issue_permissions(Certificate& certificate)
{
auto* permissions = vanetza::asn1::allocate<v3::asn1::PsidGroupPermissions>();
permissions->subjectPermissions.present = Vanetza_Security_SubjectPermissions_PR_all;
permissions->subjectPermissions.choice.all = 0;
certificate.add_cert_issue_permission(permissions);
}
Certificate make_certificate(
const ecdsa256::PublicKey& subject_key, const Certificate* issuer, bool authority)
{
Certificate certificate;
certificate->version = 3;
certificate->type = Vanetza_Security_CertificateType_explicit;
set_issuer(certificate, issuer);
if (authority) {
static const char name[] = "Hybrid test CA";
certificate->toBeSigned.id.present = Vanetza_Security_CertificateId_PR_name;
OCTET_STRING_fromBuf(
&certificate->toBeSigned.id.choice.name, name, sizeof(name) - 1);
add_all_issue_permissions(certificate);
} else {
certificate->toBeSigned.id.present = Vanetza_Security_CertificateId_PR_none;
certificate.add_app_permission(aid::CA, ByteBuffer { 1, 0, 0 });
}
static const std::array<char, 3> craca_id {{ 0, 0, 0 }};
OCTET_STRING_fromBuf(
&certificate->toBeSigned.cracaId, craca_id.data(), craca_id.size());
certificate->toBeSigned.crlSeries = 0;
certificate->toBeSigned.validityPeriod.start = 0;
certificate->toBeSigned.validityPeriod.duration.present =
Vanetza_Security_Duration_PR_years;
certificate->toBeSigned.validityPeriod.duration.choice.years = 10;
set_verification_key(certificate, subject_key);
return certificate;
}
void sign_hybrid(
Certificate& subject, const Certificate* issuer,
Backend& ecc_backend, pqc::Backend& pqc_backend,
const PrivateKey& ecc_key, const pqc::PrivateKey& pqc_key)
{
pqc::sign_alternative_certificate(
subject, issuer, ecc_backend, pqc_backend, pqc_key);
pqc::sign_primary_certificate(subject, issuer, ecc_backend, ecc_key);
}
void set_unsupported_alternative_signature(Certificate& certificate)
{
pqc::clear_alternative_signature(certificate);
auto* signature = vanetza::asn1::allocate<v3::asn1::Signature>();
signature->present = Vanetza_Security_Signature_PR_ecdsaNistP256Signature;
auto& r = signature->choice.ecdsaNistP256Signature.rSig;
r.present = Vanetza_Security_EccP256CurvePoint_PR_x_only;
const std::array<char, 32> zeroes {{}};
OCTET_STRING_fromBuf(&r.choice.x_only, zeroes.data(), zeroes.size());
OCTET_STRING_fromBuf(
&signature->choice.ecdsaNistP256Signature.sSig,
zeroes.data(), zeroes.size());
certificate->toBeSigned.altSignatureValue = signature;
}
struct Chain
{
explicit Chain(bool hybrid_material = true) :
ecc_backend(create_backend_or_throw("default")),
pqc_backend(pqc::create_fndsa512_backend()),
root_ecc_pair(ecc_backend->generate_key_pair()),
aa_ecc_pair(ecc_backend->generate_key_pair()),
at_ecc_pair(ecc_backend->generate_key_pair()),
root_ecc_key(to_private_key(root_ecc_pair.private_key)),
aa_ecc_key(to_private_key(aa_ecc_pair.private_key)),
root_pqc_pair(pqc_backend->generate_key_pair()),
aa_pqc_pair(pqc_backend->generate_key_pair())
{
root = make_certificate(root_ecc_pair.public_key, nullptr, true);
if (hybrid_material) {
pqc::set_alternative_public_key(root, root_pqc_pair.public_key);
sign_hybrid(root, nullptr, *ecc_backend, *pqc_backend, root_ecc_key, root_pqc_pair.private_key);
} else {
pqc::sign_primary_certificate(root, nullptr, *ecc_backend, root_ecc_key);
}
aa = make_certificate(aa_ecc_pair.public_key, &root, true);
if (hybrid_material) {
pqc::set_alternative_public_key(aa, aa_pqc_pair.public_key);
sign_hybrid(aa, &root, *ecc_backend, *pqc_backend, root_ecc_key, root_pqc_pair.private_key);
} else {
pqc::sign_primary_certificate(aa, &root, *ecc_backend, root_ecc_key);
}
at = make_certificate(at_ecc_pair.public_key, &aa, false);
if (hybrid_material) {
sign_hybrid(at, &aa, *ecc_backend, *pqc_backend, aa_ecc_key, aa_pqc_pair.private_key);
} else {
pqc::sign_primary_certificate(at, &aa, *ecc_backend, aa_ecc_key);
}
}
std::unique_ptr<Backend> ecc_backend;
std::unique_ptr<pqc::Backend> pqc_backend;
ecdsa256::KeyPair root_ecc_pair;
ecdsa256::KeyPair aa_ecc_pair;
ecdsa256::KeyPair at_ecc_pair;
PrivateKey root_ecc_key;
PrivateKey aa_ecc_key;
pqc::KeyPair root_pqc_pair;
pqc::KeyPair aa_pqc_pair;
Certificate root;
Certificate aa;
Certificate at;
};
v3::CertificateValidator::Verdict validate(
Chain& chain, pqc::HybridCertificateValidator::VerificationPolicy policy)
{
TrustStore trust_store;
trust_store.insert(chain.root);
IssuerMemoryLookup issuer_lookup;
if (!issuer_lookup.insert(chain.root) || !issuer_lookup.insert(chain.aa)) {
throw std::runtime_error("cannot populate issuer lookup");
}
pqc::HybridCertificateValidator validator;
validator.use_issuer_lookup(&issuer_lookup);
validator.use_trust_store(&trust_store);
validator.use_backends(chain.ecc_backend.get(), chain.pqc_backend.get());
validator.use_verification_policy(policy);
validator.disable_time_checks(true);
validator.disable_location_checks(true);
return validator.valid_for_signing(chain.at, aid::CA);
}
} // namespace
TEST(HybridCertificate, fndsa512_backend_rejects_invalid_material_sizes)
{
auto backend = pqc::create_fndsa512_backend();
const auto key_pair = backend->generate_key_pair();
const ByteBuffer message { 1, 2, 3, 4 };
const auto signature = backend->sign(key_pair.private_key, message);
EXPECT_EQ(pqc::fndsa512_public_key_size, key_pair.public_key.bytes.size());
EXPECT_EQ(pqc::fndsa512_private_key_size, key_pair.private_key.bytes.size());
EXPECT_EQ(pqc::fndsa512_signature_size, signature.bytes.size());
EXPECT_TRUE(backend->verify(key_pair.public_key, message, signature));
pqc::PublicKey short_key { ByteBuffer(1, 0) };
pqc::PrivateKey short_private_key { ByteBuffer(1, 0) };
pqc::Signature short_signature { ByteBuffer(1, 0) };
EXPECT_THROW(backend->sign(short_private_key, message), std::invalid_argument);
EXPECT_FALSE(backend->verify(short_key, message, signature));
EXPECT_FALSE(backend->verify(key_pair.public_key, message, short_signature));
}
TEST(HybridCertificate, material_roundtrips_through_oer)
{
Chain chain;
Certificate decoded;
ASSERT_TRUE(decoded.decode(chain.at.encode()));
EXPECT_EQ(pqc::MaterialState::EndEntity, pqc::alternative_material_state(decoded));
EXPECT_FALSE(pqc::get_alternative_public_key(decoded));
const auto signature = pqc::get_alternative_signature(decoded);
ASSERT_TRUE(signature);
EXPECT_EQ(pqc::fndsa512_signature_size, signature->bytes.size());
Certificate decoded_aa;
ASSERT_TRUE(decoded_aa.decode(chain.aa.encode()));
EXPECT_EQ(pqc::MaterialState::Authority, pqc::alternative_material_state(decoded_aa));
ASSERT_TRUE(pqc::get_alternative_public_key(decoded_aa));
ASSERT_TRUE(pqc::get_alternative_signature(decoded_aa));
}
TEST(HybridCertificate, verifies_both_nested_signature_layers)
{
Chain chain;
EXPECT_TRUE(pqc::verify_alternative_certificate(
chain.root, nullptr, *chain.ecc_backend, *chain.pqc_backend));
EXPECT_TRUE(pqc::verify_primary_certificate(chain.root, nullptr, *chain.ecc_backend));
EXPECT_TRUE(pqc::verify_alternative_certificate(
chain.aa, &chain.root, *chain.ecc_backend, *chain.pqc_backend));
EXPECT_TRUE(pqc::verify_primary_certificate(chain.aa, &chain.root, *chain.ecc_backend));
EXPECT_TRUE(pqc::verify_alternative_certificate(
chain.at, &chain.aa, *chain.ecc_backend, *chain.pqc_backend));
EXPECT_TRUE(pqc::verify_primary_certificate(chain.at, &chain.aa, *chain.ecc_backend));
}
TEST(HybridCertificate, certificate_hash_rejects_a_non_matching_issuer)
{
Chain chain;
EXPECT_THROW(
pqc::calculate_certificate_hash(
*chain.ecc_backend, HashAlgorithm::SHA256, chain.at, &chain.root,
pqc::SignatureLayer::Primary),
std::invalid_argument);
EXPECT_FALSE(pqc::verify_primary_certificate(
chain.at, &chain.root, *chain.ecc_backend));
EXPECT_FALSE(pqc::verify_alternative_certificate(
chain.at, &chain.root, *chain.ecc_backend, *chain.pqc_backend));
}
TEST(HybridCertificate, signing_rejects_mismatched_alternative_private_key)
{
Chain chain;
const auto unrelated_key_pair = chain.pqc_backend->generate_key_pair();
pqc::clear_alternative_signature(chain.at);
EXPECT_THROW(
pqc::sign_alternative_certificate(
chain.at, &chain.aa, *chain.ecc_backend, *chain.pqc_backend,
unrelated_key_pair.private_key),
std::invalid_argument);
EXPECT_FALSE(pqc::get_alternative_signature(chain.at));
}
TEST(HybridCertificate, signing_rejects_mismatched_primary_private_key)
{
Chain chain;
const auto unrelated_key_pair = chain.ecc_backend->generate_key_pair();
EXPECT_THROW(
pqc::sign_primary_certificate(
chain.at, &chain.aa, *chain.ecc_backend,
to_private_key(unrelated_key_pair.private_key)),
std::invalid_argument);
EXPECT_TRUE(pqc::verify_primary_certificate(
chain.at, &chain.aa, *chain.ecc_backend));
}
TEST(HybridCertificate, primary_signature_covers_alternative_signature)
{
Chain chain;
auto signature = pqc::get_alternative_signature(chain.at);
ASSERT_TRUE(signature);
signature->bytes.front() ^= 0x01;
pqc::set_alternative_signature(chain.at, *signature);
EXPECT_FALSE(pqc::verify_alternative_certificate(
chain.at, &chain.aa, *chain.ecc_backend, *chain.pqc_backend));
EXPECT_FALSE(pqc::verify_primary_certificate(chain.at, &chain.aa, *chain.ecc_backend));
pqc::sign_primary_certificate(
chain.at, &chain.aa, *chain.ecc_backend, chain.aa_ecc_key);
EXPECT_TRUE(pqc::verify_primary_certificate(chain.at, &chain.aa, *chain.ecc_backend));
EXPECT_FALSE(pqc::verify_alternative_certificate(
chain.at, &chain.aa, *chain.ecc_backend, *chain.pqc_backend));
}
TEST(HybridCertificate, validator_accepts_authentic_hybrid_chain)
{
Chain chain;
EXPECT_EQ(CertificateValidator::Verdict::Valid,
validate(chain, pqc::HybridCertificateValidator::VerificationPolicy::HybridRequired));
}
TEST(HybridCertificate, validator_verifies_authority_certificates_in_the_chain)
{
Chain chain;
ASSERT_TRUE(chain.aa->signature);
ASSERT_EQ(Vanetza_Security_Signature_PR_ecdsaNistP256Signature,
chain.aa->signature->present);
auto& encoded_s = chain.aa->signature->choice.ecdsaNistP256Signature.sSig;
ASSERT_TRUE(encoded_s.buf);
ASSERT_GT(encoded_s.size, 0u);
encoded_s.buf[0] ^= 0x01;
// Reissue the AT for the modified AA so leaf verification still succeeds.
chain.at = make_certificate(chain.at_ecc_pair.public_key, &chain.aa, false);
sign_hybrid(
chain.at, &chain.aa, *chain.ecc_backend, *chain.pqc_backend,
chain.aa_ecc_key, chain.aa_pqc_pair.private_key);
EXPECT_TRUE(pqc::verify_primary_certificate(
chain.at, &chain.aa, *chain.ecc_backend));
EXPECT_FALSE(pqc::verify_primary_certificate(
chain.aa, &chain.root, *chain.ecc_backend));
EXPECT_EQ(CertificateValidator::Verdict::Untrusted,
validate(chain, pqc::HybridCertificateValidator::VerificationPolicy::HybridRequired));
}
TEST(HybridCertificate, validator_policy_is_explicit_for_classical_chains)
{
Chain chain(false);
EXPECT_EQ(CertificateValidator::Verdict::Untrusted,
validate(chain, pqc::HybridCertificateValidator::VerificationPolicy::HybridRequired));
EXPECT_EQ(CertificateValidator::Verdict::Valid,
validate(chain, pqc::HybridCertificateValidator::VerificationPolicy::HybridIfPresent));
EXPECT_EQ(CertificateValidator::Verdict::Valid,
validate(chain, pqc::HybridCertificateValidator::VerificationPolicy::ClassicalOnly));
}
TEST(HybridCertificate, hybrid_policy_rejects_incomplete_authority_material)
{
Chain chain;
pqc::clear_alternative_signature(chain.aa);
pqc::sign_primary_certificate(
chain.aa, &chain.root, *chain.ecc_backend, chain.root_ecc_key);
chain.at = make_certificate(chain.at_ecc_pair.public_key, &chain.aa, false);
sign_hybrid(
chain.at, &chain.aa, *chain.ecc_backend, *chain.pqc_backend,
chain.aa_ecc_key, chain.aa_pqc_pair.private_key);
EXPECT_EQ(pqc::MaterialState::Inconsistent,
pqc::alternative_material_state(chain.aa));
EXPECT_EQ(CertificateValidator::Verdict::Untrusted,
validate(chain, pqc::HybridCertificateValidator::VerificationPolicy::HybridIfPresent));
EXPECT_EQ(CertificateValidator::Verdict::Valid,
validate(chain, pqc::HybridCertificateValidator::VerificationPolicy::ClassicalOnly));
}
TEST(HybridCertificate, classical_policy_still_verifies_outer_signature)
{
Chain chain;
auto signature = pqc::get_alternative_signature(chain.at);
ASSERT_TRUE(signature);
signature->bytes.front() ^= 0x01;
pqc::set_alternative_signature(chain.at, *signature);
pqc::sign_primary_certificate(
chain.at, &chain.aa, *chain.ecc_backend, chain.aa_ecc_key);
EXPECT_EQ(CertificateValidator::Verdict::Untrusted,
validate(chain, pqc::HybridCertificateValidator::VerificationPolicy::HybridRequired));
EXPECT_EQ(CertificateValidator::Verdict::Untrusted,
validate(chain, pqc::HybridCertificateValidator::VerificationPolicy::HybridIfPresent));
EXPECT_EQ(CertificateValidator::Verdict::Valid,
validate(chain, pqc::HybridCertificateValidator::VerificationPolicy::ClassicalOnly));
}
TEST(HybridCertificate, every_policy_rejects_a_broken_primary_signature)
{
Chain chain;
ASSERT_TRUE(chain.at->signature);
ASSERT_EQ(Vanetza_Security_Signature_PR_ecdsaNistP256Signature,
chain.at->signature->present);
auto& encoded_s = chain.at->signature->choice.ecdsaNistP256Signature.sSig;
ASSERT_TRUE(encoded_s.buf);
ASSERT_GT(encoded_s.size, 0u);
encoded_s.buf[0] ^= 0x01;
EXPECT_EQ(CertificateValidator::Verdict::Untrusted,
validate(chain, pqc::HybridCertificateValidator::VerificationPolicy::HybridRequired));
EXPECT_EQ(CertificateValidator::Verdict::Untrusted,
validate(chain, pqc::HybridCertificateValidator::VerificationPolicy::HybridIfPresent));
EXPECT_EQ(CertificateValidator::Verdict::Untrusted,
validate(chain, pqc::HybridCertificateValidator::VerificationPolicy::ClassicalOnly));
}
TEST(HybridCertificate, authorization_ticket_rejects_alternative_public_key)
{
Chain chain;
pqc::set_alternative_public_key(chain.at, chain.aa_pqc_pair.public_key);
EXPECT_EQ(pqc::MaterialState::Inconsistent,
pqc::alternative_material_state(chain.at));
}
TEST(HybridCertificate, unsupported_alternative_choice_is_not_treated_as_absent)
{
Chain chain;
set_unsupported_alternative_signature(chain.at);
pqc::sign_primary_certificate(
chain.at, &chain.aa, *chain.ecc_backend, chain.aa_ecc_key);
EXPECT_EQ(pqc::MaterialState::Inconsistent,
pqc::alternative_material_state(chain.at));
EXPECT_EQ(CertificateValidator::Verdict::Untrusted,
validate(chain, pqc::HybridCertificateValidator::VerificationPolicy::HybridIfPresent));
}