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:
@@ -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
|
||||
+386
@@ -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
|
||||
+81
@@ -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
|
||||
+153
@@ -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
|
||||
+77
@@ -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)
|
||||
+441
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user