Keep the colleague's microbu-esp32c5 tree in this repository
obu-firmware builds against vanetza-idf from microbu-esp32c5/external, but that tree was gitignored, so a clone of this repository could not build the firmware it ships. It is now committed here as ordinary files in its own folder, microbu-esp32c5/: the colleague's commit cf4b99f plus the V2X2MAP bridge's signature verification (--trust) used on the bench. Nothing is fetched from or pushed to the colleague's repository; this repository and its remotes carry everything. The folder's own .gitignore keeps build output, downloaded components and private key material out, as it did there; the committed file set is identical to that repository's tracked files. The ESP32-C5 is still flashed from obu-firmware/, which only takes vanetza-idf from microbu-esp32c5/, so the two stay separate folders. FLASHING.md says how to take a newer version of the colleague's tree (copy it over the folder, rebuild, test, commit).
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
include(UseGTest)
|
||||
|
||||
configure_gtest_directory(
|
||||
INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/..
|
||||
COMPILE_DEFINITIONS OPENSSL_API_COMPAT=0x10101000L
|
||||
LINK_LIBRARIES pki_library)
|
||||
add_gtest(CertificateRevocationList certificate_revocation_list.cpp)
|
||||
add_gtest(CertificateRole certificate_role.cpp)
|
||||
add_gtest(CertificateTrustList certificate_trust_list.cpp)
|
||||
add_gtest(CredentialFilesystemStorage credential_filesystem_storage.cpp)
|
||||
add_gtest(CrlFilesystemStore crl_store.cpp)
|
||||
add_gtest(AtRequest at_request.cpp)
|
||||
add_gtest(AtResponse at_response.cpp)
|
||||
add_gtest(EaRequest ea_request.cpp)
|
||||
add_gtest(EaResponse ea_response.cpp)
|
||||
add_gtest(Ecies ecies.cpp)
|
||||
add_gtest(HexString hexstring.cpp)
|
||||
add_gtest(Http http.cpp)
|
||||
add_gtest(Keys keys.cpp)
|
||||
add_gtest(Pem pem.cpp)
|
||||
add_gtest(PkiTime time.cpp)
|
||||
add_gtest(SecurityModuleNG security_module.cpp)
|
||||
add_gtest(SignedBuilder signed_builder.cpp)
|
||||
add_gtest(PruneCommand prune_command.cpp)
|
||||
@@ -0,0 +1,281 @@
|
||||
#include "at_request.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "encrypted_data.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include "signed_data.hpp"
|
||||
#include "stub_certificate.hpp"
|
||||
#include "validation.hpp"
|
||||
#include <vanetza/asn1/asn1c_wrapper.hpp>
|
||||
#include <vanetza/asn1/security/EtsiTs102941Data.h>
|
||||
#include <vanetza/asn1/security/InnerAtRequest.h>
|
||||
#include <vanetza/asn1/security/SharedAtRequest.h>
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <vanetza/common/clock.hpp>
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
#include <vanetza/security/v3/basic_elements.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class AuthorizationRequestTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
AuthorizationRequestTest() : m_credentials(std::make_shared<MockCredentialStorage>()), m_security(m_credentials)
|
||||
{
|
||||
// EA / AA need both verification and encryption keys; EC needs only verification.
|
||||
PublicKey ea_verify = m_security.create_key(KeyType::NistP256);
|
||||
PublicKey ea_encrypt = m_security.create_key(KeyType::NistP256);
|
||||
m_ea = build_stub_certificate(ea_verify, &ea_encrypt);
|
||||
|
||||
PublicKey aa_verify = m_security.create_key(KeyType::NistP256);
|
||||
PublicKey aa_encrypt = m_security.create_key(KeyType::NistP256);
|
||||
m_aa = build_stub_certificate(aa_verify, &aa_encrypt);
|
||||
|
||||
m_ec_key = m_security.create_key(KeyType::NistP256);
|
||||
m_ec = build_stub_certificate(m_ec_key);
|
||||
}
|
||||
|
||||
AuthorizationRequestParameters make_params()
|
||||
{
|
||||
AuthorizationRequestParameters p;
|
||||
p.ec = &m_ec;
|
||||
p.ea_certificate = &m_ea;
|
||||
p.aa_certificate = &m_aa;
|
||||
p.verification_key = m_security.create_key(KeyType::NistP256);
|
||||
p.permissions = { PsidSsp { aid::CA, { 0x01, 0x00, 0x00 } } };
|
||||
return p;
|
||||
}
|
||||
|
||||
// Decode `bytes` as an EtsiTs102941Data, transparently unwrapping a POP
|
||||
// EtsiTs103097Data-Signed envelope when present (the default for AT
|
||||
// requests; TS 102 941 §6.2.3.3.1).
|
||||
bool decode_inner_management_data(const ByteBuffer& bytes,
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t>& mgmt)
|
||||
{
|
||||
SignedData pop;
|
||||
if (pop.decode(bytes) && pop->content->present == Vanetza_Security_Ieee1609Dot2Content_PR_signedData) {
|
||||
const auto* sd = pop->content->choice.signedData;
|
||||
const auto& payload = sd->tbsData->payload->data->content->choice.unsecuredData;
|
||||
return mgmt.decode(payload.buf, payload.size);
|
||||
}
|
||||
return mgmt.decode(bytes.data(), bytes.size());
|
||||
}
|
||||
|
||||
std::shared_ptr<MockCredentialStorage> m_credentials;
|
||||
OpenSslSecurityModule m_security;
|
||||
Certificate m_ea;
|
||||
Certificate m_aa;
|
||||
Certificate m_ec;
|
||||
PublicKey m_ec_key;
|
||||
};
|
||||
|
||||
// Empty permission list violates CertificateSubjectAttributes invariants and
|
||||
// makes the request meaningless. The builder rejects it early.
|
||||
TEST_F(AuthorizationRequestTest, rejects_empty_permissions)
|
||||
{
|
||||
auto params = make_params();
|
||||
params.permissions.clear();
|
||||
EXPECT_THROW(build_signed_authorization_request(m_security, params), std::invalid_argument);
|
||||
}
|
||||
|
||||
// Required certificates: ec, ea, aa. Missing any → throw.
|
||||
TEST_F(AuthorizationRequestTest, rejects_missing_certificates)
|
||||
{
|
||||
{
|
||||
auto params = make_params();
|
||||
params.ec = nullptr;
|
||||
EXPECT_THROW(build_signed_authorization_request(m_security, params), std::invalid_argument);
|
||||
}
|
||||
{
|
||||
auto params = make_params();
|
||||
params.ea_certificate = nullptr;
|
||||
EXPECT_THROW(build_signed_authorization_request(m_security, params), std::invalid_argument);
|
||||
}
|
||||
{
|
||||
auto params = make_params();
|
||||
params.aa_certificate = nullptr;
|
||||
EXPECT_THROW(build_authorization_request(m_security, params), std::invalid_argument);
|
||||
}
|
||||
}
|
||||
|
||||
// With include_pop = true (default), the bytes returned are an
|
||||
// EtsiTs103097Data-Signed envelope (signer = self) carrying the
|
||||
// EtsiTs102941Data{authorizationRequest} as unsecuredData payload, and the
|
||||
// signature verifies under the new AT verification key.
|
||||
// (TS 102 941 §6.2.3.3.1 AuthorizationRequestMessageWithPop.)
|
||||
TEST_F(AuthorizationRequestTest, default_includes_pop_signed_envelope)
|
||||
{
|
||||
auto params = make_params();
|
||||
ByteBuffer encoded = build_signed_authorization_request(m_security, params);
|
||||
|
||||
SignedData pop;
|
||||
ASSERT_TRUE(pop.decode(encoded));
|
||||
ASSERT_EQ(Vanetza_Security_Ieee1609Dot2Content_PR_signedData, pop->content->present);
|
||||
const auto& sd = *pop->content->choice.signedData;
|
||||
EXPECT_EQ(Vanetza_Security_SignerIdentifier_PR_self, sd.signer.present);
|
||||
EXPECT_EQ(aid::SCR, sd.tbsData->headerInfo.psid);
|
||||
|
||||
// Signature verifies under the verification key (signer = self → empty signer cert)
|
||||
Sha256Hash digest = calculate_digest<Sha256Hash>(m_security, *sd.tbsData, nullptr);
|
||||
EXPECT_TRUE(m_security.verify(digest, make_signature(sd.signature), params.verification_key));
|
||||
}
|
||||
|
||||
// With include_pop = false, the bytes returned are the bare EtsiTs102941Data
|
||||
// (AuthorizationRequestMessage variant — no POP envelope).
|
||||
TEST_F(AuthorizationRequestTest, opt_out_of_pop_returns_bare_management_data)
|
||||
{
|
||||
auto params = make_params();
|
||||
params.include_pop = false;
|
||||
ByteBuffer encoded = build_signed_authorization_request(m_security, params);
|
||||
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
|
||||
ASSERT_TRUE(mgmt.decode(encoded.data(), encoded.size()));
|
||||
EXPECT_EQ(Vanetza_Security_Version_v1, mgmt->version);
|
||||
EXPECT_EQ(Vanetza_Security_EtsiTs102941DataContent_PR_authorizationRequest, mgmt->content.present);
|
||||
}
|
||||
|
||||
// SharedAtRequest fields per TS 102 941 §6.2.3.3.1: eaId = HashedId8(EA),
|
||||
// keyTag (16 bytes), certificateFormat = ts103097v131. With no AT
|
||||
// encryption key, keyTag = first 16 bytes of HMAC-SHA256(hmacKey, verifyKey).
|
||||
TEST_F(AuthorizationRequestTest, shared_at_request_fields_match_spec)
|
||||
{
|
||||
auto params = make_params();
|
||||
ByteBuffer encoded = build_signed_authorization_request(m_security, params);
|
||||
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
|
||||
ASSERT_TRUE(decode_inner_management_data(encoded, mgmt));
|
||||
const auto& iar = mgmt->content.choice.authorizationRequest;
|
||||
const auto& sar = iar.sharedAtRequest;
|
||||
|
||||
HashedId8 ea_hid8 = m_ea.calculate_hashed_id8(m_security);
|
||||
EXPECT_TRUE(sar.eaId == ea_hid8.octets);
|
||||
EXPECT_EQ(Vanetza_Security_CertificateFormat_ts103097v131, sar.certificateFormat);
|
||||
EXPECT_EQ(16, sar.keyTag.size);
|
||||
|
||||
// Default params have no at_encryption_key; encryptionKey must be absent
|
||||
// and keyTag is HMAC over verifyKey only.
|
||||
EXPECT_EQ(nullptr, iar.publicKeys.encryptionKey);
|
||||
|
||||
ByteBuffer verify_oer =
|
||||
asn1::encode_oer(asn_DEF_Vanetza_Security_PublicVerificationKey, &iar.publicKeys.verificationKey);
|
||||
ByteBuffer hmac_key(iar.hmacKey.buf, iar.hmacKey.buf + iar.hmacKey.size);
|
||||
EXPECT_EQ(32, hmac_key.size());
|
||||
ByteBuffer expected_tag = m_security.calculate_hmac_sha256(hmac_key, verify_oer);
|
||||
EXPECT_EQ(0, std::memcmp(sar.keyTag.buf, expected_tag.data(), 16));
|
||||
|
||||
ASSERT_NE(nullptr, sar.requestedSubjectAttributes.appPermissions);
|
||||
ASSERT_EQ(1, sar.requestedSubjectAttributes.appPermissions->list.count);
|
||||
EXPECT_EQ(static_cast<long>(aid::CA), sar.requestedSubjectAttributes.appPermissions->list.array[0]->psid);
|
||||
}
|
||||
|
||||
// When at_encryption_key is provided, it appears in publicKeys and is folded
|
||||
// into the keyTag HMAC input (verifyKey || encKey).
|
||||
TEST_F(AuthorizationRequestTest, optional_at_encryption_key_extends_keytag)
|
||||
{
|
||||
auto params = make_params();
|
||||
params.at_encryption_key = m_security.create_key(KeyType::NistP256);
|
||||
ByteBuffer encoded = build_signed_authorization_request(m_security, params);
|
||||
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
|
||||
ASSERT_TRUE(decode_inner_management_data(encoded, mgmt));
|
||||
const auto& iar = mgmt->content.choice.authorizationRequest;
|
||||
|
||||
ASSERT_NE(nullptr, iar.publicKeys.encryptionKey);
|
||||
|
||||
ByteBuffer verify_oer =
|
||||
asn1::encode_oer(asn_DEF_Vanetza_Security_PublicVerificationKey, &iar.publicKeys.verificationKey);
|
||||
ByteBuffer enc_oer = asn1::encode_oer(asn_DEF_Vanetza_Security_PublicEncryptionKey, iar.publicKeys.encryptionKey);
|
||||
ByteBuffer combined;
|
||||
combined.insert(combined.end(), verify_oer.begin(), verify_oer.end());
|
||||
combined.insert(combined.end(), enc_oer.begin(), enc_oer.end());
|
||||
ByteBuffer hmac_key(iar.hmacKey.buf, iar.hmacKey.buf + iar.hmacKey.size);
|
||||
ByteBuffer expected_tag = m_security.calculate_hmac_sha256(hmac_key, combined);
|
||||
EXPECT_EQ(0, std::memcmp(iar.sharedAtRequest.keyTag.buf, expected_tag.data(), 16));
|
||||
}
|
||||
|
||||
// ecSignature is the privacy-preserving `encryptedEcSignature` variant; the
|
||||
// inner ciphertext is addressed to the EA (recipientId = HashedId8(EA)).
|
||||
TEST_F(AuthorizationRequestTest, ec_signature_is_encrypted_to_ea)
|
||||
{
|
||||
auto params = make_params();
|
||||
ByteBuffer encoded = build_signed_authorization_request(m_security, params);
|
||||
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
|
||||
ASSERT_TRUE(decode_inner_management_data(encoded, mgmt));
|
||||
const auto& iar = mgmt->content.choice.authorizationRequest;
|
||||
ASSERT_EQ(Vanetza_Security_EcSignature_PR_encryptedEcSignature, iar.ecSignature.present);
|
||||
|
||||
const auto& enc_sig = iar.ecSignature.choice.encryptedEcSignature;
|
||||
ASSERT_EQ(Vanetza_Security_Ieee1609Dot2Content_PR_encryptedData, enc_sig.content->present);
|
||||
const auto& recipients = enc_sig.content->choice.encryptedData.recipients;
|
||||
ASSERT_GE(recipients.list.count, 1);
|
||||
|
||||
HashedId8 ea_hid8 = m_ea.calculate_hashed_id8(m_security);
|
||||
const auto* recip = recipients.list.array[0];
|
||||
ASSERT_EQ(Vanetza_Security_RecipientInfo_PR_certRecipInfo, recip->present);
|
||||
EXPECT_TRUE(recip->choice.certRecipInfo.recipientId == ea_hid8.octets);
|
||||
}
|
||||
|
||||
// By default no validityPeriod hint is sent (TS 102 941 §6.2.3.3.1: optional).
|
||||
// The AA picks the validity unilaterally.
|
||||
TEST_F(AuthorizationRequestTest, default_omits_validity_period_hint)
|
||||
{
|
||||
auto params = make_params();
|
||||
ByteBuffer encoded = build_signed_authorization_request(m_security, params);
|
||||
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
|
||||
ASSERT_TRUE(decode_inner_management_data(encoded, mgmt));
|
||||
const auto& sar = mgmt->content.choice.authorizationRequest.sharedAtRequest;
|
||||
EXPECT_EQ(nullptr, sar.requestedSubjectAttributes.validityPeriod);
|
||||
}
|
||||
|
||||
// When `validity_period` is set, the SharedAtRequest carries a validityPeriod
|
||||
// hint with the requested start (encoded as IEEE 1609.2 Time32) and duration
|
||||
// (hours variant). Per CP §7.2.1, duration ≤ 1 week.
|
||||
TEST_F(AuthorizationRequestTest, validity_period_hint_propagates_to_shared_at_request)
|
||||
{
|
||||
auto params = make_params();
|
||||
Clock::time_point start = Clock::time_point { std::chrono::hours(24 * 7) }; // arbitrary t > epoch
|
||||
ValidityPeriodHint hint;
|
||||
hint.start = start;
|
||||
hint.duration = std::chrono::hours(168);
|
||||
params.validity_period = hint;
|
||||
|
||||
ByteBuffer encoded = build_signed_authorization_request(m_security, params);
|
||||
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
|
||||
ASSERT_TRUE(decode_inner_management_data(encoded, mgmt));
|
||||
const auto& sar = mgmt->content.choice.authorizationRequest.sharedAtRequest;
|
||||
ASSERT_NE(nullptr, sar.requestedSubjectAttributes.validityPeriod);
|
||||
|
||||
EXPECT_EQ(security::v3::convert_time32(start), sar.requestedSubjectAttributes.validityPeriod->start);
|
||||
ASSERT_EQ(Vanetza_Security_Duration_PR_hours, sar.requestedSubjectAttributes.validityPeriod->duration.present);
|
||||
EXPECT_EQ(168u, sar.requestedSubjectAttributes.validityPeriod->duration.choice.hours);
|
||||
}
|
||||
|
||||
// build_authorization_request returns an EncryptedData addressed to the AA.
|
||||
TEST_F(AuthorizationRequestTest, outer_encrypted_to_aa)
|
||||
{
|
||||
auto params = make_params();
|
||||
EncryptedData encrypted = build_authorization_request(m_security, params);
|
||||
|
||||
ASSERT_EQ(Vanetza_Security_Ieee1609Dot2Content_PR_encryptedData, encrypted->content->present);
|
||||
const auto& recipients = encrypted->content->choice.encryptedData.recipients;
|
||||
ASSERT_GE(recipients.list.count, 1);
|
||||
|
||||
HashedId8 aa_hid8 = m_aa.calculate_hashed_id8(m_security);
|
||||
const auto* recip = recipients.list.array[0];
|
||||
ASSERT_EQ(Vanetza_Security_RecipientInfo_PR_certRecipInfo, recip->present);
|
||||
EXPECT_TRUE(recip->choice.certRecipInfo.recipientId == aa_hid8.octets);
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,180 @@
|
||||
#include "at_response.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "exception.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include "stub_certificate.hpp"
|
||||
#include "time.hpp"
|
||||
#include "validation.hpp"
|
||||
#include <vanetza/asn1/asn1c_wrapper.hpp>
|
||||
#include <vanetza/asn1/security/AuthorizationResponseCode.h>
|
||||
#include <vanetza/asn1/security/EtsiTs102941Data.h>
|
||||
#include <vanetza/asn1/security/EtsiTs103097Certificate.h>
|
||||
#include <vanetza/asn1/security/Ieee1609Dot2Data.h>
|
||||
#include <vanetza/asn1/security/InnerAtResponse.h>
|
||||
#include <vanetza/asn1/security/SignedData.h>
|
||||
#include <vanetza/asn1/security/Time64.h>
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
#include <vanetza/security/v3/basic_elements.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr std::uint8_t protocol_version = 3;
|
||||
|
||||
// Builder for a decrypted authorization response. Defaults produce a valid
|
||||
// response with code=ok and a synthetic AT certificate. Knobs flip individual
|
||||
// invariants for negative tests.
|
||||
struct AtResponseBuilder
|
||||
{
|
||||
OpenSslSecurityModule* security;
|
||||
const Certificate* aa_cert;
|
||||
const PublicKey* aa_signing_key;
|
||||
|
||||
Vanetza_Security_AuthorizationResponseCode_t code = Vanetza_Security_AuthorizationResponseCode_ok;
|
||||
bool include_certificate = true;
|
||||
boost::optional<long> psid_override;
|
||||
boost::optional<HashedId8> signer_digest_override;
|
||||
|
||||
ByteBuffer build() const
|
||||
{
|
||||
// Inner: EtsiTs102941Data { authorizationResponse { InnerAtResponse } }
|
||||
MgmtData inner;
|
||||
inner->version = Vanetza_Security_Version_v1;
|
||||
inner->content.present = Vanetza_Security_EtsiTs102941DataContent_PR_authorizationResponse;
|
||||
auto& at_resp = inner->content.choice.authorizationResponse;
|
||||
|
||||
uint8_t req_hash[16];
|
||||
std::fill(std::begin(req_hash), std::end(req_hash), 0xCD);
|
||||
OCTET_STRING_fromBuf(&at_resp.requestHash, reinterpret_cast<char*>(req_hash), 16);
|
||||
at_resp.responseCode = code;
|
||||
if (include_certificate) {
|
||||
PublicKey dummy_key = security->create_key(KeyType::NistP256);
|
||||
Certificate dummy_at = build_stub_certificate(dummy_key);
|
||||
ByteBuffer encoded_at = dummy_at.encode();
|
||||
void* decoded = nullptr;
|
||||
EXPECT_TRUE(asn1::decode_oer(asn_DEF_Vanetza_Security_EtsiTs103097Certificate, &decoded, encoded_at));
|
||||
at_resp.certificate = static_cast<Vanetza_Security_EtsiTs103097Certificate*>(decoded);
|
||||
}
|
||||
EXPECT_TRUE(inner.validate());
|
||||
ByteBuffer inner_encoded = inner.encode();
|
||||
|
||||
// Outer: EtsiTs103097Data { signedData (signer=digest(AA)) }
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_Ieee1609Dot2Data_t> outer(asn_DEF_Vanetza_Security_Ieee1609Dot2Data);
|
||||
outer->protocolVersion = protocol_version;
|
||||
outer->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
outer->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_signedData;
|
||||
outer->content->choice.signedData = asn1::allocate<Vanetza_Security_SignedData_t>();
|
||||
auto& sd = *outer->content->choice.signedData;
|
||||
sd.hashId = Vanetza_Security_HashAlgorithm_sha256;
|
||||
|
||||
sd.signer.present = Vanetza_Security_SignerIdentifier_PR_digest;
|
||||
HashedId8 aa_hid8 = signer_digest_override.value_or(aa_cert->calculate_hashed_id8(*security));
|
||||
OCTET_STRING_fromBuf(&sd.signer.choice.digest, reinterpret_cast<const char*>(aa_hid8.octets.data()), 8);
|
||||
|
||||
sd.tbsData = asn1::allocate<Vanetza_Security_ToBeSignedData_t>();
|
||||
sd.tbsData->headerInfo.psid = psid_override.value_or(aid::SCR);
|
||||
sd.tbsData->headerInfo.generationTime = asn1::allocate<Vanetza_Security_Time64_t>();
|
||||
asn_uint642INTEGER(sd.tbsData->headerInfo.generationTime, security::v3::convert_time64(current_time()));
|
||||
|
||||
sd.tbsData->payload = asn1::allocate<Vanetza_Security_SignedDataPayload_t>();
|
||||
auto* sdp = sd.tbsData->payload;
|
||||
sdp->data = asn1::allocate<Vanetza_Security_EtsiTs103097Data_t>();
|
||||
sdp->data->protocolVersion = protocol_version;
|
||||
sdp->data->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
sdp->data->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_unsecuredData;
|
||||
copy(inner_encoded, sdp->data->content->choice.unsecuredData);
|
||||
|
||||
Sha256Hash digest = calculate_digest<Sha256Hash>(*security, *sd.tbsData, &aa_cert->raw());
|
||||
auto signature = security->sign(ByteBuffer { digest.octets.begin(), digest.octets.end() }, *aa_signing_key);
|
||||
EXPECT_TRUE(static_cast<bool>(signature));
|
||||
|
||||
sd.signature.present = Vanetza_Security_Signature_PR_ecdsaNistP256Signature;
|
||||
sd.signature.choice.ecdsaNistP256Signature.rSig.present = Vanetza_Security_EccP256CurvePoint_PR_x_only;
|
||||
copy_left_padded(signature->r, sd.signature.choice.ecdsaNistP256Signature.rSig.choice.x_only, 32);
|
||||
copy_left_padded(signature->s, sd.signature.choice.ecdsaNistP256Signature.sSig, 32);
|
||||
|
||||
return outer.encode();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class AuthorizationResponseTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
AuthorizationResponseTest() :
|
||||
m_credentials(std::make_shared<MockCredentialStorage>()), m_security(m_credentials),
|
||||
m_aa_key(m_security.create_key(KeyType::NistP256)), m_aa_certificate(build_stub_certificate(m_aa_key))
|
||||
{
|
||||
}
|
||||
|
||||
AtResponseBuilder builder()
|
||||
{
|
||||
return AtResponseBuilder { &m_security, &m_aa_certificate, &m_aa_key };
|
||||
}
|
||||
|
||||
std::shared_ptr<MockCredentialStorage> m_credentials;
|
||||
OpenSslSecurityModule m_security;
|
||||
PublicKey m_aa_key;
|
||||
Certificate m_aa_certificate;
|
||||
};
|
||||
|
||||
TEST_F(AuthorizationResponseTest, happy_path_returns_code_ok_and_certificate)
|
||||
{
|
||||
ByteBuffer payload = builder().build();
|
||||
AuthorizationResponse resp = parse_authorization_response(m_security, payload, m_aa_certificate);
|
||||
EXPECT_EQ(Vanetza_Security_AuthorizationResponseCode_ok, resp.code);
|
||||
ASSERT_TRUE(resp.certificate.has_value());
|
||||
EXPECT_EQ(16u, resp.request_hash.size());
|
||||
}
|
||||
|
||||
TEST_F(AuthorizationResponseTest, rejects_signer_digest_mismatch)
|
||||
{
|
||||
AtResponseBuilder b = builder();
|
||||
HashedId8 other;
|
||||
other.octets = { 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22 };
|
||||
b.signer_digest_override = other;
|
||||
ByteBuffer payload = b.build();
|
||||
EXPECT_THROW(parse_authorization_response(m_security, payload, m_aa_certificate), VerificationFailure);
|
||||
}
|
||||
|
||||
TEST_F(AuthorizationResponseTest, rejects_wrong_psid)
|
||||
{
|
||||
AtResponseBuilder b = builder();
|
||||
b.psid_override = 0x20; // arbitrary non-SCR
|
||||
ByteBuffer payload = b.build();
|
||||
EXPECT_THROW(parse_authorization_response(m_security, payload, m_aa_certificate), DecodingFailure);
|
||||
}
|
||||
|
||||
TEST_F(AuthorizationResponseTest, non_ok_code_returns_without_throwing)
|
||||
{
|
||||
AtResponseBuilder b = builder();
|
||||
b.code = Vanetza_Security_AuthorizationResponseCode_deniedpermissions;
|
||||
b.include_certificate = false;
|
||||
ByteBuffer payload = b.build();
|
||||
AuthorizationResponse resp = parse_authorization_response(m_security, payload, m_aa_certificate);
|
||||
EXPECT_EQ(Vanetza_Security_AuthorizationResponseCode_deniedpermissions, resp.code);
|
||||
EXPECT_FALSE(resp.certificate.has_value());
|
||||
}
|
||||
|
||||
TEST_F(AuthorizationResponseTest, rejects_ok_without_certificate)
|
||||
{
|
||||
AtResponseBuilder b = builder();
|
||||
b.code = Vanetza_Security_AuthorizationResponseCode_ok;
|
||||
b.include_certificate = false;
|
||||
ByteBuffer payload = b.build();
|
||||
EXPECT_THROW(parse_authorization_response(m_security, payload, m_aa_certificate), DecodingFailure);
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
#include "certificate_revocation_list.hpp"
|
||||
#include "crl_test_fixture.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class CertificateRevocationListTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
CertificateRevocationListTest() :
|
||||
m_credentials(std::make_shared<MockCredentialStorage>()), m_security(m_credentials)
|
||||
{
|
||||
}
|
||||
|
||||
std::shared_ptr<MockCredentialStorage> m_credentials;
|
||||
OpenSslSecurityModule m_security;
|
||||
};
|
||||
|
||||
TEST_F(CertificateRevocationListTest, decode_rejects_garbage)
|
||||
{
|
||||
CertificateRevocationList crl;
|
||||
EXPECT_FALSE(crl.decode(ByteBuffer { 0x00, 0x01, 0x02, 0x03 }));
|
||||
}
|
||||
|
||||
TEST_F(CertificateRevocationListTest, get_hashed_id8_returns_signer_digest)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
CertificateRevocationList crl = build_test_crl(issuer, { make_id(0x11) });
|
||||
|
||||
auto got = crl.get_hashed_id8(m_security);
|
||||
ASSERT_TRUE(got);
|
||||
EXPECT_EQ(issuer, *got);
|
||||
}
|
||||
|
||||
TEST_F(CertificateRevocationListTest, revoked_entries_returns_list)
|
||||
{
|
||||
HashedId8 a = make_id(0x11);
|
||||
HashedId8 b = make_id(0x22);
|
||||
HashedId8 c = make_id(0x33);
|
||||
CertificateRevocationList crl = build_test_crl(make_id(0xAB), { a, b, c });
|
||||
|
||||
auto entries = crl.revoked_entries();
|
||||
ASSERT_TRUE(entries);
|
||||
ASSERT_EQ(3u, entries->size());
|
||||
EXPECT_EQ(a, (*entries)[0]);
|
||||
EXPECT_EQ(b, (*entries)[1]);
|
||||
EXPECT_EQ(c, (*entries)[2]);
|
||||
}
|
||||
|
||||
TEST_F(CertificateRevocationListTest, revoked_entries_empty_is_valid)
|
||||
{
|
||||
CertificateRevocationList crl = build_test_crl(make_id(0xAB), {});
|
||||
|
||||
auto entries = crl.revoked_entries();
|
||||
ASSERT_TRUE(entries);
|
||||
EXPECT_TRUE(entries->empty());
|
||||
}
|
||||
|
||||
TEST_F(CertificateRevocationListTest, encode_decode_roundtrip)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
HashedId8 revoked = make_id(0x42);
|
||||
CertificateRevocationList original = build_test_crl(issuer, { revoked });
|
||||
|
||||
ByteBuffer bytes = original.encode();
|
||||
CertificateRevocationList decoded;
|
||||
ASSERT_TRUE(decoded.decode(bytes));
|
||||
|
||||
auto issuer_out = decoded.get_hashed_id8(m_security);
|
||||
ASSERT_TRUE(issuer_out);
|
||||
EXPECT_EQ(issuer, *issuer_out);
|
||||
|
||||
auto entries = decoded.revoked_entries();
|
||||
ASSERT_TRUE(entries);
|
||||
ASSERT_EQ(1u, entries->size());
|
||||
EXPECT_EQ(revoked, (*entries)[0]);
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,163 @@
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include <vanetza/asn1/security/EtsiTs103097Certificate.h>
|
||||
#include <vanetza/asn1/security/PsidGroupPermissions.h>
|
||||
#include <vanetza/asn1/security/PsidSsp.h>
|
||||
#include <vanetza/asn1/security/SequenceOfPsidGroupPermissions.h>
|
||||
#include <vanetza/asn1/security/SequenceOfPsidSsp.h>
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
#include <vanetza/security/public_key.hpp>
|
||||
#include <vanetza/security/v3/certificate.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
PublicKey make_key()
|
||||
{
|
||||
static auto credentials = std::make_shared<MockCredentialStorage>();
|
||||
static OpenSslSecurityModule sm(credentials);
|
||||
return sm.create_key(KeyType::BrainpoolP256r1);
|
||||
}
|
||||
|
||||
enum class Issuer
|
||||
{
|
||||
Self,
|
||||
Digest
|
||||
};
|
||||
|
||||
enum class CertId
|
||||
{
|
||||
Name,
|
||||
None
|
||||
};
|
||||
|
||||
void set_issuer(Vanetza_Security_EtsiTs103097Certificate_t& cert, Issuer issuer)
|
||||
{
|
||||
if (issuer == Issuer::Self) {
|
||||
cert.issuer.present = Vanetza_Security_IssuerIdentifier_PR_self;
|
||||
cert.issuer.choice.self = Vanetza_Security_HashAlgorithm_sha256;
|
||||
} else {
|
||||
static const std::array<char, 8> digest { 1, 2, 3, 4, 5, 6, 7, 8 };
|
||||
cert.issuer.present = Vanetza_Security_IssuerIdentifier_PR_sha256AndDigest;
|
||||
OCTET_STRING_fromBuf(&cert.issuer.choice.sha256AndDigest, digest.data(), digest.size());
|
||||
}
|
||||
}
|
||||
|
||||
void set_cert_id(Vanetza_Security_ToBeSignedCertificate_t& tbs, CertId id)
|
||||
{
|
||||
if (id == CertId::Name) {
|
||||
tbs.id.present = Vanetza_Security_CertificateId_PR_name;
|
||||
OCTET_STRING_fromBuf(&tbs.id.choice.name, "test-cert", 9);
|
||||
} else {
|
||||
tbs.id.present = Vanetza_Security_CertificateId_PR_none;
|
||||
}
|
||||
}
|
||||
|
||||
void add_app_perm(Vanetza_Security_ToBeSignedCertificate_t& tbs, ItsAid psid)
|
||||
{
|
||||
if (!tbs.appPermissions) {
|
||||
tbs.appPermissions = asn1::allocate<Vanetza_Security_SequenceOfPsidSsp_t>();
|
||||
}
|
||||
auto* ps = asn1::allocate<Vanetza_Security_PsidSsp_t>();
|
||||
ps->psid = static_cast<long>(psid);
|
||||
ASN_SEQUENCE_ADD(&tbs.appPermissions->list, ps);
|
||||
}
|
||||
|
||||
void add_cert_issue_perm(Vanetza_Security_ToBeSignedCertificate_t& tbs, ItsAid psid)
|
||||
{
|
||||
if (!tbs.certIssuePermissions) {
|
||||
tbs.certIssuePermissions = asn1::allocate<Vanetza_Security_SequenceOfPsidGroupPermissions_t>();
|
||||
}
|
||||
auto* group = asn1::allocate<Vanetza_Security_PsidGroupPermissions_t>();
|
||||
group->subjectPermissions.present = Vanetza_Security_SubjectPermissions_PR_explicit;
|
||||
vanetza::security::v3::add_psid_group_permission(group, psid, { 0x01 }, { 0xff });
|
||||
ASN_SEQUENCE_ADD(&tbs.certIssuePermissions->list, group);
|
||||
}
|
||||
|
||||
Certificate build(Issuer issuer, CertId id, const std::vector<ItsAid>& app_perms,
|
||||
const std::vector<ItsAid>& cert_issue_perms)
|
||||
{
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs103097Certificate_t>
|
||||
cert(asn_DEF_Vanetza_Security_EtsiTs103097Certificate);
|
||||
cert->version = 3;
|
||||
cert->type = Vanetza_Security_CertificateType_explicit;
|
||||
set_issuer(*cert, issuer);
|
||||
|
||||
auto& tbs = cert->toBeSigned;
|
||||
set_cert_id(tbs, id);
|
||||
tbs.cracaId.buf = static_cast<uint8_t*>(std::calloc(3, 1));
|
||||
tbs.cracaId.size = 3;
|
||||
tbs.crlSeries = 0;
|
||||
tbs.validityPeriod.start = 0;
|
||||
tbs.validityPeriod.duration.present = Vanetza_Security_Duration_PR_hours;
|
||||
tbs.validityPeriod.duration.choice.hours = 24;
|
||||
tbs.verifyKeyIndicator.present = Vanetza_Security_VerificationKeyIndicator_PR_verificationKey;
|
||||
set_verification_key(tbs.verifyKeyIndicator.choice.verificationKey, make_key());
|
||||
|
||||
for (ItsAid p : app_perms) {
|
||||
add_app_perm(tbs, p);
|
||||
}
|
||||
for (ItsAid p : cert_issue_perms) {
|
||||
add_cert_issue_perm(tbs, p);
|
||||
}
|
||||
|
||||
Certificate result;
|
||||
EXPECT_TRUE(result.decode(cert.encode()));
|
||||
return result;
|
||||
}
|
||||
|
||||
TEST(CertificateRole, root_ca_self_issued_with_cert_issue_and_crl_ctl_app_perms)
|
||||
{
|
||||
auto cert = build(Issuer::Self, CertId::Name, { aid::CRL, aid::CTL }, { aid::SCR });
|
||||
EXPECT_EQ(CertificateRole::RootCa, certificate_role(cert));
|
||||
}
|
||||
|
||||
TEST(CertificateRole, tlm_self_issued_signs_ctl_without_cert_issue)
|
||||
{
|
||||
auto cert = build(Issuer::Self, CertId::Name, { aid::CTL }, {});
|
||||
EXPECT_EQ(CertificateRole::Tlm, certificate_role(cert));
|
||||
}
|
||||
|
||||
TEST(CertificateRole, enrolment_authority_issues_scr)
|
||||
{
|
||||
auto cert = build(Issuer::Digest, CertId::Name, { aid::SCR }, { aid::SCR });
|
||||
EXPECT_EQ(CertificateRole::EnrolmentAuthority, certificate_role(cert));
|
||||
}
|
||||
|
||||
TEST(CertificateRole, authorization_authority_issues_services)
|
||||
{
|
||||
auto cert = build(Issuer::Digest, CertId::Name, { aid::SCR }, { aid::CA });
|
||||
EXPECT_EQ(CertificateRole::AuthorizationAuthority, certificate_role(cert));
|
||||
}
|
||||
|
||||
TEST(CertificateRole, enrolment_credential_has_certificate_id_name)
|
||||
{
|
||||
auto cert = build(Issuer::Digest, CertId::Name, { aid::SCR }, {});
|
||||
EXPECT_EQ(CertificateRole::EnrolmentCredential, certificate_role(cert));
|
||||
}
|
||||
|
||||
TEST(CertificateRole, authorization_ticket_has_certificate_id_none)
|
||||
{
|
||||
auto cert = build(Issuer::Digest, CertId::None, { aid::CA }, {});
|
||||
EXPECT_EQ(CertificateRole::AuthorizationTicket, certificate_role(cert));
|
||||
}
|
||||
|
||||
TEST(CertificateRole, unknown_when_no_profile_matches)
|
||||
{
|
||||
auto cert = build(Issuer::Self, CertId::Name, { aid::CA }, {});
|
||||
EXPECT_EQ(CertificateRole::Unknown, certificate_role(cert));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
#include "certificate_trust_list.hpp"
|
||||
#include "ctl_test_fixture.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include "stub_certificate.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class CertificateTrustListTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
CertificateTrustListTest() :
|
||||
m_credentials(std::make_shared<MockCredentialStorage>()),
|
||||
m_security(std::make_shared<OpenSslSecurityModule>(m_credentials))
|
||||
{
|
||||
}
|
||||
|
||||
std::shared_ptr<MockCredentialStorage> m_credentials;
|
||||
std::shared_ptr<OpenSslSecurityModule> m_security;
|
||||
};
|
||||
|
||||
TEST_F(CertificateTrustListTest, is_full_ctl_true_for_full)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
auto ctl = TestRcaCtlBuilder(issuer, true, 5).build();
|
||||
auto is_full = ctl.is_full_ctl();
|
||||
ASSERT_TRUE(is_full);
|
||||
EXPECT_TRUE(*is_full);
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, is_full_ctl_false_for_delta)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
auto ctl = TestRcaCtlBuilder(issuer, false, 6).build();
|
||||
auto is_full = ctl.is_full_ctl();
|
||||
ASSERT_TRUE(is_full);
|
||||
EXPECT_FALSE(*is_full);
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, ctl_sequence_returns_value)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
auto ctl = TestRcaCtlBuilder(issuer, true, 42).build();
|
||||
auto seq = ctl.ctl_sequence();
|
||||
ASSERT_TRUE(seq);
|
||||
EXPECT_EQ(42u, *seq);
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, get_hashed_id8_matches_signer)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
auto ctl = TestRcaCtlBuilder(issuer, true, 1).build();
|
||||
auto got = ctl.get_hashed_id8(*m_security);
|
||||
ASSERT_TRUE(got);
|
||||
EXPECT_EQ(issuer, *got);
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, processor_records_aa_from_full_ctl)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
PublicKey aa_key = m_security->create_key(KeyType::NistP256);
|
||||
auto ctl = TestRcaCtlBuilder(issuer, true, 1).add_aa(aa_key, "https://aa.example/v1").build();
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(ctl);
|
||||
|
||||
auto aa = processor.get_authorization_authority(issuer);
|
||||
ASSERT_TRUE(aa);
|
||||
EXPECT_EQ("https://aa.example/v1", aa->access_point);
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, processor_records_ea_from_full_ctl)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
PublicKey ea_key = m_security->create_key(KeyType::NistP256);
|
||||
auto ctl = TestRcaCtlBuilder(issuer, true, 1).add_ea(ea_key, "https://ea.example/aa").build();
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(ctl);
|
||||
|
||||
auto ea = processor.get_enrolment_authority(issuer);
|
||||
ASSERT_TRUE(ea);
|
||||
EXPECT_EQ("https://ea.example/aa", ea->aa_access_point);
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, full_ctl_clears_prior_state)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
PublicKey key_a = m_security->create_key(KeyType::NistP256);
|
||||
PublicKey key_b = m_security->create_key(KeyType::NistP256);
|
||||
|
||||
auto ctl_first = TestRcaCtlBuilder(issuer, true, 1)
|
||||
.add_aa(key_a, "https://aa.example/v1")
|
||||
.add_ea(key_a, "https://ea.example/aa")
|
||||
.build();
|
||||
auto ctl_second =
|
||||
TestRcaCtlBuilder(issuer, true, 2).add_aa(key_b, "https://aa.example/v2").build(); // No EA in second full CTL
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(ctl_first);
|
||||
ASSERT_TRUE(processor.get_authorization_authority(issuer));
|
||||
ASSERT_TRUE(processor.get_enrolment_authority(issuer));
|
||||
|
||||
processor.process(ctl_second);
|
||||
auto aa = processor.get_authorization_authority(issuer);
|
||||
ASSERT_TRUE(aa);
|
||||
EXPECT_EQ("https://aa.example/v2", aa->access_point);
|
||||
// EA was in the first full CTL but absent from the second: must be gone.
|
||||
EXPECT_FALSE(processor.get_enrolment_authority(issuer));
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, delta_ctl_applies_on_top_of_prior_state)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
PublicKey ea_key = m_security->create_key(KeyType::NistP256);
|
||||
PublicKey aa_key = m_security->create_key(KeyType::NistP256);
|
||||
|
||||
auto full = TestRcaCtlBuilder(issuer, true, 1).add_ea(ea_key, "https://ea.example/aa").build();
|
||||
auto delta = TestRcaCtlBuilder(issuer, false, 2).add_aa(aa_key, "https://aa.example/v1").build();
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(full);
|
||||
processor.process(delta);
|
||||
|
||||
// EA from the full CTL must survive — delta does NOT clear.
|
||||
EXPECT_TRUE(processor.get_enrolment_authority(issuer));
|
||||
// AA was added by the delta.
|
||||
EXPECT_TRUE(processor.get_authorization_authority(issuer));
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, delta_ctl_can_delete_certificate)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
PublicKey aa_key = m_security->create_key(KeyType::NistP256);
|
||||
|
||||
auto full = TestRcaCtlBuilder(issuer, true, 1).add_aa(aa_key, "https://aa.example/v1").build();
|
||||
auto delta = TestRcaCtlBuilder(issuer, false, 2)
|
||||
.delete_cert(issuer) // remove the entry keyed by the issuer (the AA bucket)
|
||||
.build();
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(full);
|
||||
ASSERT_TRUE(processor.get_authorization_authority(issuer));
|
||||
|
||||
processor.process(delta);
|
||||
EXPECT_FALSE(processor.get_authorization_authority(issuer));
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, re_announced_aa_overwrites_previous)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
PublicKey key_a = m_security->create_key(KeyType::NistP256);
|
||||
PublicKey key_b = m_security->create_key(KeyType::NistP256);
|
||||
|
||||
auto ctl = TestRcaCtlBuilder(issuer, true, 1)
|
||||
.add_aa(key_a, "https://aa.example/v1")
|
||||
.add_aa(key_b, "https://aa.example/v2")
|
||||
.build();
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(ctl);
|
||||
|
||||
auto aa = processor.get_authorization_authority(issuer);
|
||||
ASSERT_TRUE(aa);
|
||||
// Last add wins (insert_or_assign).
|
||||
EXPECT_EQ("https://aa.example/v2", aa->access_point);
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, is_full_ctl_works_for_tlm_ctl)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xCD);
|
||||
auto ctl = TestTlmCtlBuilder(issuer, true, 7).build();
|
||||
auto is_full = ctl.is_full_ctl();
|
||||
ASSERT_TRUE(is_full);
|
||||
EXPECT_TRUE(*is_full);
|
||||
auto seq = ctl.ctl_sequence();
|
||||
ASSERT_TRUE(seq);
|
||||
EXPECT_EQ(7u, *seq);
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, processor_records_root_ca_from_tlm_ctl)
|
||||
{
|
||||
HashedId8 tlm_issuer = make_id(0xCD);
|
||||
PublicKey rca_key = m_security->create_key(KeyType::NistP256);
|
||||
auto ectl = TestTlmCtlBuilder(tlm_issuer, true, 1).add_root_ca(rca_key).build();
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(ectl);
|
||||
|
||||
// The RCA's HashedId8 is computed from the cert itself (not the issuer).
|
||||
// Locate it via the same path the processor uses internally.
|
||||
Certificate stub = build_stub_certificate(rca_key);
|
||||
HashedId8 rca_hid = stub.calculate_hashed_id8(*m_security);
|
||||
|
||||
EXPECT_TRUE(processor.get_root_ca(rca_hid));
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, processor_records_tlm_from_tlm_ctl)
|
||||
{
|
||||
HashedId8 tlm_issuer = make_id(0xCD);
|
||||
PublicKey tlm_key = m_security->create_key(KeyType::NistP256);
|
||||
auto ectl = TestTlmCtlBuilder(tlm_issuer, true, 1).add_tlm(tlm_key, "https://cpoc.example/").build();
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(ectl);
|
||||
|
||||
Certificate stub = build_stub_certificate(tlm_key);
|
||||
HashedId8 tlm_hid = stub.calculate_hashed_id8(*m_security);
|
||||
|
||||
EXPECT_TRUE(processor.get_trust_list_manager(tlm_hid));
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, full_tlm_ctl_clears_prior_root_cas)
|
||||
{
|
||||
HashedId8 tlm_issuer = make_id(0xCD);
|
||||
PublicKey rca_a_key = m_security->create_key(KeyType::NistP256);
|
||||
PublicKey rca_b_key = m_security->create_key(KeyType::NistP256);
|
||||
|
||||
Certificate rca_a_stub = build_stub_certificate(rca_a_key);
|
||||
HashedId8 rca_a_hid = rca_a_stub.calculate_hashed_id8(*m_security);
|
||||
|
||||
auto first = TestTlmCtlBuilder(tlm_issuer, true, 1).add_root_ca(rca_a_key).build();
|
||||
auto second = TestTlmCtlBuilder(tlm_issuer, true, 2).add_root_ca(rca_b_key).build();
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(first);
|
||||
ASSERT_TRUE(processor.get_root_ca(rca_a_hid));
|
||||
|
||||
processor.process(second);
|
||||
// RCA A was in the first full ECTL but absent from the second: must be gone.
|
||||
EXPECT_FALSE(processor.get_root_ca(rca_a_hid));
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, delta_tlm_ctl_can_delete_root_ca)
|
||||
{
|
||||
HashedId8 tlm_issuer = make_id(0xCD);
|
||||
PublicKey rca_key = m_security->create_key(KeyType::NistP256);
|
||||
Certificate rca_stub = build_stub_certificate(rca_key);
|
||||
HashedId8 rca_hid = rca_stub.calculate_hashed_id8(*m_security);
|
||||
|
||||
auto full = TestTlmCtlBuilder(tlm_issuer, true, 1).add_root_ca(rca_key).build();
|
||||
auto delta = TestTlmCtlBuilder(tlm_issuer, false, 2).delete_cert(rca_hid).build();
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(full);
|
||||
ASSERT_TRUE(processor.get_root_ca(rca_hid));
|
||||
|
||||
processor.process(delta);
|
||||
EXPECT_FALSE(processor.get_root_ca(rca_hid));
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
#include "credential_filesystem_storage.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <fstream>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
using namespace vanetza::pki;
|
||||
|
||||
static const std::string privKeyPem =
|
||||
"-----BEGIN EC PRIVATE KEY-----\n"
|
||||
"MHcCAQEEIHlpKyVDNCQ2x5IFeBelQyyvANI8iTFIoCTfNARm1LVxoAoGCCqGSM49\n"
|
||||
"AwEHoUQDQgAEvfkk5CcwIqP29fGu9n6kXmojUpV2xUlnM2BwJMoYmiVU7lF9p7tu\n"
|
||||
"+KUbGlhPaYLN0UZ13KQTmi3gV3aLolyXUw==\n"
|
||||
"-----END EC PRIVATE KEY-----";
|
||||
|
||||
static const vanetza::ByteBuffer privKeyRaw = {
|
||||
0x18, 0xab, 0x8e, 0xcd, 0xf6, 0x5f, 0xb8, 0x06,
|
||||
0x3b, 0x24, 0xe5, 0xd6, 0x77, 0xf9, 0x47, 0x6a,
|
||||
0xd6, 0xef, 0x7b, 0x54, 0x0b, 0x8f, 0x2a, 0x15,
|
||||
0xe0, 0x09, 0x7c, 0x23, 0xe9, 0xf2, 0x73, 0x11
|
||||
};
|
||||
|
||||
static const vanetza::ByteBuffer pubKeyRawX = {
|
||||
0x54, 0x80, 0xd4, 0x56, 0x9e, 0x50, 0x4d, 0x8e,
|
||||
0x1a, 0x14, 0xcc, 0x0e, 0xd7, 0xea, 0xd0, 0xcf,
|
||||
0x13, 0x49, 0xb8, 0x84, 0xa9, 0xb7, 0x9b, 0xe5,
|
||||
0x2f, 0x4a, 0x4c, 0xe1, 0x62, 0xa3, 0x75, 0xe4
|
||||
};
|
||||
|
||||
static const vanetza::ByteBuffer pubKeyRawY = {
|
||||
0x95, 0x01, 0x22, 0x47, 0xb5, 0x7a, 0x32, 0xc0,
|
||||
0xd9, 0x3c, 0x61, 0x00, 0xd7, 0xe6, 0x7e, 0xe9,
|
||||
0x24, 0xe1, 0x40, 0x84, 0x77, 0x12, 0x7e, 0xa6,
|
||||
0x9f, 0x23, 0x7b, 0xda, 0x40, 0xd9, 0x09, 0x32
|
||||
};
|
||||
|
||||
class CredentialFilesystemStorageTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
root = std::filesystem::temp_directory_path() / "pki_unit_test" / "credentials";
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
|
||||
std::filesystem::path root;
|
||||
};
|
||||
|
||||
TEST_F(CredentialFilesystemStorageTest, store_and_fetch)
|
||||
{
|
||||
CredentialFilesystemStorage storage(root);
|
||||
PublicKey pub;
|
||||
pub.type = KeyType::BrainpoolP256r1;
|
||||
pub.compression = KeyCompression::NoCompression;
|
||||
pub.x = pubKeyRawX;
|
||||
pub.y = pubKeyRawY;
|
||||
|
||||
PrivateKey priv;
|
||||
priv.key = privKeyRaw;
|
||||
priv.type = KeyType::BrainpoolP256r1;
|
||||
|
||||
const std::string filename = "025480D4569E504D8E1A14CC0ED7EAD0CF1349B884A9B79BE52F4A4CE162A375E4.pem";
|
||||
std::filesystem::remove(root / filename);
|
||||
EXPECT_FALSE(std::filesystem::is_regular_file(root / filename));
|
||||
EXPECT_FALSE(storage.fetch(pub));
|
||||
storage.store(pub, priv);
|
||||
boost::optional<PrivateKey> fetched = storage.fetch(pub);
|
||||
ASSERT_TRUE(fetched);
|
||||
EXPECT_EQ(priv.type, fetched->type);
|
||||
EXPECT_EQ(priv.key, fetched->key);
|
||||
EXPECT_TRUE(std::filesystem::is_regular_file(root / filename));
|
||||
}
|
||||
|
||||
TEST_F(CredentialFilesystemStorageTest, list_yields_canonical_hex_stems_only)
|
||||
{
|
||||
std::filesystem::create_directories(root);
|
||||
// Three valid-looking .pem files (P-256 stems = "02"/"03" + 64 hex chars).
|
||||
std::ofstream(root / "025480D4569E504D8E1A14CC0ED7EAD0CF1349B884A9B79BE52F4A4CE162A375E4.pem").put('x');
|
||||
std::ofstream(root / "030000000000000000000000000000000000000000000000000000000000000000.pem").put('x');
|
||||
std::ofstream(root / "021111111111111111111111111111111111111111111111111111111111111111.pem").put('x');
|
||||
// Decoys that must not appear in list():
|
||||
std::ofstream(root / "ignore.txt").put('x'); // wrong extension
|
||||
std::ofstream(root / "garbage.pem").put('x'); // wrong stem format
|
||||
std::ofstream(root / "025480D4569E504D8E1A14CC0ED7EAD0CF1349B884A9B79BE52F4A4CE162A375.pem")
|
||||
.put('x'); // 64 chars (one short)
|
||||
std::filesystem::create_directory(root / "subdir");
|
||||
|
||||
CredentialFilesystemStorage storage(root);
|
||||
std::set<std::string> seen;
|
||||
for (const auto& name : storage.list()) {
|
||||
seen.insert(name);
|
||||
}
|
||||
|
||||
EXPECT_EQ(3u, seen.size());
|
||||
EXPECT_TRUE(seen.count("025480D4569E504D8E1A14CC0ED7EAD0CF1349B884A9B79BE52F4A4CE162A375E4"));
|
||||
EXPECT_TRUE(seen.count("030000000000000000000000000000000000000000000000000000000000000000"));
|
||||
EXPECT_TRUE(seen.count("021111111111111111111111111111111111111111111111111111111111111111"));
|
||||
}
|
||||
|
||||
TEST_F(CredentialFilesystemStorageTest, list_on_empty_directory)
|
||||
{
|
||||
std::filesystem::create_directories(root);
|
||||
CredentialFilesystemStorage storage(root);
|
||||
std::vector<std::string> names;
|
||||
for (const auto& n : storage.list()) {
|
||||
names.push_back(n);
|
||||
}
|
||||
EXPECT_TRUE(names.empty());
|
||||
}
|
||||
|
||||
TEST_F(CredentialFilesystemStorageTest, discard_by_name_removes_file)
|
||||
{
|
||||
std::filesystem::create_directories(root);
|
||||
const std::string stem = "030000000000000000000000000000000000000000000000000000000000000000";
|
||||
std::ofstream(root / (stem + ".pem")).put('x');
|
||||
ASSERT_TRUE(std::filesystem::is_regular_file(root / (stem + ".pem")));
|
||||
|
||||
CredentialFilesystemStorage storage(root);
|
||||
EXPECT_TRUE(storage.discard(stem));
|
||||
EXPECT_FALSE(std::filesystem::is_regular_file(root / (stem + ".pem")));
|
||||
}
|
||||
|
||||
TEST_F(CredentialFilesystemStorageTest, discard_by_name_returns_false_for_unknown)
|
||||
{
|
||||
std::filesystem::create_directories(root);
|
||||
CredentialFilesystemStorage storage(root);
|
||||
EXPECT_FALSE(storage.discard("020000000000000000000000000000000000000000000000000000000000000000"));
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
#include "crl_store.hpp"
|
||||
#include "certificate_revocation_list.hpp"
|
||||
#include "crl_test_fixture.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <fstream>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class CrlFilesystemStoreTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
CrlFilesystemStoreTest() :
|
||||
m_credentials(std::make_shared<MockCredentialStorage>()),
|
||||
m_security(std::make_shared<OpenSslSecurityModule>(m_credentials))
|
||||
{
|
||||
}
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
m_root = std::filesystem::temp_directory_path() / "pki_unit_test" / "crl_store";
|
||||
std::filesystem::remove_all(m_root);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
std::filesystem::remove_all(m_root);
|
||||
}
|
||||
|
||||
std::shared_ptr<MockCredentialStorage> m_credentials;
|
||||
std::shared_ptr<OpenSslSecurityModule> m_security;
|
||||
std::filesystem::path m_root;
|
||||
};
|
||||
|
||||
TEST_F(CrlFilesystemStoreTest, store_and_lookup_roundtrip)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
HashedId8 revoked = make_id(0x42);
|
||||
CertificateRevocationList crl = build_test_crl(issuer, { revoked });
|
||||
|
||||
CrlFilesystemStore store(m_security, m_root);
|
||||
ASSERT_TRUE(store.store(crl));
|
||||
EXPECT_TRUE(store.is_revoked(issuer.octets, revoked.octets));
|
||||
}
|
||||
|
||||
TEST_F(CrlFilesystemStoreTest, is_revoked_returns_false_for_unknown_issuer)
|
||||
{
|
||||
CrlFilesystemStore store(m_security, m_root);
|
||||
HashedId8 known_issuer = make_id(0xAB);
|
||||
HashedId8 revoked = make_id(0x42);
|
||||
ASSERT_TRUE(store.store(build_test_crl(known_issuer, { revoked })));
|
||||
|
||||
HashedId8 other_issuer = make_id(0xCD);
|
||||
EXPECT_FALSE(store.is_revoked(other_issuer.octets, revoked.octets));
|
||||
}
|
||||
|
||||
TEST_F(CrlFilesystemStoreTest, is_revoked_returns_false_for_unlisted_cert)
|
||||
{
|
||||
CrlFilesystemStore store(m_security, m_root);
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
ASSERT_TRUE(store.store(build_test_crl(issuer, { make_id(0x42) })));
|
||||
|
||||
HashedId8 other_cert = make_id(0x99);
|
||||
EXPECT_FALSE(store.is_revoked(issuer.octets, other_cert.octets));
|
||||
}
|
||||
|
||||
TEST_F(CrlFilesystemStoreTest, empty_crl_clears_prior_entries)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
HashedId8 revoked = make_id(0x42);
|
||||
|
||||
CrlFilesystemStore store(m_security, m_root);
|
||||
ASSERT_TRUE(store.store(build_test_crl(issuer, { revoked })));
|
||||
ASSERT_TRUE(store.is_revoked(issuer.octets, revoked.octets));
|
||||
|
||||
ASSERT_TRUE(store.store(build_test_crl(issuer, {})));
|
||||
EXPECT_FALSE(store.is_revoked(issuer.octets, revoked.octets));
|
||||
}
|
||||
|
||||
TEST_F(CrlFilesystemStoreTest, constructor_loads_existing_crl_files)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
HashedId8 revoked = make_id(0x42);
|
||||
|
||||
{
|
||||
CrlFilesystemStore writer(m_security, m_root);
|
||||
ASSERT_TRUE(writer.store(build_test_crl(issuer, { revoked })));
|
||||
}
|
||||
|
||||
CrlFilesystemStore reader(m_security, m_root);
|
||||
EXPECT_TRUE(reader.is_revoked(issuer.octets, revoked.octets));
|
||||
}
|
||||
|
||||
TEST_F(CrlFilesystemStoreTest, constructor_ignores_non_crl_files_and_garbage)
|
||||
{
|
||||
std::filesystem::create_directories(m_root);
|
||||
std::ofstream(m_root / "not_a_crl.txt") << "ignored";
|
||||
std::ofstream(m_root / "garbage.crl") << "\x00\x01\x02";
|
||||
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
HashedId8 revoked = make_id(0x42);
|
||||
{
|
||||
CrlFilesystemStore writer(m_security, m_root);
|
||||
ASSERT_TRUE(writer.store(build_test_crl(issuer, { revoked })));
|
||||
}
|
||||
|
||||
CrlFilesystemStore reader(m_security, m_root);
|
||||
EXPECT_TRUE(reader.is_revoked(issuer.octets, revoked.octets));
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
|
||||
#include "asn1.hpp"
|
||||
#include "certificate_revocation_list.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include <vanetza/asn1/asn1c_wrapper.hpp>
|
||||
#include <vanetza/asn1/security/EtsiTs102941Data.h>
|
||||
#include <vanetza/asn1/security/EtsiTs103097Data.h>
|
||||
#include <vanetza/asn1/security/Ieee1609Dot2Content.h>
|
||||
#include <vanetza/asn1/security/SignedData.h>
|
||||
#include <vanetza/asn1/security/ToBeSignedCrl.h>
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
#include <array>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
// Build a CertificateRevocationList carrying \p revoked entries, signed (in
|
||||
// the SignerIdentifier sense) by the digest \p issuer. The ECDSA signature
|
||||
// itself is dummy zeros — sufficient for tests that don't verify it.
|
||||
inline CertificateRevocationList build_test_crl(const HashedId8& issuer, const std::vector<HashedId8>& revoked)
|
||||
{
|
||||
// The signed payload is an EtsiTs102941Data whose content CHOICE carries the
|
||||
// ToBeSignedCrl — matching CertificateRevocationList::revoked_entries().
|
||||
ByteBuffer tbs_bytes;
|
||||
{
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
|
||||
mgmt->version = 1;
|
||||
mgmt->content.present = Vanetza_Security_EtsiTs102941DataContent_PR_certificateRevocationList;
|
||||
Vanetza_Security_ToBeSignedCrl_t& tbs = mgmt->content.choice.certificateRevocationList;
|
||||
tbs.version = 1;
|
||||
tbs.thisUpdate = 0;
|
||||
tbs.nextUpdate = 0;
|
||||
for (const auto& id : revoked) {
|
||||
auto* entry = asn1::allocate<Vanetza_Security_CrlEntry_t>();
|
||||
OCTET_STRING_fromBuf(entry, reinterpret_cast<const char*>(id.octets.data()), id.octets.size());
|
||||
asn_sequence_add(&tbs.entries, entry);
|
||||
}
|
||||
tbs_bytes = mgmt.encode();
|
||||
}
|
||||
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_CertificateRevocationListMessage_t>
|
||||
outer(asn_DEF_Vanetza_Security_CertificateRevocationListMessage);
|
||||
outer->protocolVersion = 3;
|
||||
outer->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
outer->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_signedData;
|
||||
|
||||
auto* signed_data = asn1::allocate<Vanetza_Security_SignedData_t>();
|
||||
outer->content->choice.signedData = signed_data;
|
||||
signed_data->hashId = Vanetza_Security_HashAlgorithm_sha256;
|
||||
|
||||
signed_data->signer.present = Vanetza_Security_SignerIdentifier_PR_digest;
|
||||
OCTET_STRING_fromBuf(&signed_data->signer.choice.digest, reinterpret_cast<const char*>(issuer.octets.data()),
|
||||
issuer.octets.size());
|
||||
|
||||
signed_data->tbsData = asn1::allocate<Vanetza_Security_ToBeSignedData_t>();
|
||||
signed_data->tbsData->headerInfo.psid = aid::CRL;
|
||||
signed_data->tbsData->payload = asn1::allocate<Vanetza_Security_SignedDataPayload_t>();
|
||||
auto* inner = asn1::allocate<Vanetza_Security_EtsiTs103097Data_t>();
|
||||
signed_data->tbsData->payload->data = inner;
|
||||
inner->protocolVersion = 3;
|
||||
inner->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
inner->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_unsecuredData;
|
||||
OCTET_STRING_fromBuf(&inner->content->choice.unsecuredData, reinterpret_cast<const char*>(tbs_bytes.data()),
|
||||
tbs_bytes.size());
|
||||
|
||||
signed_data->signature.present = Vanetza_Security_Signature_PR_ecdsaNistP256Signature;
|
||||
signed_data->signature.choice.ecdsaNistP256Signature.rSig.present = Vanetza_Security_EccP256CurvePoint_PR_x_only;
|
||||
std::array<std::uint8_t, 32> zeros {};
|
||||
OCTET_STRING_fromBuf(&signed_data->signature.choice.ecdsaNistP256Signature.rSig.choice.x_only,
|
||||
reinterpret_cast<const char*>(zeros.data()), zeros.size());
|
||||
OCTET_STRING_fromBuf(&signed_data->signature.choice.ecdsaNistP256Signature.sSig,
|
||||
reinterpret_cast<const char*>(zeros.data()), zeros.size());
|
||||
|
||||
ByteBuffer outer_bytes = outer.encode();
|
||||
CertificateRevocationList crl;
|
||||
if (!crl.decode(outer_bytes)) {
|
||||
throw std::runtime_error("test fixture: outer CRL message could not be decoded");
|
||||
}
|
||||
return crl;
|
||||
}
|
||||
|
||||
inline HashedId8 make_id(std::uint8_t fill)
|
||||
{
|
||||
HashedId8 id;
|
||||
id.octets.fill(fill);
|
||||
return id;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,313 @@
|
||||
#pragma once
|
||||
|
||||
#include "asn1.hpp"
|
||||
#include "certificate_trust_list.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "keys.hpp"
|
||||
#include <vanetza/asn1/asn1c_wrapper.hpp>
|
||||
#include <vanetza/asn1/security/CtlCommand.h>
|
||||
#include <vanetza/asn1/security/EtsiTs102941Data.h>
|
||||
#include <vanetza/asn1/security/EtsiTs103097Data.h>
|
||||
#include <vanetza/asn1/security/Ieee1609Dot2Content.h>
|
||||
#include <vanetza/asn1/security/RcaCertificateTrustListMessage.h>
|
||||
#include <vanetza/asn1/security/SignedData.h>
|
||||
#include <vanetza/asn1/security/TlmCertificateTrustListMessage.h>
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
#include <vanetza/security/v3/basic_elements.hpp>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
// Builds an RCA-signed CTL message. Internal: fills an EtsiTs102941Data with a
|
||||
// CtlFormat, OER-encodes it, wraps in a signed message with the issuer's
|
||||
// HashedId8 as signer (dummy signature). decode() the result into a
|
||||
// CertificateTrustList for use in tests.
|
||||
class TestRcaCtlBuilder
|
||||
{
|
||||
public:
|
||||
TestRcaCtlBuilder(const HashedId8& issuer, bool is_full, std::uint8_t sequence) : m_issuer(issuer)
|
||||
{
|
||||
m_data->version = 1;
|
||||
m_data->content.present = Vanetza_Security_EtsiTs102941DataContent_PR_certificateTrustListRca;
|
||||
auto& format = m_data->content.choice.certificateTrustListRca;
|
||||
format.version = 1;
|
||||
format.nextUpdate = 0;
|
||||
format.isFullCtl = is_full ? 1 : 0;
|
||||
format.ctlSequence = sequence;
|
||||
}
|
||||
|
||||
TestRcaCtlBuilder& add_aa(const PublicKey& key, const std::string& url)
|
||||
{
|
||||
auto* cmd = asn1::allocate<Vanetza_Security_CtlCommand_t>();
|
||||
cmd->present = Vanetza_Security_CtlCommand_PR_add;
|
||||
cmd->choice.add.present = Vanetza_Security_CtlEntry_PR_aa;
|
||||
auto& aa = cmd->choice.add.choice.aa;
|
||||
populate_stub_cert(aa.aaCertificate, key, "test-aa");
|
||||
OCTET_STRING_fromBuf(&aa.accessPoint, url.data(), url.size());
|
||||
asn_sequence_add(&m_data->content.choice.certificateTrustListRca.ctlCommands, cmd);
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestRcaCtlBuilder& add_ea(const PublicKey& key, const std::string& aa_access_point)
|
||||
{
|
||||
auto* cmd = asn1::allocate<Vanetza_Security_CtlCommand_t>();
|
||||
cmd->present = Vanetza_Security_CtlCommand_PR_add;
|
||||
cmd->choice.add.present = Vanetza_Security_CtlEntry_PR_ea;
|
||||
auto& ea = cmd->choice.add.choice.ea;
|
||||
populate_stub_cert(ea.eaCertificate, key, "test-ea");
|
||||
OCTET_STRING_fromBuf(&ea.aaAccessPoint, aa_access_point.data(), aa_access_point.size());
|
||||
// itsAccessPoint left null (OPTIONAL)
|
||||
asn_sequence_add(&m_data->content.choice.certificateTrustListRca.ctlCommands, cmd);
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestRcaCtlBuilder& add_dc(const std::string& url, const std::vector<HashedId8>& certs)
|
||||
{
|
||||
auto* cmd = asn1::allocate<Vanetza_Security_CtlCommand_t>();
|
||||
cmd->present = Vanetza_Security_CtlCommand_PR_add;
|
||||
cmd->choice.add.present = Vanetza_Security_CtlEntry_PR_dc;
|
||||
auto& dc = cmd->choice.add.choice.dc;
|
||||
OCTET_STRING_fromBuf(&dc.url, url.data(), url.size());
|
||||
for (const auto& id : certs) {
|
||||
auto* entry = asn1::allocate<Vanetza_Security_HashedId8_t>();
|
||||
OCTET_STRING_fromBuf(entry, reinterpret_cast<const char*>(id.octets.data()), id.octets.size());
|
||||
asn_sequence_add(&dc.cert, entry);
|
||||
}
|
||||
asn_sequence_add(&m_data->content.choice.certificateTrustListRca.ctlCommands, cmd);
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestRcaCtlBuilder& delete_cert(const HashedId8& id)
|
||||
{
|
||||
auto* cmd = asn1::allocate<Vanetza_Security_CtlCommand_t>();
|
||||
cmd->present = Vanetza_Security_CtlCommand_PR_delete;
|
||||
cmd->choice.Delete.present = Vanetza_Security_CtlDelete_PR_cert;
|
||||
OCTET_STRING_fromBuf(&cmd->choice.Delete.choice.cert, reinterpret_cast<const char*>(id.octets.data()),
|
||||
id.octets.size());
|
||||
asn_sequence_add(&m_data->content.choice.certificateTrustListRca.ctlCommands, cmd);
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestRcaCtlBuilder& delete_dc(const std::string& url)
|
||||
{
|
||||
auto* cmd = asn1::allocate<Vanetza_Security_CtlCommand_t>();
|
||||
cmd->present = Vanetza_Security_CtlCommand_PR_delete;
|
||||
cmd->choice.Delete.present = Vanetza_Security_CtlDelete_PR_dc;
|
||||
OCTET_STRING_fromBuf(&cmd->choice.Delete.choice.dc, url.data(), url.size());
|
||||
asn_sequence_add(&m_data->content.choice.certificateTrustListRca.ctlCommands, cmd);
|
||||
return *this;
|
||||
}
|
||||
|
||||
CertificateTrustList build()
|
||||
{
|
||||
ByteBuffer inner_bytes = m_data.encode();
|
||||
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_RcaCertificateTrustListMessage_t>
|
||||
outer(asn_DEF_Vanetza_Security_RcaCertificateTrustListMessage);
|
||||
outer->protocolVersion = 3;
|
||||
outer->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
outer->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_signedData;
|
||||
|
||||
auto* signed_data = asn1::allocate<Vanetza_Security_SignedData_t>();
|
||||
outer->content->choice.signedData = signed_data;
|
||||
signed_data->hashId = Vanetza_Security_HashAlgorithm_sha256;
|
||||
|
||||
signed_data->signer.present = Vanetza_Security_SignerIdentifier_PR_digest;
|
||||
OCTET_STRING_fromBuf(&signed_data->signer.choice.digest, reinterpret_cast<const char*>(m_issuer.octets.data()),
|
||||
m_issuer.octets.size());
|
||||
|
||||
signed_data->tbsData = asn1::allocate<Vanetza_Security_ToBeSignedData_t>();
|
||||
signed_data->tbsData->headerInfo.psid = aid::CTL;
|
||||
signed_data->tbsData->payload = asn1::allocate<Vanetza_Security_SignedDataPayload_t>();
|
||||
auto* inner_data = asn1::allocate<Vanetza_Security_EtsiTs103097Data_t>();
|
||||
signed_data->tbsData->payload->data = inner_data;
|
||||
inner_data->protocolVersion = 3;
|
||||
inner_data->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
inner_data->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_unsecuredData;
|
||||
OCTET_STRING_fromBuf(&inner_data->content->choice.unsecuredData,
|
||||
reinterpret_cast<const char*>(inner_bytes.data()), inner_bytes.size());
|
||||
|
||||
signed_data->signature.present = Vanetza_Security_Signature_PR_ecdsaNistP256Signature;
|
||||
signed_data->signature.choice.ecdsaNistP256Signature.rSig.present =
|
||||
Vanetza_Security_EccP256CurvePoint_PR_x_only;
|
||||
std::array<std::uint8_t, 32> zeros {};
|
||||
OCTET_STRING_fromBuf(&signed_data->signature.choice.ecdsaNistP256Signature.rSig.choice.x_only,
|
||||
reinterpret_cast<const char*>(zeros.data()), zeros.size());
|
||||
OCTET_STRING_fromBuf(&signed_data->signature.choice.ecdsaNistP256Signature.sSig,
|
||||
reinterpret_cast<const char*>(zeros.data()), zeros.size());
|
||||
|
||||
ByteBuffer outer_bytes = outer.encode();
|
||||
CertificateTrustList ctl;
|
||||
if (!ctl.decode(outer_bytes)) {
|
||||
throw std::runtime_error("CTL test fixture: outer message could not be decoded");
|
||||
}
|
||||
return ctl;
|
||||
}
|
||||
|
||||
private:
|
||||
static void populate_stub_cert(Vanetza_Security_EtsiTs103097Certificate_t& cert, const PublicKey& key,
|
||||
const char* name)
|
||||
{
|
||||
cert.version = 3;
|
||||
cert.type = Vanetza_Security_CertificateType_explicit;
|
||||
cert.issuer.present = Vanetza_Security_IssuerIdentifier_PR_self;
|
||||
cert.issuer.choice.self = Vanetza_Security_HashAlgorithm_sha256;
|
||||
|
||||
auto& tbs = cert.toBeSigned;
|
||||
tbs.id.present = Vanetza_Security_CertificateId_PR_name;
|
||||
OCTET_STRING_fromBuf(&tbs.id.choice.name, name, std::strlen(name));
|
||||
tbs.cracaId.buf = static_cast<std::uint8_t*>(std::calloc(3, 1));
|
||||
tbs.cracaId.size = 3;
|
||||
tbs.crlSeries = 0;
|
||||
tbs.validityPeriod.start = security::v3::convert_time32(Clock::time_point {});
|
||||
tbs.validityPeriod.duration.present = Vanetza_Security_Duration_PR_hours;
|
||||
tbs.validityPeriod.duration.choice.hours = 24;
|
||||
tbs.verifyKeyIndicator.present = Vanetza_Security_VerificationKeyIndicator_PR_verificationKey;
|
||||
set_verification_key(tbs.verifyKeyIndicator.choice.verificationKey, key);
|
||||
}
|
||||
|
||||
HashedId8 m_issuer;
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> m_data { asn_DEF_Vanetza_Security_EtsiTs102941Data };
|
||||
};
|
||||
|
||||
inline HashedId8 make_id(std::uint8_t fill)
|
||||
{
|
||||
HashedId8 id;
|
||||
id.octets.fill(fill);
|
||||
return id;
|
||||
}
|
||||
|
||||
// Builds a TLM-signed CTL message (ECTL). Mirrors TestRcaCtlBuilder but for
|
||||
// the certificateTrustListTlm variant; entries are RootCaEntry / TlmEntry / DcEntry.
|
||||
class TestTlmCtlBuilder
|
||||
{
|
||||
public:
|
||||
TestTlmCtlBuilder(const HashedId8& issuer, bool is_full, std::uint8_t sequence) : m_issuer(issuer)
|
||||
{
|
||||
m_data->version = 1;
|
||||
m_data->content.present = Vanetza_Security_EtsiTs102941DataContent_PR_certificateTrustListTlm;
|
||||
auto& format = m_data->content.choice.certificateTrustListTlm;
|
||||
format.version = 1;
|
||||
format.nextUpdate = 0;
|
||||
format.isFullCtl = is_full ? 1 : 0;
|
||||
format.ctlSequence = sequence;
|
||||
}
|
||||
|
||||
TestTlmCtlBuilder& add_root_ca(const PublicKey& key)
|
||||
{
|
||||
auto* cmd = asn1::allocate<Vanetza_Security_CtlCommand_t>();
|
||||
cmd->present = Vanetza_Security_CtlCommand_PR_add;
|
||||
cmd->choice.add.present = Vanetza_Security_CtlEntry_PR_rca;
|
||||
auto& rca = cmd->choice.add.choice.rca;
|
||||
// Name matches build_stub_certificate so tests can derive the same HashedId8.
|
||||
populate_stub_cert(rca.selfsignedRootCa, key, "test-cert");
|
||||
asn_sequence_add(&m_data->content.choice.certificateTrustListTlm.ctlCommands, cmd);
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestTlmCtlBuilder& add_tlm(const PublicKey& key, const std::string& cpoc_url)
|
||||
{
|
||||
auto* cmd = asn1::allocate<Vanetza_Security_CtlCommand_t>();
|
||||
cmd->present = Vanetza_Security_CtlCommand_PR_add;
|
||||
cmd->choice.add.present = Vanetza_Security_CtlEntry_PR_tlm;
|
||||
auto& tlm = cmd->choice.add.choice.tlm;
|
||||
populate_stub_cert(tlm.selfSignedTLMCertificate, key, "test-cert");
|
||||
OCTET_STRING_fromBuf(&tlm.accessPoint, cpoc_url.data(), cpoc_url.size());
|
||||
asn_sequence_add(&m_data->content.choice.certificateTrustListTlm.ctlCommands, cmd);
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestTlmCtlBuilder& delete_cert(const HashedId8& id)
|
||||
{
|
||||
auto* cmd = asn1::allocate<Vanetza_Security_CtlCommand_t>();
|
||||
cmd->present = Vanetza_Security_CtlCommand_PR_delete;
|
||||
cmd->choice.Delete.present = Vanetza_Security_CtlDelete_PR_cert;
|
||||
OCTET_STRING_fromBuf(&cmd->choice.Delete.choice.cert, reinterpret_cast<const char*>(id.octets.data()),
|
||||
id.octets.size());
|
||||
asn_sequence_add(&m_data->content.choice.certificateTrustListTlm.ctlCommands, cmd);
|
||||
return *this;
|
||||
}
|
||||
|
||||
CertificateTrustList build()
|
||||
{
|
||||
ByteBuffer inner_bytes = m_data.encode();
|
||||
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_TlmCertificateTrustListMessage_t>
|
||||
outer(asn_DEF_Vanetza_Security_TlmCertificateTrustListMessage);
|
||||
outer->protocolVersion = 3;
|
||||
outer->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
outer->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_signedData;
|
||||
|
||||
auto* signed_data = asn1::allocate<Vanetza_Security_SignedData_t>();
|
||||
outer->content->choice.signedData = signed_data;
|
||||
signed_data->hashId = Vanetza_Security_HashAlgorithm_sha256;
|
||||
|
||||
signed_data->signer.present = Vanetza_Security_SignerIdentifier_PR_digest;
|
||||
OCTET_STRING_fromBuf(&signed_data->signer.choice.digest, reinterpret_cast<const char*>(m_issuer.octets.data()),
|
||||
m_issuer.octets.size());
|
||||
|
||||
signed_data->tbsData = asn1::allocate<Vanetza_Security_ToBeSignedData_t>();
|
||||
signed_data->tbsData->headerInfo.psid = aid::CTL;
|
||||
signed_data->tbsData->payload = asn1::allocate<Vanetza_Security_SignedDataPayload_t>();
|
||||
auto* inner_data = asn1::allocate<Vanetza_Security_EtsiTs103097Data_t>();
|
||||
signed_data->tbsData->payload->data = inner_data;
|
||||
inner_data->protocolVersion = 3;
|
||||
inner_data->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
inner_data->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_unsecuredData;
|
||||
OCTET_STRING_fromBuf(&inner_data->content->choice.unsecuredData,
|
||||
reinterpret_cast<const char*>(inner_bytes.data()), inner_bytes.size());
|
||||
|
||||
signed_data->signature.present = Vanetza_Security_Signature_PR_ecdsaNistP256Signature;
|
||||
signed_data->signature.choice.ecdsaNistP256Signature.rSig.present =
|
||||
Vanetza_Security_EccP256CurvePoint_PR_x_only;
|
||||
std::array<std::uint8_t, 32> zeros {};
|
||||
OCTET_STRING_fromBuf(&signed_data->signature.choice.ecdsaNistP256Signature.rSig.choice.x_only,
|
||||
reinterpret_cast<const char*>(zeros.data()), zeros.size());
|
||||
OCTET_STRING_fromBuf(&signed_data->signature.choice.ecdsaNistP256Signature.sSig,
|
||||
reinterpret_cast<const char*>(zeros.data()), zeros.size());
|
||||
|
||||
ByteBuffer outer_bytes = outer.encode();
|
||||
CertificateTrustList ctl;
|
||||
if (!ctl.decode(outer_bytes)) {
|
||||
throw std::runtime_error("TLM CTL test fixture: outer message could not be decoded");
|
||||
}
|
||||
return ctl;
|
||||
}
|
||||
|
||||
private:
|
||||
static void populate_stub_cert(Vanetza_Security_EtsiTs103097Certificate_t& cert, const PublicKey& key,
|
||||
const char* name)
|
||||
{
|
||||
cert.version = 3;
|
||||
cert.type = Vanetza_Security_CertificateType_explicit;
|
||||
cert.issuer.present = Vanetza_Security_IssuerIdentifier_PR_self;
|
||||
cert.issuer.choice.self = Vanetza_Security_HashAlgorithm_sha256;
|
||||
|
||||
auto& tbs = cert.toBeSigned;
|
||||
tbs.id.present = Vanetza_Security_CertificateId_PR_name;
|
||||
OCTET_STRING_fromBuf(&tbs.id.choice.name, name, std::strlen(name));
|
||||
tbs.cracaId.buf = static_cast<std::uint8_t*>(std::calloc(3, 1));
|
||||
tbs.cracaId.size = 3;
|
||||
tbs.crlSeries = 0;
|
||||
tbs.validityPeriod.start = security::v3::convert_time32(Clock::time_point {});
|
||||
tbs.validityPeriod.duration.present = Vanetza_Security_Duration_PR_hours;
|
||||
tbs.validityPeriod.duration.choice.hours = 24;
|
||||
tbs.verifyKeyIndicator.present = Vanetza_Security_VerificationKeyIndicator_PR_verificationKey;
|
||||
set_verification_key(tbs.verifyKeyIndicator.choice.verificationKey, key);
|
||||
}
|
||||
|
||||
HashedId8 m_issuer;
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> m_data { asn_DEF_Vanetza_Security_EtsiTs102941Data };
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,283 @@
|
||||
#include "ea_request.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include "signed_data.hpp"
|
||||
#include "stub_certificate.hpp"
|
||||
#include "validation.hpp"
|
||||
#include <vanetza/asn1/security/EtsiTs102941Data.h>
|
||||
#include <vanetza/asn1/security/InnerEcRequest.h>
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
// Fixture that provides a SecurityModule with in-memory credential storage and
|
||||
// a fresh bootstrap/verification key pair.
|
||||
class EnrolmentRequestTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
EnrolmentRequestTest() : m_credentials(std::make_shared<MockCredentialStorage>()), m_security(m_credentials)
|
||||
{
|
||||
}
|
||||
|
||||
EnrolmentRequestParameters make_params(KeyType type = KeyType::NistP256)
|
||||
{
|
||||
EnrolmentRequestParameters params;
|
||||
params.its_id = "test-station-42";
|
||||
params.verification_key = m_security.create_key(type);
|
||||
params.outer_signer_key = m_security.create_key(type);
|
||||
return params;
|
||||
}
|
||||
|
||||
// Decode the unsecuredData payload of a signed Ieee1609Dot2Data into the given wrapper type.
|
||||
template<class Wrapper> static Wrapper decode_unsecured(const Vanetza_Security_SignedData_t& signed_data)
|
||||
{
|
||||
const auto& payload = signed_data.tbsData->payload->data->content->choice.unsecuredData;
|
||||
Wrapper w;
|
||||
EXPECT_TRUE(w.decode(payload.buf, payload.size));
|
||||
return w;
|
||||
}
|
||||
|
||||
std::shared_ptr<MockCredentialStorage> m_credentials;
|
||||
OpenSslSecurityModule m_security;
|
||||
};
|
||||
|
||||
// The builder must reject parameters that do not satisfy ETSI TS 102 941 / IEEE 1609.2 basic invariants.
|
||||
TEST_F(EnrolmentRequestTest, rejects_empty_its_id)
|
||||
{
|
||||
auto params = make_params();
|
||||
params.its_id.clear();
|
||||
EXPECT_THROW(build_signed_enrolment_request(m_security, params), std::invalid_argument);
|
||||
}
|
||||
|
||||
// The signed output must parse as an EtsiTs103097Data-Signed with the exact
|
||||
// structure specified in ETSI TS 102 941 clause 6.2.3.2:
|
||||
// Ieee1609Dot2Data(signedData(tbsData.payload.data = unsecured(<EtsiTs102941Data>), signer = self))
|
||||
TEST_F(EnrolmentRequestTest, outer_structure_matches_TS102941)
|
||||
{
|
||||
auto params = make_params();
|
||||
ByteBuffer encoded = build_signed_enrolment_request(m_security, params);
|
||||
|
||||
SignedData outer;
|
||||
ASSERT_TRUE(outer.decode(encoded));
|
||||
EXPECT_EQ(3u, outer->protocolVersion);
|
||||
ASSERT_EQ(Vanetza_Security_Ieee1609Dot2Content_PR_signedData, outer->content->present);
|
||||
const auto* sd = outer->content->choice.signedData;
|
||||
ASSERT_NE(nullptr, sd);
|
||||
EXPECT_EQ(Vanetza_Security_HashAlgorithm_sha256, sd->hashId);
|
||||
EXPECT_EQ(Vanetza_Security_SignerIdentifier_PR_self, sd->signer.present);
|
||||
ASSERT_NE(nullptr, sd->tbsData);
|
||||
EXPECT_EQ(aid::SCR, sd->tbsData->headerInfo.psid);
|
||||
ASSERT_NE(nullptr, sd->tbsData->payload);
|
||||
ASSERT_NE(nullptr, sd->tbsData->payload->data);
|
||||
ASSERT_EQ(Vanetza_Security_Ieee1609Dot2Content_PR_unsecuredData, sd->tbsData->payload->data->content->present);
|
||||
}
|
||||
|
||||
// The outer signature must verify against the canonical (bootstrap) key, as
|
||||
// required by ETSI TS 103 525-2 clause 5.2.2.1 (SECPKI_ITSS_ENR_02_BV).
|
||||
TEST_F(EnrolmentRequestTest, outer_signature_verifies_with_canonical_key)
|
||||
{
|
||||
auto params = make_params();
|
||||
ByteBuffer encoded = build_signed_enrolment_request(m_security, params);
|
||||
|
||||
SignedData outer;
|
||||
ASSERT_TRUE(outer.decode(encoded));
|
||||
const auto& sd = *outer->content->choice.signedData;
|
||||
|
||||
Sha256Hash digest = calculate_digest<Sha256Hash>(m_security, *sd.tbsData, nullptr);
|
||||
Signature signature = make_signature(sd.signature);
|
||||
EXPECT_TRUE(m_security.verify(digest, signature, params.outer_signer_key));
|
||||
}
|
||||
|
||||
// The POP signature carried in the EtsiTs102941Data enrolmentRequest content
|
||||
// must verify against the *new* verification key (IEEE 1609.2 / TS 102 941
|
||||
// proof-of-possession).
|
||||
TEST_F(EnrolmentRequestTest, pop_signature_verifies_with_verification_key)
|
||||
{
|
||||
auto params = make_params();
|
||||
ByteBuffer encoded = build_signed_enrolment_request(m_security, params);
|
||||
|
||||
SignedData outer;
|
||||
ASSERT_TRUE(outer.decode(encoded));
|
||||
MgmtData mgmt = decode_unsecured<MgmtData>(*outer->content->choice.signedData);
|
||||
ASSERT_EQ(Vanetza_Security_EtsiTs102941DataContent_PR_enrolmentRequest, mgmt->content.present);
|
||||
|
||||
const auto& pop = mgmt->content.choice.enrolmentRequest;
|
||||
EXPECT_EQ(3u, pop.protocolVersion);
|
||||
ASSERT_EQ(Vanetza_Security_Ieee1609Dot2Content_PR_signedData, pop.content->present);
|
||||
const auto& pop_sd = *pop.content->choice.signedData;
|
||||
EXPECT_EQ(Vanetza_Security_SignerIdentifier_PR_self, pop_sd.signer.present);
|
||||
|
||||
Sha256Hash digest = calculate_digest<Sha256Hash>(m_security, *pop_sd.tbsData, nullptr);
|
||||
Signature signature = make_signature(pop_sd.signature);
|
||||
EXPECT_TRUE(m_security.verify(digest, signature, params.verification_key));
|
||||
}
|
||||
|
||||
// The innermost InnerEcRequest must carry the inputs unchanged: itsId, the
|
||||
// requested verification key, and the permission list.
|
||||
TEST_F(EnrolmentRequestTest, inner_ec_request_carries_inputs)
|
||||
{
|
||||
auto params = make_params();
|
||||
ByteBuffer encoded = build_signed_enrolment_request(m_security, params);
|
||||
|
||||
SignedData outer;
|
||||
ASSERT_TRUE(outer.decode(encoded));
|
||||
MgmtData mgmt = decode_unsecured<MgmtData>(*outer->content->choice.signedData);
|
||||
|
||||
// Extract inner request from the POP's unsecured payload
|
||||
const auto& pop_sd = *mgmt->content.choice.enrolmentRequest.content->choice.signedData;
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_InnerEcRequest_t> inner(asn_DEF_Vanetza_Security_InnerEcRequest);
|
||||
const auto& pop_payload = pop_sd.tbsData->payload->data->content->choice.unsecuredData;
|
||||
ASSERT_TRUE(inner.decode(pop_payload.buf, pop_payload.size));
|
||||
|
||||
EXPECT_EQ(params.its_id.size(), static_cast<size_t>(inner->itsId.size));
|
||||
EXPECT_EQ(0, std::memcmp(inner->itsId.buf, params.its_id.data(), inner->itsId.size));
|
||||
EXPECT_EQ(Vanetza_Security_CertificateFormat_ts103097v131, inner->certificateFormat);
|
||||
|
||||
ASSERT_NE(nullptr, inner->requestedSubjectAttributes.appPermissions);
|
||||
ASSERT_EQ(1, inner->requestedSubjectAttributes.appPermissions->list.count);
|
||||
auto* first = inner->requestedSubjectAttributes.appPermissions->list.array[0];
|
||||
EXPECT_EQ(static_cast<long>(aid::SCR), first->psid);
|
||||
EXPECT_NE(nullptr, first->ssp);
|
||||
}
|
||||
|
||||
// The verification key placed inside the InnerEcRequest must match the
|
||||
// curve/encoding expected for its KeyType.
|
||||
TEST_F(EnrolmentRequestTest, inner_verification_key_matches_curve)
|
||||
{
|
||||
for (auto key_type : { KeyType::NistP256, KeyType::BrainpoolP256r1, KeyType::BrainpoolP384r1 }) {
|
||||
auto params = make_params(key_type);
|
||||
ByteBuffer encoded = build_signed_enrolment_request(m_security, params);
|
||||
|
||||
SignedData outer;
|
||||
ASSERT_TRUE(outer.decode(encoded));
|
||||
MgmtData mgmt = decode_unsecured<MgmtData>(*outer->content->choice.signedData);
|
||||
const auto& pop_sd = *mgmt->content.choice.enrolmentRequest.content->choice.signedData;
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_InnerEcRequest_t> inner(asn_DEF_Vanetza_Security_InnerEcRequest);
|
||||
const auto& pop_payload = pop_sd.tbsData->payload->data->content->choice.unsecuredData;
|
||||
ASSERT_TRUE(inner.decode(pop_payload.buf, pop_payload.size));
|
||||
|
||||
const auto& vkey = inner->publicKeys.verificationKey;
|
||||
switch (key_type) {
|
||||
case KeyType::NistP256:
|
||||
EXPECT_EQ(Vanetza_Security_PublicVerificationKey_PR_ecdsaNistP256, vkey.present);
|
||||
break;
|
||||
case KeyType::BrainpoolP256r1:
|
||||
EXPECT_EQ(Vanetza_Security_PublicVerificationKey_PR_ecdsaBrainpoolP256r1, vkey.present);
|
||||
break;
|
||||
case KeyType::BrainpoolP384r1:
|
||||
EXPECT_EQ(Vanetza_Security_PublicVerificationKey_PR_ecdsaBrainpoolP384r1, vkey.present);
|
||||
break;
|
||||
default:
|
||||
FAIL() << "unexpected key type";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ETSI TS 103 097 v1.3.1 clause 7.2.2 "Enrolment credential":
|
||||
// The appPermissions of an enrolment credential shall contain only the PSID
|
||||
// for secured certificate requests (SCR, 623).
|
||||
// This is also what the TS 103 525-3 abstract test suite requests in its
|
||||
// reference inner EC request, and what the 0_EU-EA_L0 EA's certIssuePermissions
|
||||
// authorise it to issue. Requesting CA/DENM/etc. permissions on the EC causes
|
||||
// the EA to reject the request (often with an opaque error).
|
||||
//
|
||||
// The builder is permissive by design: It accepts whatever the caller passes
|
||||
// so that test tools can also exercise non-conformant requests. But we fix
|
||||
// the *conformant* expected shape in a regression test so drift is caught.
|
||||
TEST_F(EnrolmentRequestTest, conformant_EC_permission_is_SCR_only_TS103097_7_2_2)
|
||||
{
|
||||
EnrolmentRequestParameters params;
|
||||
params.its_id = "station-conformant";
|
||||
params.verification_key = m_security.create_key(KeyType::NistP256);
|
||||
params.outer_signer_key = m_security.create_key(KeyType::NistP256);
|
||||
// TS 103 525-3 reference value PX_INNER_EC_CERTFICATE_BITMAP_SSP_SCR
|
||||
|
||||
ByteBuffer encoded = build_signed_enrolment_request(m_security, params);
|
||||
SignedData outer;
|
||||
ASSERT_TRUE(outer.decode(encoded));
|
||||
MgmtData mgmt = decode_unsecured<MgmtData>(*outer->content->choice.signedData);
|
||||
const auto& pop_sd = *mgmt->content.choice.enrolmentRequest.content->choice.signedData;
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_InnerEcRequest_t> inner(asn_DEF_Vanetza_Security_InnerEcRequest);
|
||||
const auto& pop_payload = pop_sd.tbsData->payload->data->content->choice.unsecuredData;
|
||||
ASSERT_TRUE(inner.decode(pop_payload.buf, pop_payload.size));
|
||||
|
||||
const auto* perms = inner->requestedSubjectAttributes.appPermissions;
|
||||
ASSERT_NE(nullptr, perms);
|
||||
ASSERT_EQ(1, perms->list.count);
|
||||
EXPECT_EQ(static_cast<long>(aid::SCR), perms->list.array[0]->psid);
|
||||
ASSERT_NE(nullptr, perms->list.array[0]->ssp);
|
||||
EXPECT_EQ(Vanetza_Security_ServiceSpecificPermissions_PR_bitmapSsp, perms->list.array[0]->ssp->present);
|
||||
}
|
||||
|
||||
// Mixed-curve stress: use Brainpool for the bootstrap key and NIST for the
|
||||
// verification key (or vice versa) and verify both signatures still validate.
|
||||
// This catches bugs where the code assumes both keys share a curve.
|
||||
TEST_F(EnrolmentRequestTest, mixed_curves_still_verify)
|
||||
{
|
||||
EnrolmentRequestParameters params;
|
||||
params.its_id = "mixed-curve-station";
|
||||
params.verification_key = m_security.create_key(KeyType::BrainpoolP256r1);
|
||||
params.outer_signer_key = m_security.create_key(KeyType::NistP256);
|
||||
|
||||
ByteBuffer encoded = build_signed_enrolment_request(m_security, params);
|
||||
SignedData outer;
|
||||
ASSERT_TRUE(outer.decode(encoded));
|
||||
const auto& outer_sd = *outer->content->choice.signedData;
|
||||
EXPECT_TRUE(m_security.verify(calculate_digest<Sha256Hash>(m_security, *outer_sd.tbsData, nullptr),
|
||||
make_signature(outer_sd.signature), params.outer_signer_key));
|
||||
|
||||
MgmtData mgmt = decode_unsecured<MgmtData>(outer_sd);
|
||||
const auto& pop_sd = *mgmt->content.choice.enrolmentRequest.content->choice.signedData;
|
||||
EXPECT_TRUE(m_security.verify(calculate_digest<Sha256Hash>(m_security, *pop_sd.tbsData, nullptr),
|
||||
make_signature(pop_sd.signature), params.verification_key));
|
||||
}
|
||||
|
||||
// TS 102 941 §6.2.3.2.1: for a re-keying (renewal) enrolment request, the
|
||||
// outer EtsiTs103097Data-Signed shall use SignerIdentifier = digest holding
|
||||
// HashedId8 of the current EC, and the signature shall be computed with the
|
||||
// EC's private key. Passing `outer_signer_certificate` selects this variant.
|
||||
TEST_F(EnrolmentRequestTest, outer_digest_signed_when_certificate_provided)
|
||||
{
|
||||
// Stand-in EC: a verification-key-only stub certificate whose private key
|
||||
// the security module can sign with.
|
||||
PublicKey ec_key = m_security.create_key(KeyType::NistP256);
|
||||
Certificate ec_cert = build_stub_certificate(ec_key);
|
||||
|
||||
EnrolmentRequestParameters params;
|
||||
params.its_id = "renewal-station"; // would be HashedId8 bytes in real use
|
||||
params.verification_key = m_security.create_key(KeyType::NistP256);
|
||||
params.outer_signer_key = ec_key;
|
||||
params.outer_signer_certificate = &ec_cert;
|
||||
|
||||
ByteBuffer encoded = build_signed_enrolment_request(m_security, params);
|
||||
|
||||
SignedData outer;
|
||||
ASSERT_TRUE(outer.decode(encoded));
|
||||
const auto& sd = *outer->content->choice.signedData;
|
||||
|
||||
// signer.present == digest, and digest matches HashedId8(ec_cert)
|
||||
ASSERT_EQ(Vanetza_Security_SignerIdentifier_PR_digest, sd.signer.present);
|
||||
HashedId8 expected_hid8 = ec_cert.calculate_hashed_id8(m_security);
|
||||
EXPECT_TRUE(sd.signer.choice.digest == expected_hid8.octets);
|
||||
|
||||
// signature verifies over calculate_digest(tbs, &ec_cert) using the EC key
|
||||
Sha256Hash digest = calculate_digest<Sha256Hash>(m_security, *sd.tbsData, &ec_cert.raw());
|
||||
EXPECT_TRUE(m_security.verify(digest, make_signature(sd.signature), ec_key));
|
||||
|
||||
// PoP (inner) remains signer = self regardless of outer signer choice.
|
||||
MgmtData mgmt = decode_unsecured<MgmtData>(sd);
|
||||
const auto& pop_sd = *mgmt->content.choice.enrolmentRequest.content->choice.signedData;
|
||||
EXPECT_EQ(Vanetza_Security_SignerIdentifier_PR_self, pop_sd.signer.present);
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,279 @@
|
||||
#include "ea_response.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "exception.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include "stub_certificate.hpp"
|
||||
#include "time.hpp"
|
||||
#include "validation.hpp"
|
||||
#include <vanetza/asn1/asn1c_wrapper.hpp>
|
||||
#include <vanetza/asn1/security/EnrolmentResponseCode.h>
|
||||
#include <vanetza/asn1/security/EtsiTs102941Data.h>
|
||||
#include <vanetza/asn1/security/EtsiTs103097Certificate.h>
|
||||
#include <vanetza/asn1/security/Ieee1609Dot2Data.h>
|
||||
#include <vanetza/asn1/security/InnerEcResponse.h>
|
||||
#include <vanetza/asn1/security/SignedData.h>
|
||||
#include <vanetza/asn1/security/Time64.h>
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
#include <vanetza/security/v3/basic_elements.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr std::uint8_t protocol_version = 3;
|
||||
|
||||
// Builder for a decrypted enrolment response. Defaults produce a structurally
|
||||
// valid response with code=ok and a synthetic EC certificate. Each knob
|
||||
// switches one invariant off so tests can assert the parser rejects it.
|
||||
struct ResponseBuilder
|
||||
{
|
||||
OpenSslSecurityModule* security;
|
||||
const Certificate* ea_cert;
|
||||
const PublicKey* ea_signing_key;
|
||||
|
||||
// Knobs
|
||||
Vanetza_Security_EnrolmentResponseCode_t code = Vanetza_Security_EnrolmentResponseCode_ok;
|
||||
bool include_certificate = true;
|
||||
boost::optional<long> psid_override;
|
||||
// NOTHING = use the correct `digest` variant; other values force that variant
|
||||
Vanetza_Security_SignerIdentifier_PR signer_variant = Vanetza_Security_SignerIdentifier_PR_NOTHING;
|
||||
// if set, forces the digest bytes placed in signer.digest (overriding the
|
||||
// correct EA HashedId8). Only consulted when signer_variant is `digest`
|
||||
// or NOTHING (digest default).
|
||||
boost::optional<HashedId8> signer_digest_override;
|
||||
bool corrupt_signature = false;
|
||||
// if true, wrap the final payload as unsecuredData instead of signedData
|
||||
bool outer_as_unsecured = false;
|
||||
|
||||
ByteBuffer build() const
|
||||
{
|
||||
// inner: EtsiTs102941Data { enrolmentResponse { InnerEcResponse } }
|
||||
MgmtData inner;
|
||||
inner->version = Vanetza_Security_Version_v1;
|
||||
inner->content.present = Vanetza_Security_EtsiTs102941DataContent_PR_enrolmentResponse;
|
||||
auto& ec_resp = inner->content.choice.enrolmentResponse;
|
||||
// 16-byte requestHash (SHA-256 prefix in the real flow)
|
||||
uint8_t req_hash[16];
|
||||
std::fill(std::begin(req_hash), std::end(req_hash), 0xAB);
|
||||
OCTET_STRING_fromBuf(&ec_resp.requestHash, reinterpret_cast<char*>(req_hash), 16);
|
||||
ec_resp.responseCode = code;
|
||||
if (include_certificate) {
|
||||
// Synthesize a dummy EC cert. Not cryptographically valid, just
|
||||
// OER-encodable, so the parser can decode it and the test can
|
||||
// check it came through intact.
|
||||
PublicKey dummy_key = security->create_key(KeyType::NistP256);
|
||||
Certificate dummy_ec = build_stub_certificate(dummy_key);
|
||||
ByteBuffer encoded_ec = dummy_ec.encode();
|
||||
void* decoded = nullptr;
|
||||
EXPECT_TRUE(asn1::decode_oer(asn_DEF_Vanetza_Security_EtsiTs103097Certificate, &decoded, encoded_ec));
|
||||
// InnerEcResponse.certificate is a pointer to the shim struct; the decoded layout is bit-compatible.
|
||||
ec_resp.certificate = static_cast<Vanetza_Security_EtsiTs103097Certificate*>(decoded);
|
||||
}
|
||||
EXPECT_TRUE(inner.validate());
|
||||
ByteBuffer inner_encoded = inner.encode();
|
||||
|
||||
// outer: EtsiTs103097Data { signedData or unsecuredData }
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_Ieee1609Dot2Data_t> outer(asn_DEF_Vanetza_Security_Ieee1609Dot2Data);
|
||||
outer->protocolVersion = protocol_version;
|
||||
outer->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
|
||||
if (outer_as_unsecured) {
|
||||
outer->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_unsecuredData;
|
||||
copy(inner_encoded, outer->content->choice.unsecuredData);
|
||||
return outer.encode();
|
||||
}
|
||||
|
||||
outer->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_signedData;
|
||||
outer->content->choice.signedData = asn1::allocate<Vanetza_Security_SignedData_t>();
|
||||
auto& sd = *outer->content->choice.signedData;
|
||||
sd.hashId = Vanetza_Security_HashAlgorithm_sha256;
|
||||
|
||||
// signer
|
||||
const auto variant = (signer_variant == Vanetza_Security_SignerIdentifier_PR_NOTHING) ?
|
||||
Vanetza_Security_SignerIdentifier_PR_digest :
|
||||
signer_variant;
|
||||
sd.signer.present = variant;
|
||||
if (variant == Vanetza_Security_SignerIdentifier_PR_digest) {
|
||||
HashedId8 ea_hid8 = ea_cert->calculate_hashed_id8(*security);
|
||||
if (signer_digest_override) {
|
||||
ea_hid8 = *signer_digest_override;
|
||||
}
|
||||
OCTET_STRING_fromBuf(&sd.signer.choice.digest, reinterpret_cast<const char*>(ea_hid8.octets.data()), 8);
|
||||
}
|
||||
|
||||
// tbsData
|
||||
sd.tbsData = asn1::allocate<Vanetza_Security_ToBeSignedData_t>();
|
||||
sd.tbsData->headerInfo.psid = psid_override.value_or(aid::SCR);
|
||||
sd.tbsData->headerInfo.generationTime = asn1::allocate<Vanetza_Security_Time64_t>();
|
||||
asn_uint642INTEGER(sd.tbsData->headerInfo.generationTime, security::v3::convert_time64(current_time()));
|
||||
|
||||
sd.tbsData->payload = asn1::allocate<Vanetza_Security_SignedDataPayload_t>();
|
||||
auto* sdp = sd.tbsData->payload;
|
||||
sdp->data = asn1::allocate<Vanetza_Security_EtsiTs103097Data_t>();
|
||||
sdp->data->protocolVersion = protocol_version;
|
||||
sdp->data->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
sdp->data->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_unsecuredData;
|
||||
copy(inner_encoded, sdp->data->content->choice.unsecuredData);
|
||||
|
||||
// signature
|
||||
const Vanetza_Security_Certificate_t* signer_cert_for_hash =
|
||||
(variant == Vanetza_Security_SignerIdentifier_PR_self) ? nullptr : &ea_cert->raw();
|
||||
Sha256Hash digest = calculate_digest<Sha256Hash>(*security, *sd.tbsData, signer_cert_for_hash);
|
||||
auto signature = security->sign(ByteBuffer { digest.octets.begin(), digest.octets.end() }, *ea_signing_key);
|
||||
EXPECT_TRUE(static_cast<bool>(signature));
|
||||
|
||||
sd.signature.present = Vanetza_Security_Signature_PR_ecdsaNistP256Signature;
|
||||
sd.signature.choice.ecdsaNistP256Signature.rSig.present = Vanetza_Security_EccP256CurvePoint_PR_x_only;
|
||||
copy_left_padded(signature->r, sd.signature.choice.ecdsaNistP256Signature.rSig.choice.x_only, 32);
|
||||
copy_left_padded(signature->s, sd.signature.choice.ecdsaNistP256Signature.sSig, 32);
|
||||
|
||||
if (corrupt_signature) {
|
||||
// Flip one byte in the encoded signature after we've laid it down.
|
||||
// Easiest: perturb sSig in place.
|
||||
auto& s_oct = sd.signature.choice.ecdsaNistP256Signature.sSig;
|
||||
if (s_oct.size > 0) {
|
||||
s_oct.buf[0] ^= 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
return outer.encode();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class EnrolmentResponseTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
EnrolmentResponseTest() :
|
||||
m_credentials(std::make_shared<MockCredentialStorage>()), m_security(m_credentials),
|
||||
m_ea_key(m_security.create_key(KeyType::NistP256)), m_ea_certificate(build_stub_certificate(m_ea_key))
|
||||
{
|
||||
}
|
||||
|
||||
ResponseBuilder builder()
|
||||
{
|
||||
return ResponseBuilder { &m_security, &m_ea_certificate, &m_ea_key };
|
||||
}
|
||||
|
||||
std::shared_ptr<MockCredentialStorage> m_credentials;
|
||||
OpenSslSecurityModule m_security;
|
||||
PublicKey m_ea_key;
|
||||
Certificate m_ea_certificate;
|
||||
};
|
||||
|
||||
// Happy path: a properly EA-signed response with code=ok and a certificate
|
||||
// round-trips into an EnrolmentResponse whose fields match what went in.
|
||||
TEST_F(EnrolmentResponseTest, happy_path_returns_code_ok_and_certificate)
|
||||
{
|
||||
ByteBuffer payload = builder().build();
|
||||
|
||||
EnrolmentResponse resp = parse_enrolment_response(m_security, payload, m_ea_certificate);
|
||||
|
||||
EXPECT_EQ(Vanetza_Security_EnrolmentResponseCode_ok, resp.code);
|
||||
ASSERT_TRUE(resp.certificate.has_value());
|
||||
EXPECT_EQ(16u, resp.request_hash.size());
|
||||
}
|
||||
|
||||
// The parser must reject random bytes that do not decode as Ieee1609Dot2Data.
|
||||
TEST_F(EnrolmentResponseTest, rejects_undecodable_input)
|
||||
{
|
||||
ByteBuffer garbage(16, 0xFF);
|
||||
EXPECT_THROW(parse_enrolment_response(m_security, garbage, m_ea_certificate), DecodingFailure);
|
||||
}
|
||||
|
||||
// The outer content must be signedData: unsecuredData is not acceptable for
|
||||
// an enrolment response.
|
||||
TEST_F(EnrolmentResponseTest, rejects_outer_unsecured_data)
|
||||
{
|
||||
ResponseBuilder b = builder();
|
||||
b.outer_as_unsecured = true;
|
||||
ByteBuffer payload = b.build();
|
||||
|
||||
EXPECT_THROW(parse_enrolment_response(m_security, payload, m_ea_certificate), DecodingFailure);
|
||||
}
|
||||
|
||||
// TS 102 941 §6.2.3.2.2: the outer signer shall be `digest` naming the EA.
|
||||
// A `self` signer (e.g. accidentally replaying a request shape) is rejected.
|
||||
TEST_F(EnrolmentResponseTest, rejects_self_signer)
|
||||
{
|
||||
ResponseBuilder b = builder();
|
||||
b.signer_variant = Vanetza_Security_SignerIdentifier_PR_self;
|
||||
ByteBuffer payload = b.build();
|
||||
|
||||
EXPECT_THROW(parse_enrolment_response(m_security, payload, m_ea_certificate), DecodingFailure);
|
||||
}
|
||||
|
||||
// The signer's digest must match HashedId8(ea_certificate). A mismatch means
|
||||
// the response was signed by someone else — must be rejected.
|
||||
TEST_F(EnrolmentResponseTest, rejects_signer_digest_mismatch)
|
||||
{
|
||||
ResponseBuilder b = builder();
|
||||
HashedId8 other;
|
||||
other.octets = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77 };
|
||||
b.signer_digest_override = other;
|
||||
ByteBuffer payload = b.build();
|
||||
|
||||
EXPECT_THROW(parse_enrolment_response(m_security, payload, m_ea_certificate), VerificationFailure);
|
||||
}
|
||||
|
||||
// TS 102 941 §6.2.3.2.2: the outer tbsData.headerInfo.psid shall be SCR.
|
||||
TEST_F(EnrolmentResponseTest, rejects_wrong_psid)
|
||||
{
|
||||
ResponseBuilder b = builder();
|
||||
b.psid_override = 0x20; // arbitrary non-SCR
|
||||
ByteBuffer payload = b.build();
|
||||
|
||||
EXPECT_THROW(parse_enrolment_response(m_security, payload, m_ea_certificate), DecodingFailure);
|
||||
}
|
||||
|
||||
// A signature that does not verify against the EA certificate must be
|
||||
// rejected — even when all structural fields match.
|
||||
TEST_F(EnrolmentResponseTest, rejects_bad_signature)
|
||||
{
|
||||
ResponseBuilder b = builder();
|
||||
b.corrupt_signature = true;
|
||||
ByteBuffer payload = b.build();
|
||||
|
||||
EXPECT_THROW(parse_enrolment_response(m_security, payload, m_ea_certificate), VerificationFailure);
|
||||
}
|
||||
|
||||
// A non-ok response code is not a structural failure; the parser returns it
|
||||
// to the caller so initial and renewal flows can treat it differently.
|
||||
// (Current initial flow will still throw on non-ok, but that's a caller
|
||||
// policy, not a parser one.)
|
||||
TEST_F(EnrolmentResponseTest, non_ok_code_returns_without_throwing)
|
||||
{
|
||||
ResponseBuilder b = builder();
|
||||
b.code = Vanetza_Security_EnrolmentResponseCode_deniedpermissions;
|
||||
b.include_certificate = false;
|
||||
ByteBuffer payload = b.build();
|
||||
|
||||
EnrolmentResponse resp = parse_enrolment_response(m_security, payload, m_ea_certificate);
|
||||
|
||||
EXPECT_EQ(Vanetza_Security_EnrolmentResponseCode_deniedpermissions, resp.code);
|
||||
EXPECT_FALSE(resp.certificate.has_value());
|
||||
}
|
||||
|
||||
// Code=ok but no certificate violates TS 102 941 — parser must reject.
|
||||
TEST_F(EnrolmentResponseTest, rejects_ok_without_certificate)
|
||||
{
|
||||
ResponseBuilder b = builder();
|
||||
b.code = Vanetza_Security_EnrolmentResponseCode_ok;
|
||||
b.include_certificate = false;
|
||||
ByteBuffer payload = b.build();
|
||||
|
||||
EXPECT_THROW(parse_enrolment_response(m_security, payload, m_ea_certificate), DecodingFailure);
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,358 @@
|
||||
#include "ecies.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <openssl/bn.h>
|
||||
#include <openssl/ec.h>
|
||||
#include <openssl/ecdh.h>
|
||||
#include <openssl/obj_mac.h>
|
||||
|
||||
using vanetza::ByteBuffer;
|
||||
using namespace vanetza::pki;
|
||||
|
||||
/**
|
||||
* Test vectors from IEEE 1609.2-2017 (Annex D)
|
||||
*/
|
||||
|
||||
TEST(Ecies, kdf2_vector1)
|
||||
{
|
||||
const ByteBuffer shared {{
|
||||
0x96, 0xC0, 0x56, 0x19, 0xD5, 0x6C, 0x32, 0x8A,
|
||||
0xB9, 0x5F, 0xE8, 0x4B, 0x18, 0x26, 0x4B, 0x08,
|
||||
0x72, 0x5B, 0x85, 0xE3, 0x3F, 0xD3, 0x4F, 0x08
|
||||
}};
|
||||
const ByteBuffer kdp;
|
||||
const ByteBuffer expected {{
|
||||
0x44, 0x30, 0x24, 0xc3, 0xda, 0xe6, 0x6b, 0x95,
|
||||
0xe6, 0xf5, 0x67, 0x06, 0x01, 0x55, 0x8f, 0x71
|
||||
}};
|
||||
|
||||
OpenSslSecurityModule security(std::make_shared<MockCredentialStorage>());
|
||||
EXPECT_EQ(expected, calculate_kdf2(security, shared, kdp, expected.size()));
|
||||
}
|
||||
|
||||
TEST(Ecies, kdf2_vector2)
|
||||
{
|
||||
const ByteBuffer shared {{
|
||||
0x96, 0xF6, 0x00, 0xB7, 0x3A, 0xD6, 0xAC, 0x56,
|
||||
0x29, 0x57, 0x7E, 0xCE, 0xD5, 0x17, 0x43, 0xDD,
|
||||
0x2C, 0x24, 0xC2, 0x1B, 0x1A, 0xC8, 0x3E, 0xE4
|
||||
}};
|
||||
const ByteBuffer kdp;
|
||||
const ByteBuffer expected {{
|
||||
0xb6, 0x29, 0x51, 0x62, 0xa7, 0x80, 0x4f, 0x56,
|
||||
0x67, 0xba, 0x90, 0x70, 0xf8, 0x2f, 0xa5, 0x22
|
||||
}};
|
||||
|
||||
OpenSslSecurityModule security(std::make_shared<MockCredentialStorage>());
|
||||
EXPECT_EQ(expected, calculate_kdf2(security, shared, kdp, expected.size()));
|
||||
}
|
||||
|
||||
TEST(Ecies, kdf2_vector3)
|
||||
{
|
||||
const ByteBuffer shared {{
|
||||
0x22, 0x51, 0x8B, 0x10, 0xE7, 0x0F, 0x2A, 0x3F,
|
||||
0x24, 0x38, 0x10, 0xAE, 0x32, 0x54, 0x13, 0x9E,
|
||||
0xFB, 0xEE, 0x04, 0xAA, 0x57, 0xC7, 0xAF, 0x7D
|
||||
}};
|
||||
const ByteBuffer kdp {{
|
||||
0x75, 0xEE, 0xF8, 0x1A, 0xA3, 0x04, 0x1E, 0x33,
|
||||
0xB8, 0x09, 0x71, 0x20, 0x3D, 0x2C, 0x0C, 0x52
|
||||
}};
|
||||
const ByteBuffer expected {{
|
||||
0xc4, 0x98, 0xaf, 0x77, 0x16, 0x1c, 0xc5, 0x9f,
|
||||
0x29, 0x62, 0xb9, 0xa7, 0x13, 0xe2, 0xb2, 0x15,
|
||||
0x15, 0x2d, 0x13, 0x97, 0x66, 0xce, 0x34, 0xa7,
|
||||
0x76, 0xdf, 0x11, 0x86, 0x6a, 0x69, 0xbf, 0x2e,
|
||||
0x52, 0xa1, 0x3d, 0x9c, 0x7c, 0x6f, 0xc8, 0x78,
|
||||
0xc5, 0x0c, 0x5e, 0xa0, 0xbc, 0x7b, 0x00, 0xe0,
|
||||
0xda, 0x24, 0x47, 0xcf, 0xd8, 0x74, 0xf6, 0xcf,
|
||||
0x92, 0xf3, 0x0d, 0x00, 0x97, 0x11, 0x14, 0x85,
|
||||
0x50, 0x0c, 0x90, 0xc3, 0xaf, 0x8b, 0x48, 0x78,
|
||||
0x72, 0xd0, 0x46, 0x85, 0xd1, 0x4c, 0x8d, 0x1d,
|
||||
0xc8, 0xd7, 0xfa, 0x08, 0xbe, 0xb0, 0xce, 0x0a,
|
||||
0xba, 0xbc, 0x11, 0xf0, 0xbd, 0x49, 0x62, 0x69,
|
||||
0x14, 0x2d, 0x43, 0x52, 0x5a, 0x78, 0xe5, 0xbc,
|
||||
0x79, 0xa1, 0x7f, 0x59, 0x67, 0x6a, 0x57, 0x06,
|
||||
0xdc, 0x54, 0xd5, 0x4d, 0x4d, 0x1f, 0x0b, 0xd7,
|
||||
0xe3, 0x86, 0x12, 0x8e, 0xc2, 0x6a, 0xfc, 0x21
|
||||
}};
|
||||
|
||||
OpenSslSecurityModule security(std::make_shared<MockCredentialStorage>());
|
||||
EXPECT_EQ(expected, calculate_kdf2(security, shared, kdp, expected.size()));
|
||||
}
|
||||
|
||||
TEST(Ecies, kdf2_vector4)
|
||||
{
|
||||
const ByteBuffer shared {{
|
||||
0x7E, 0x33, 0x5A, 0xFA, 0x4B, 0x31, 0xD7, 0x72,
|
||||
0xC0, 0x63, 0x5C, 0x7B, 0x0E, 0x06, 0xF2, 0x6F,
|
||||
0xCD, 0x78, 0x1D, 0xF9, 0x47, 0xD2, 0x99, 0x0A
|
||||
}};
|
||||
const ByteBuffer kdp {{
|
||||
0xD6, 0x5A, 0x48, 0x12, 0x73, 0x3F, 0x8C, 0xDB,
|
||||
0xCD, 0xFB, 0x4B, 0x2F, 0x4C, 0x19, 0x1D, 0x87
|
||||
}};
|
||||
const ByteBuffer expected {{
|
||||
0xc0, 0xbd, 0x9e, 0x38, 0xa8, 0xf9, 0xde, 0x14,
|
||||
0xc2, 0xac, 0xd3, 0x5b, 0x2f, 0x34, 0x10, 0xc6,
|
||||
0x98, 0x8c, 0xf0, 0x24, 0x00, 0x54, 0x36, 0x31,
|
||||
0xe0, 0xd6, 0xa4, 0xc1, 0xd0, 0x30, 0x36, 0x5a,
|
||||
0xcb, 0xf3, 0x98, 0x11, 0x5e, 0x51, 0xaa, 0xdd,
|
||||
0xeb, 0xdc, 0x95, 0x90, 0x66, 0x42, 0x10, 0xf9,
|
||||
0xaa, 0x9f, 0xed, 0x77, 0x0d, 0x4c, 0x57, 0xed,
|
||||
0xea, 0xfa, 0x0b, 0x8c, 0x14, 0xf9, 0x33, 0x00,
|
||||
0x86, 0x52, 0x51, 0x21, 0x8c, 0x26, 0x2d, 0x63,
|
||||
0xda, 0xdc, 0x47, 0xdf, 0xa0, 0xe0, 0x28, 0x48,
|
||||
0x26, 0x79, 0x39, 0x85, 0x13, 0x7e, 0x0a, 0x54,
|
||||
0x4e, 0xc8, 0x0a, 0xbf, 0x2f, 0xdf, 0x5a, 0xb9,
|
||||
0x0b, 0xda, 0xea, 0x66, 0x20, 0x40, 0x12, 0xef,
|
||||
0xe3, 0x49, 0x71, 0xdc, 0x43, 0x1d, 0x62, 0x5c,
|
||||
0xd9, 0xa3, 0x29, 0xb8, 0x21, 0x7c, 0xc8, 0xfd,
|
||||
0x0d, 0x9f, 0x02, 0xb1, 0x3f, 0x2f, 0x6b, 0x0b
|
||||
}};
|
||||
|
||||
OpenSslSecurityModule security(std::make_shared<MockCredentialStorage>());
|
||||
EXPECT_EQ(expected, calculate_kdf2(security, shared, kdp, expected.size()));
|
||||
}
|
||||
|
||||
// Regression test: previously, the 0_EU-EA_L0 certificate's encryption key is
|
||||
// stored in compressed-y-1 form and the ECIES context must still derive a
|
||||
// working shared secret from it.
|
||||
TEST(Ecies, shared_secret_with_compressed_y1_key)
|
||||
{
|
||||
// x coordinate of the 0_EU-EA_L0 EA certificate encryption key
|
||||
const ByteBuffer eu_ea_x {{
|
||||
0x53, 0x8A, 0x8D, 0x36, 0x0D, 0x00, 0xD8, 0x48,
|
||||
0x0F, 0x5B, 0x29, 0x54, 0xFA, 0xB2, 0x42, 0xFB,
|
||||
0x98, 0xB3, 0x38, 0x70, 0xF7, 0xA0, 0xC3, 0x3C,
|
||||
0xF6, 0x52, 0xCB, 0x46, 0xA1, 0x74, 0xE2, 0x48
|
||||
}};
|
||||
|
||||
PublicKey compressed;
|
||||
compressed.type = KeyType::NistP256;
|
||||
compressed.compression = KeyCompression::Y1;
|
||||
compressed.x = eu_ea_x;
|
||||
|
||||
OpenSslSecurityModule security(std::make_shared<MockCredentialStorage>());
|
||||
Sha256Hash info {};
|
||||
// Must not throw and must produce a non-empty 32-byte shared secret
|
||||
auto ecies = security.create_ecies_context(compressed, info);
|
||||
EXPECT_EQ(32u, ecies->shared_secret().size());
|
||||
EXPECT_FALSE(ecies->shared_secret() == ByteBuffer(32, 0));
|
||||
}
|
||||
|
||||
TEST(Ecies, encryption_roundtrip_compressed_receiver)
|
||||
{
|
||||
// Receiver was created as uncompressed but is then compressed before handing it
|
||||
// to the ECIES context. Decryption must still succeed -- otherwise we have a
|
||||
// bug in how compressed keys are mapped back to curve points.
|
||||
OpenSslSecurityModule security(std::make_shared<MockCredentialStorage>());
|
||||
PublicKey receiver = security.create_key(KeyType::NistP256);
|
||||
ASSERT_NE(KeyCompression::NoCompression, receiver.compression);
|
||||
|
||||
Sha256Hash info { 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef };
|
||||
auto ecies = security.create_ecies_context(receiver, info);
|
||||
|
||||
const ByteBuffer plaintext {{
|
||||
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
|
||||
0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff
|
||||
}};
|
||||
const ByteBuffer ciphertext = ecies->encrypt(plaintext);
|
||||
const ByteBuffer decrypted = ecies->decrypt(ciphertext.data(), ciphertext.size());
|
||||
EXPECT_NE(plaintext, ciphertext);
|
||||
EXPECT_EQ(plaintext, decrypted);
|
||||
}
|
||||
|
||||
TEST(Ecies, encryption_roundtrip)
|
||||
{
|
||||
OpenSslSecurityModule security(std::make_shared<MockCredentialStorage>());
|
||||
PublicKey receiver = security.create_key(KeyType::NistP256);
|
||||
Sha256Hash info { 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef };
|
||||
auto ecies = security.create_ecies_context(receiver, info);
|
||||
|
||||
const ByteBuffer plaintext {{
|
||||
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
|
||||
0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff
|
||||
}};
|
||||
const ByteBuffer ciphertext = ecies->encrypt(plaintext);
|
||||
const ByteBuffer decrypted = ecies->decrypt(ciphertext.data(), ciphertext.size());
|
||||
EXPECT_NE(plaintext, ciphertext);
|
||||
EXPECT_EQ(plaintext, decrypted);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Mock that returns fixed test-vector values, so encrypt_key() can be tested
|
||||
// against IEEE 1609.2 Annex D.6.2 known-answer data.
|
||||
class FixedEciesContext : public SecurityModule::EciesContext
|
||||
{
|
||||
public:
|
||||
FixedEciesContext(ByteBuffer shared) : m_shared_secret(std::move(shared))
|
||||
{
|
||||
}
|
||||
|
||||
PublicKey ephemeral_public_key() const override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
PublicKey recipient_public_key() const override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
ByteBuffer shared_secret() const override
|
||||
{
|
||||
return m_shared_secret;
|
||||
}
|
||||
|
||||
ByteBuffer encrypted_key() const override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
ByteBuffer authentication_tag() const override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
ByteBuffer nonce() const override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
void nonce(const ByteBuffer&) override
|
||||
{
|
||||
}
|
||||
|
||||
ByteBuffer encrypt(const ByteBuffer&) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
ByteBuffer decrypt(const std::uint8_t*, std::size_t) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
private:
|
||||
ByteBuffer m_shared_secret;
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// IEEE 1609.2-2016 Annex D.6.2 ECIES1 test vector
|
||||
// End-to-end: runs ECDH with the vector's ephemeral private key and recipient
|
||||
// public key through OpenSSL, then pipes the resulting shared secret through
|
||||
// encrypt_key(). This catches any bug in the ECDH input handling that a
|
||||
// standalone KDF2 test would miss.
|
||||
TEST(Ecies, ecies_IEEE_1609_2_Annex_D_6_2_ECIES1_full)
|
||||
{
|
||||
const ByteBuffer sender_eph_priv {{
|
||||
0x13, 0x84, 0xC3, 0x1D, 0x69, 0x82, 0xD5, 0x2B,
|
||||
0xCA, 0x3B, 0xED, 0x8A, 0x7E, 0x60, 0xF5, 0x2F,
|
||||
0xEC, 0xDA, 0xB4, 0x4E, 0x5C, 0x0E, 0xA1, 0x66,
|
||||
0x81, 0x5A, 0x81, 0x59, 0xE0, 0x9F, 0xFB, 0x42
|
||||
}};
|
||||
const ByteBuffer recipient_pub_x {{
|
||||
0x8C, 0x5E, 0x20, 0xFE, 0x31, 0x93, 0x5F, 0x6F,
|
||||
0xA6, 0x82, 0xA1, 0xF6, 0xD4, 0x6E, 0x44, 0x68,
|
||||
0x53, 0x4F, 0xFE, 0xA1, 0xA6, 0x98, 0xB1, 0x4B,
|
||||
0x0B, 0x12, 0x51, 0x3E, 0xED, 0x8D, 0xEB, 0x11
|
||||
}};
|
||||
const ByteBuffer recipient_pub_y {{
|
||||
0x12, 0x70, 0xFE, 0xC2, 0x42, 0x7E, 0x6A, 0x15,
|
||||
0x4D, 0xFC, 0xAE, 0x33, 0x68, 0x58, 0x43, 0x96,
|
||||
0xC8, 0x25, 0x1A, 0x04, 0xE2, 0xAE, 0x7D, 0x87,
|
||||
0xB0, 0x16, 0xFF, 0x65, 0xD2, 0x2D, 0x6F, 0x9E
|
||||
}};
|
||||
const ByteBuffer aes_key {{
|
||||
0x91, 0x69, 0x15, 0x5B, 0x08, 0xB0, 0x76, 0x74,
|
||||
0xCB, 0xAD, 0xF7, 0x5F, 0xB4, 0x6A, 0x7B, 0x0D
|
||||
}};
|
||||
const Sha256Hash recipient_info {{
|
||||
0xA6, 0xB7, 0xB5, 0x25, 0x54, 0xB4, 0x20, 0x3F,
|
||||
0x7E, 0x3A, 0xCF, 0xDB, 0x3A, 0x3E, 0xD8, 0x67,
|
||||
0x4E, 0xE0, 0x86, 0xCE, 0x59, 0x06, 0xA7, 0xCA,
|
||||
0xC2, 0xF8, 0xA3, 0x98, 0x30, 0x6D, 0x3B, 0xE9
|
||||
}};
|
||||
const ByteBuffer expected_wrapped {{
|
||||
0xA6, 0x34, 0x20, 0x13, 0xD6, 0x23, 0xAD, 0x6C,
|
||||
0x5F, 0x68, 0x82, 0x46, 0x96, 0x73, 0xAE, 0x33
|
||||
}};
|
||||
const ByteBuffer expected_tag {{
|
||||
0x80, 0xE1, 0xD8, 0x5D, 0x30, 0xF1, 0xBA, 0xE4,
|
||||
0xEC, 0xF1, 0xA5, 0x34, 0xA8, 0x9A, 0x07, 0x86
|
||||
}};
|
||||
|
||||
// Set up sender ephemeral EC_KEY with the test vector's private scalar
|
||||
EC_KEY* sender = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
|
||||
BIGNUM* priv_bn = BN_bin2bn(sender_eph_priv.data(), sender_eph_priv.size(), nullptr);
|
||||
ASSERT_EQ(1, EC_KEY_set_private_key(sender, priv_bn));
|
||||
|
||||
// Reconstruct recipient public point from (x, y)
|
||||
const EC_GROUP* group = EC_KEY_get0_group(sender);
|
||||
EC_POINT* recipient_point = EC_POINT_new(group);
|
||||
BIGNUM* rx = BN_bin2bn(recipient_pub_x.data(), recipient_pub_x.size(), nullptr);
|
||||
BIGNUM* ry = BN_bin2bn(recipient_pub_y.data(), recipient_pub_y.size(), nullptr);
|
||||
ASSERT_EQ(1, EC_POINT_set_affine_coordinates(group, recipient_point, rx, ry, nullptr));
|
||||
|
||||
// Compute shared secret
|
||||
ByteBuffer shared(32);
|
||||
int got = ECDH_compute_key(shared.data(), shared.size(), recipient_point, sender, nullptr);
|
||||
ASSERT_EQ(32, got);
|
||||
|
||||
BN_free(priv_bn);
|
||||
BN_free(rx);
|
||||
BN_free(ry);
|
||||
EC_POINT_free(recipient_point);
|
||||
EC_KEY_free(sender);
|
||||
|
||||
// Now feed the shared secret through encrypt_key
|
||||
OpenSslSecurityModule security(std::make_shared<MockCredentialStorage>());
|
||||
FixedEciesContext ecies(shared);
|
||||
auto result = encrypt_key(security, ecies, aes_key, recipient_info);
|
||||
|
||||
EXPECT_EQ(expected_wrapped, result.wrapped_key);
|
||||
EXPECT_EQ(expected_tag, result.authentication_tag);
|
||||
}
|
||||
|
||||
// IEEE 1609.2-2016 Annex D.6.2 ECIES1 test vector
|
||||
// Verifies the KDF2 + key wrapping + HMAC-SHA256 tag chain used by encrypt_key().
|
||||
TEST(Ecies, encrypt_key_IEEE_1609_2_Annex_D_6_2_ECIES1)
|
||||
{
|
||||
// Shared secret z = x-coord of [k_E] * Q_recipient (precomputed)
|
||||
const ByteBuffer shared_secret {{
|
||||
0xd4, 0x43, 0x08, 0x02, 0x3f, 0xbb, 0xd3, 0xe9,
|
||||
0x06, 0xb9, 0xf3, 0xb4, 0x0e, 0x5e, 0x0b, 0x62,
|
||||
0x54, 0x11, 0x70, 0x3c, 0x4a, 0x99, 0x24, 0x9d,
|
||||
0x35, 0xa1, 0x39, 0x06, 0x38, 0x6a, 0x3e, 0xe3
|
||||
}};
|
||||
const ByteBuffer aes_key {{
|
||||
0x91, 0x69, 0x15, 0x5B, 0x08, 0xB0, 0x76, 0x74,
|
||||
0xCB, 0xAD, 0xF7, 0x5F, 0xB4, 0x6A, 0x7B, 0x0D
|
||||
}};
|
||||
const Sha256Hash recipient_info {{
|
||||
0xA6, 0xB7, 0xB5, 0x25, 0x54, 0xB4, 0x20, 0x3F,
|
||||
0x7E, 0x3A, 0xCF, 0xDB, 0x3A, 0x3E, 0xD8, 0x67,
|
||||
0x4E, 0xE0, 0x86, 0xCE, 0x59, 0x06, 0xA7, 0xCA,
|
||||
0xC2, 0xF8, 0xA3, 0x98, 0x30, 0x6D, 0x3B, 0xE9
|
||||
}};
|
||||
const ByteBuffer expected_wrapped {{
|
||||
0xA6, 0x34, 0x20, 0x13, 0xD6, 0x23, 0xAD, 0x6C,
|
||||
0x5F, 0x68, 0x82, 0x46, 0x96, 0x73, 0xAE, 0x33
|
||||
}};
|
||||
const ByteBuffer expected_tag {{
|
||||
0x80, 0xE1, 0xD8, 0x5D, 0x30, 0xF1, 0xBA, 0xE4,
|
||||
0xEC, 0xF1, 0xA5, 0x34, 0xA8, 0x9A, 0x07, 0x86
|
||||
}};
|
||||
|
||||
OpenSslSecurityModule security(std::make_shared<MockCredentialStorage>());
|
||||
FixedEciesContext ecies(shared_secret);
|
||||
auto result = encrypt_key(security, ecies, aes_key, recipient_info);
|
||||
|
||||
EXPECT_EQ(expected_wrapped, result.wrapped_key);
|
||||
EXPECT_EQ(expected_tag, result.authentication_tag);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#include "hexstring.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace vanetza;
|
||||
using namespace vanetza::pki;
|
||||
|
||||
TEST(HexString, encode_buffer)
|
||||
{
|
||||
const ByteBuffer buffer { 0x00, 0x0f, 0xa5, 0xff };
|
||||
EXPECT_EQ("000FA5FF", hexstring(buffer));
|
||||
EXPECT_EQ("", hexstring(ByteBuffer {}));
|
||||
}
|
||||
|
||||
TEST(HexString, encode_pointer)
|
||||
{
|
||||
const std::uint8_t bytes[] { 0xde, 0xad, 0xbe, 0xef };
|
||||
EXPECT_EQ("DEADBEEF", hexstring(bytes, sizeof(bytes)));
|
||||
}
|
||||
|
||||
TEST(HexString, encode_array)
|
||||
{
|
||||
const std::array<std::uint8_t, 3> array { 0x01, 0x23, 0x45 };
|
||||
EXPECT_EQ("012345", hexstring(array));
|
||||
}
|
||||
|
||||
TEST(HexString, encode_string)
|
||||
{
|
||||
EXPECT_EQ("414243", hexstring(std::string { "ABC" }));
|
||||
}
|
||||
|
||||
TEST(HexString, is_valid_accepts_hex)
|
||||
{
|
||||
EXPECT_TRUE(is_valid_hexstring("00"));
|
||||
EXPECT_TRUE(is_valid_hexstring("DEADBEEF"));
|
||||
EXPECT_TRUE(is_valid_hexstring("deadbeef"));
|
||||
EXPECT_TRUE(is_valid_hexstring("0123456789abcdefABCDEF"));
|
||||
}
|
||||
|
||||
TEST(HexString, is_valid_rejects_empty)
|
||||
{
|
||||
EXPECT_FALSE(is_valid_hexstring(""));
|
||||
}
|
||||
|
||||
TEST(HexString, is_valid_rejects_odd_length)
|
||||
{
|
||||
EXPECT_FALSE(is_valid_hexstring("0"));
|
||||
EXPECT_FALSE(is_valid_hexstring("abc"));
|
||||
}
|
||||
|
||||
TEST(HexString, is_valid_rejects_non_hex)
|
||||
{
|
||||
EXPECT_FALSE(is_valid_hexstring("0g"));
|
||||
EXPECT_FALSE(is_valid_hexstring("xy"));
|
||||
EXPECT_FALSE(is_valid_hexstring("12 34"));
|
||||
EXPECT_FALSE(is_valid_hexstring("0x12"));
|
||||
}
|
||||
|
||||
TEST(HexString, parse_decodes_to_bytes)
|
||||
{
|
||||
const std::string raw = parse_hexstring("DEADBEEF");
|
||||
const ByteBuffer expected { 0xde, 0xad, 0xbe, 0xef };
|
||||
EXPECT_EQ(ByteBuffer(raw.begin(), raw.end()), expected);
|
||||
}
|
||||
|
||||
TEST(HexString, parse_is_case_insensitive)
|
||||
{
|
||||
EXPECT_EQ(parse_hexstring("abcdef"), parse_hexstring("ABCDEF"));
|
||||
}
|
||||
|
||||
TEST(HexString, parse_roundtrip)
|
||||
{
|
||||
const ByteBuffer buffer { 0x00, 0x7f, 0x80, 0xff };
|
||||
const std::string raw = parse_hexstring(hexstring(buffer));
|
||||
EXPECT_EQ(ByteBuffer(raw.begin(), raw.end()), buffer);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
#include "http.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace vanetza::pki;
|
||||
|
||||
TEST(HttpQuery, from_url_http)
|
||||
{
|
||||
HttpQuery result = HttpQuery::from_url("http://www.example.com");
|
||||
EXPECT_EQ("www.example.com", result.host);
|
||||
EXPECT_EQ("http", result.which_service());
|
||||
EXPECT_EQ("", result.path);
|
||||
EXPECT_FALSE(result.secure);
|
||||
}
|
||||
|
||||
TEST(HttpQuery, from_url_http_with_port)
|
||||
{
|
||||
HttpQuery result = HttpQuery::from_url("http://www.example.com:80");
|
||||
EXPECT_EQ("www.example.com", result.host);
|
||||
EXPECT_EQ("80", result.which_service());
|
||||
EXPECT_FALSE(result.secure);
|
||||
}
|
||||
|
||||
TEST(HttpQuery, from_url_http_with_path)
|
||||
{
|
||||
HttpQuery result = HttpQuery::from_url("http://www.example.com/foo/bar");
|
||||
EXPECT_EQ("www.example.com", result.host);
|
||||
EXPECT_EQ("/foo/bar", result.path);
|
||||
}
|
||||
|
||||
TEST(HttpQuery, from_url_http_with_port_and_path)
|
||||
{
|
||||
HttpQuery result = HttpQuery::from_url("http://www.example.com:80/foo/bar");
|
||||
EXPECT_EQ("www.example.com", result.host);
|
||||
EXPECT_EQ("/foo/bar", result.path);
|
||||
EXPECT_EQ("80", result.which_service());
|
||||
}
|
||||
|
||||
TEST(HttpQuery, from_url_broken_prefix)
|
||||
{
|
||||
EXPECT_ANY_THROW(HttpQuery::from_url("shttp://www.example.com"));
|
||||
}
|
||||
|
||||
TEST(HttpQuery, from_url_https)
|
||||
{
|
||||
HttpQuery result = HttpQuery::from_url("https://example.com/");
|
||||
EXPECT_EQ("example.com", result.host);
|
||||
EXPECT_EQ("/", result.path);
|
||||
EXPECT_EQ("https", result.which_service());
|
||||
EXPECT_TRUE(result.secure);
|
||||
}
|
||||
|
||||
TEST(HttpQuery, from_url_https_with_port)
|
||||
{
|
||||
HttpQuery result = HttpQuery::from_url("https://www.example.com:8080");
|
||||
EXPECT_EQ("www.example.com", result.host);
|
||||
EXPECT_EQ("", result.path);
|
||||
EXPECT_EQ("8080", result.which_service());
|
||||
EXPECT_TRUE(result.secure);
|
||||
}
|
||||
|
||||
TEST(ResolveUrl, empty_reference_keeps_base)
|
||||
{
|
||||
EXPECT_EQ("https://aa.example.com/aa/0001", resolve_url("https://aa.example.com/aa/0001", ""));
|
||||
}
|
||||
|
||||
TEST(ResolveUrl, absolute_reference_passes_through)
|
||||
{
|
||||
EXPECT_EQ("http://other.com/x", resolve_url("https://aa.example.com/aa/0001", "http://other.com/x"));
|
||||
EXPECT_EQ("https://other.com", resolve_url("https://aa.example.com/aa/0001", "https://other.com"));
|
||||
}
|
||||
|
||||
TEST(ResolveUrl, absolute_path_replaces_path)
|
||||
{
|
||||
EXPECT_EQ("https://aa.example.com/foo", resolve_url("https://aa.example.com/aa/0001", "/foo"));
|
||||
EXPECT_EQ("https://aa.example.com/", resolve_url("https://aa.example.com/aa/0001", "/"));
|
||||
}
|
||||
|
||||
TEST(ResolveUrl, absolute_path_keeps_authority_with_port)
|
||||
{
|
||||
EXPECT_EQ("https://aa.example.com:8443/foo", resolve_url("https://aa.example.com:8443/aa/0001", "/foo"));
|
||||
}
|
||||
|
||||
TEST(ResolveUrl, relative_path_merges_onto_base_directory)
|
||||
{
|
||||
EXPECT_EQ("https://aa.example.com/aa/foo", resolve_url("https://aa.example.com/aa/0001", "foo"));
|
||||
// base without a path is treated as having a root directory
|
||||
EXPECT_EQ("https://aa.example.com/foo", resolve_url("https://aa.example.com", "foo"));
|
||||
// a trailing slash on the base keeps the last segment
|
||||
EXPECT_EQ("https://aa.example.com/aa/0001/foo", resolve_url("https://aa.example.com/aa/0001/", "foo"));
|
||||
}
|
||||
|
||||
TEST(ResolveUrl, network_path_replaces_authority_keeps_scheme)
|
||||
{
|
||||
EXPECT_EQ("https://other.com/z", resolve_url("https://aa.example.com/aa/0001", "//other.com/z"));
|
||||
EXPECT_EQ("http://other.com/z", resolve_url("http://aa.example.com/aa/0001", "//other.com/z"));
|
||||
}
|
||||
|
||||
TEST(ResolveUrl, base_query_and_fragment_are_dropped)
|
||||
{
|
||||
EXPECT_EQ("https://aa.example.com/foo", resolve_url("https://aa.example.com/aa/0001?x=1#frag", "/foo"));
|
||||
}
|
||||
|
||||
TEST(ResolveUrl, unparseable_base_throws)
|
||||
{
|
||||
EXPECT_ANY_THROW(resolve_url("not-a-url", "/foo"));
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#include "openssl.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <openssl/bn.h>
|
||||
#include <openssl/ec.h>
|
||||
#include <openssl/obj_mac.h>
|
||||
|
||||
using vanetza::ByteBuffer;
|
||||
using namespace vanetza::pki;
|
||||
|
||||
// Brainpool P-384 known-answer vector.
|
||||
// y is even, so the compressed form is Y0 and the compressed x bytes equal the uncompressed x bytes.
|
||||
static const ByteBuffer brainpool_p384_x {{
|
||||
0x3b, 0xef, 0x3b, 0xa0, 0x6b, 0x70, 0xe7, 0x20,
|
||||
0x03, 0x3e, 0x63, 0xdd, 0x7d, 0xf8, 0x7c, 0xd8,
|
||||
0x80, 0x84, 0x25, 0x0a, 0xdc, 0x52, 0xf3, 0xa7,
|
||||
0x61, 0xaf, 0xaf, 0xa0, 0x71, 0xdf, 0x29, 0x07,
|
||||
0x92, 0x45, 0x90, 0x57, 0x84, 0xe4, 0x85, 0x9d,
|
||||
0x02, 0x53, 0x67, 0xd7, 0xf4, 0xe9, 0x46, 0x30
|
||||
}};
|
||||
|
||||
static const ByteBuffer brainpool_p384_y {{
|
||||
0x08, 0x82, 0xc2, 0xf5, 0x04, 0x74, 0x72, 0xfe,
|
||||
0xcb, 0x65, 0xa4, 0xc4, 0x37, 0xfd, 0x22, 0x08,
|
||||
0x59, 0x83, 0x31, 0xbf, 0xb6, 0xcc, 0xb9, 0x30,
|
||||
0xb6, 0x73, 0xf7, 0xcc, 0x06, 0x51, 0x1e, 0x14,
|
||||
0xba, 0x52, 0x9a, 0xcf, 0x95, 0x25, 0x40, 0x85,
|
||||
0x83, 0xe7, 0x72, 0x53, 0x46, 0xb1, 0xb4, 0xba
|
||||
}};
|
||||
|
||||
TEST(KeyCompression, brainpool_p384_compress)
|
||||
{
|
||||
OpenSslPointer<EC_GROUP> group { EC_GROUP_new_by_curve_name(NID_brainpoolP384r1) };
|
||||
OpenSslPointer<EC_POINT> point { EC_POINT_new(group.raw()) };
|
||||
auto x = make_bignum(brainpool_p384_x);
|
||||
auto y = make_bignum(brainpool_p384_y);
|
||||
openssl_result(EC_POINT_set_affine_coordinates(group.raw(), point.raw(), x.raw(), y.raw(), nullptr), "set affine");
|
||||
|
||||
OpenSslPointer<EC_KEY> ec_key { EC_KEY_new_by_curve_name(NID_brainpoolP384r1) };
|
||||
openssl_result(EC_KEY_set_public_key(ec_key.raw(), point.raw()), "set pubkey");
|
||||
|
||||
PublicKey pub = make_public_key(ec_key.raw());
|
||||
EXPECT_EQ(KeyType::BrainpoolP384r1, pub.type);
|
||||
EXPECT_EQ(KeyCompression::Y0, pub.compression);
|
||||
EXPECT_EQ(brainpool_p384_x, pub.x);
|
||||
EXPECT_TRUE(pub.y.empty());
|
||||
}
|
||||
|
||||
TEST(KeyCompression, brainpool_p384_decompress)
|
||||
{
|
||||
PublicKey compressed;
|
||||
compressed.type = KeyType::BrainpoolP384r1;
|
||||
compressed.compression = KeyCompression::Y0;
|
||||
compressed.x = brainpool_p384_x;
|
||||
|
||||
OpenSslPointer<EC_POINT> point = make_ec_point(compressed);
|
||||
OpenSslPointer<EC_GROUP> group { EC_GROUP_new_by_curve_name(NID_brainpoolP384r1) };
|
||||
OpenSslPointer<BIGNUM> rx { BN_new() };
|
||||
OpenSslPointer<BIGNUM> ry { BN_new() };
|
||||
openssl_result(EC_POINT_get_affine_coordinates(group.raw(), point.raw(), rx.raw(), ry.raw(), nullptr),
|
||||
"get affine");
|
||||
|
||||
EXPECT_EQ(brainpool_p384_x, make_buffer(rx.raw()));
|
||||
EXPECT_EQ(brainpool_p384_y, make_buffer(ry.raw()));
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include "credential_storage.hpp"
|
||||
#include <vanetza/security/public_key.hpp>
|
||||
#include <boost/range/adaptor/map.hpp>
|
||||
#include <map>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
// In-memory credential storage suitable for unit tests. Store/fetch/discard
|
||||
// behave like a small std::map keyed by the public key's canonical hex string.
|
||||
class MockCredentialStorage : public CredentialStorage
|
||||
{
|
||||
public:
|
||||
void store(const PublicKey& pub, const PrivateKey& priv) override
|
||||
{
|
||||
m_credentials[security::canonical_hexstring(pub)] = priv;
|
||||
}
|
||||
|
||||
boost::optional<PrivateKey> fetch(const PublicKey& pub) override
|
||||
{
|
||||
auto it = m_credentials.find(security::canonical_hexstring(pub));
|
||||
if (it != m_credentials.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
bool discard(const PublicKey& pub) override
|
||||
{
|
||||
return m_credentials.erase(security::canonical_hexstring(pub)) > 0;
|
||||
}
|
||||
|
||||
CredentialNameRange list() const override
|
||||
{
|
||||
return m_credentials | boost::adaptors::map_keys;
|
||||
}
|
||||
|
||||
bool discard(const std::string& canonical_hex) override
|
||||
{
|
||||
return m_credentials.erase(canonical_hex) > 0;
|
||||
}
|
||||
|
||||
bool contains(const std::string& canonical_hex) const override
|
||||
{
|
||||
return m_credentials.count(canonical_hex) > 0;
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<std::string, PrivateKey> m_credentials;
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "pem.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace vanetza::pki;
|
||||
|
||||
static const std::string privKeyPem =
|
||||
"-----BEGIN EC PRIVATE KEY-----\n"
|
||||
"MHcCAQEEIHlpKyVDNCQ2x5IFeBelQyyvANI8iTFIoCTfNARm1LVxoAoGCCqGSM49\n"
|
||||
"AwEHoUQDQgAEvfkk5CcwIqP29fGu9n6kXmojUpV2xUlnM2BwJMoYmiVU7lF9p7tu\n"
|
||||
"+KUbGlhPaYLN0UZ13KQTmi3gV3aLolyXUw==\n"
|
||||
"-----END EC PRIVATE KEY-----";
|
||||
|
||||
static const vanetza::ByteBuffer privKeyRaw = {
|
||||
0x79, 0x69, 0x2b, 0x25, 0x43, 0x34, 0x24, 0x36,
|
||||
0xc7, 0x92, 0x05, 0x78, 0x17, 0xa5, 0x43, 0x2c,
|
||||
0xaf, 0x00, 0xd2, 0x3c, 0x89, 0x31, 0x48, 0xa0,
|
||||
0x24, 0xdf, 0x34, 0x04, 0x66, 0xd4, 0xb5, 0x71
|
||||
};
|
||||
|
||||
TEST(ReadPemPrivateKey, valid_prime256v1)
|
||||
{
|
||||
boost::optional<PrivateKey> key = parse_pem_private_key(privKeyPem);
|
||||
ASSERT_TRUE(key);
|
||||
EXPECT_EQ(KeyType::NistP256, key->type);
|
||||
EXPECT_EQ(privKeyRaw, key->key);
|
||||
}
|
||||
|
||||
TEST(ReadPemPrivateKey, invalid_pem)
|
||||
{
|
||||
EXPECT_FALSE(parse_pem_private_key("not a PEM"));
|
||||
EXPECT_FALSE(parse_pem_private_key(""));
|
||||
}
|
||||
|
||||
TEST(WritePemPrivateKey, roundtrip_generated)
|
||||
{
|
||||
for (KeyType type : { KeyType::NistP256, KeyType::BrainpoolP256r1, KeyType::BrainpoolP384r1 }) {
|
||||
PrivateKey generated = generate_private_key(type);
|
||||
std::string pem = make_pem(generated);
|
||||
|
||||
boost::optional<PrivateKey> parsed = parse_pem_private_key(pem);
|
||||
ASSERT_TRUE(parsed);
|
||||
EXPECT_EQ(generated.type, parsed->type);
|
||||
EXPECT_EQ(generated.key, parsed->key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
#include "prune_command.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "certificate_filesystem_storage.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include "stub_certificate.hpp"
|
||||
#include "stub_station_configuration.hpp"
|
||||
#include <vanetza/security/public_key.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
using vanetza::Clock;
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
PublicKey make_random_pubkey()
|
||||
{
|
||||
// SecurityModule::create_key generates a fresh key pair, stores the
|
||||
// private half in credential storage, and returns the public half.
|
||||
// Tests derive the canonical hex from the returned public key so test data
|
||||
// stays self-consistent.
|
||||
static auto credentials = std::make_shared<MockCredentialStorage>();
|
||||
static OpenSslSecurityModule sm(credentials);
|
||||
return sm.create_key(KeyType::BrainpoolP256r1);
|
||||
}
|
||||
|
||||
class PruneTestFixture : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
root = std::filesystem::temp_directory_path() / "pki_unit_test_prune";
|
||||
std::filesystem::remove_all(root);
|
||||
|
||||
cfg.credentials = std::make_shared<MockCredentialStorage>();
|
||||
cfg.security = std::make_shared<OpenSslSecurityModule>(cfg.credentials);
|
||||
const auto certs_dir = root / "certs";
|
||||
cfg.root_ca = std::make_shared<CertificateFilesystemStorage>(cfg.security, certs_dir, ".rca");
|
||||
cfg.enrolment_credentials = std::make_shared<CertificateFilesystemStorage>(cfg.security, certs_dir, ".ec");
|
||||
cfg.tlm = std::make_shared<CertificateFilesystemStorage>(cfg.security, certs_dir, ".tlm");
|
||||
cfg.tickets = std::make_shared<CertificateFilesystemStorage>(cfg.security, certs_dir, ".at");
|
||||
cfg.station = std::make_shared<StubStationConfiguration>();
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
|
||||
// Typed accessor for the StubStationConfiguration held in cfg.station.
|
||||
StubStationConfiguration& station()
|
||||
{
|
||||
return static_cast<StubStationConfiguration&>(*cfg.station);
|
||||
}
|
||||
|
||||
std::filesystem::path root;
|
||||
MainConfig cfg;
|
||||
};
|
||||
|
||||
constexpr auto NOW = std::chrono::seconds(1'700'000'000);
|
||||
Clock::time_point at_offset(std::chrono::seconds offset)
|
||||
{
|
||||
return Clock::time_point(NOW + offset);
|
||||
}
|
||||
|
||||
struct ExpiredRecord
|
||||
{
|
||||
std::string label;
|
||||
HashedId8 id;
|
||||
std::string name;
|
||||
Clock::time_point expires_at;
|
||||
std::string pubkey_hex; // empty for skipped entries
|
||||
bool skipped;
|
||||
};
|
||||
|
||||
class RecordingExpiredVisitor : public PruneExpiredVisitor
|
||||
{
|
||||
public:
|
||||
void on_deletable(CertificateStorage&, const std::string& label, const HashedId8& id, const Certificate& c,
|
||||
const std::string& pk) override
|
||||
{
|
||||
records.push_back({ label, id, c.get_name(), c.valid_until(), pk, false });
|
||||
}
|
||||
|
||||
void on_skipped(const std::string& label, const HashedId8& id, const Certificate& c) override
|
||||
{
|
||||
records.push_back({ label, id, c.get_name(), c.valid_until(), {}, true });
|
||||
}
|
||||
|
||||
void on_summary() override
|
||||
{
|
||||
++summary_count;
|
||||
}
|
||||
|
||||
std::vector<ExpiredRecord> records;
|
||||
std::size_t summary_count = 0;
|
||||
};
|
||||
|
||||
class RecordingOrphansVisitor : public PruneOrphansVisitor
|
||||
{
|
||||
public:
|
||||
void on_orphan(const std::string& hex) override
|
||||
{
|
||||
orphans.push_back(hex);
|
||||
}
|
||||
|
||||
void on_summary() override
|
||||
{
|
||||
++summary_count;
|
||||
}
|
||||
|
||||
std::vector<std::string> orphans;
|
||||
std::size_t summary_count = 0;
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
|
||||
using namespace vanetza::pki;
|
||||
|
||||
TEST_F(PruneTestFixture, prune_expired_picks_certs_with_valid_until_in_past)
|
||||
{
|
||||
PublicKey live = make_random_pubkey();
|
||||
PublicKey dead = make_random_pubkey();
|
||||
cfg.enrolment_credentials->store(build_stub_certificate(live, nullptr, at_offset(std::chrono::seconds(0)),
|
||||
std::chrono::hours(1)));
|
||||
cfg.enrolment_credentials->store(build_stub_certificate(dead, nullptr, at_offset(std::chrono::seconds(-86400)),
|
||||
std::chrono::hours(1)));
|
||||
|
||||
RecordingExpiredVisitor v;
|
||||
prune_expired(cfg, Clock::time_point(NOW), false, v);
|
||||
|
||||
ASSERT_EQ(1u, v.records.size());
|
||||
EXPECT_FALSE(v.records[0].skipped);
|
||||
EXPECT_EQ("EC", v.records[0].label);
|
||||
EXPECT_EQ(vanetza::security::canonical_hexstring(dead), v.records[0].pubkey_hex);
|
||||
EXPECT_EQ(1u, v.summary_count);
|
||||
}
|
||||
|
||||
TEST_F(PruneTestFixture, prune_expired_distinguishes_store_labels)
|
||||
{
|
||||
PublicKey expired_ec = make_random_pubkey();
|
||||
PublicKey expired_at = make_random_pubkey();
|
||||
cfg.enrolment_credentials->store(build_stub_certificate(expired_ec, nullptr,
|
||||
at_offset(std::chrono::seconds(-86400)), std::chrono::hours(1)));
|
||||
cfg.tickets->store(build_stub_certificate(expired_at, nullptr, at_offset(std::chrono::seconds(-86400)),
|
||||
std::chrono::hours(1)));
|
||||
|
||||
RecordingExpiredVisitor v;
|
||||
prune_expired(cfg, Clock::time_point(NOW), false, v);
|
||||
|
||||
ASSERT_EQ(2u, v.records.size());
|
||||
std::unordered_set<std::string> labels;
|
||||
for (const auto& r : v.records) {
|
||||
labels.insert(r.label);
|
||||
}
|
||||
EXPECT_EQ(2u, labels.size());
|
||||
EXPECT_TRUE(labels.count("EC"));
|
||||
EXPECT_TRUE(labels.count("AT"));
|
||||
}
|
||||
|
||||
TEST_F(PruneTestFixture, prune_expired_skips_station_referenced_without_force)
|
||||
{
|
||||
PublicKey ec_key = make_random_pubkey();
|
||||
auto ec = build_stub_certificate(ec_key, nullptr, at_offset(std::chrono::seconds(-86400)), std::chrono::hours(1));
|
||||
HashedId8 ec_id = ec.calculate_hashed_id8(*cfg.security);
|
||||
cfg.enrolment_credentials->store(ec);
|
||||
station().set_ec_identifier(ec_id);
|
||||
|
||||
RecordingExpiredVisitor v;
|
||||
prune_expired(cfg, Clock::time_point(NOW), false, v);
|
||||
|
||||
ASSERT_EQ(1u, v.records.size());
|
||||
EXPECT_TRUE(v.records[0].skipped);
|
||||
EXPECT_EQ(ec_id, v.records[0].id);
|
||||
}
|
||||
|
||||
TEST_F(PruneTestFixture, prune_expired_force_removes_station_referenced)
|
||||
{
|
||||
PublicKey ec_key = make_random_pubkey();
|
||||
auto ec = build_stub_certificate(ec_key, nullptr, at_offset(std::chrono::seconds(-86400)), std::chrono::hours(1));
|
||||
HashedId8 ec_id = ec.calculate_hashed_id8(*cfg.security);
|
||||
cfg.enrolment_credentials->store(ec);
|
||||
station().set_ec_identifier(ec_id);
|
||||
|
||||
RecordingExpiredVisitor v;
|
||||
prune_expired(cfg, Clock::time_point(NOW), false, v); // without force: skipped
|
||||
|
||||
ASSERT_EQ(1u, v.records.size());
|
||||
EXPECT_TRUE(v.records[0].skipped);
|
||||
|
||||
RecordingExpiredVisitor v2;
|
||||
prune_expired(cfg, Clock::time_point(NOW), true, v2); // with force: deletable
|
||||
ASSERT_EQ(1u, v2.records.size());
|
||||
EXPECT_FALSE(v2.records[0].skipped);
|
||||
EXPECT_EQ(ec_id, v2.records[0].id);
|
||||
}
|
||||
|
||||
TEST_F(PruneTestFixture, prune_expired_keeps_not_yet_valid_certs)
|
||||
{
|
||||
PublicKey future = make_random_pubkey();
|
||||
cfg.enrolment_credentials->store(build_stub_certificate(future, nullptr, at_offset(std::chrono::seconds(86400)),
|
||||
std::chrono::hours(1)));
|
||||
|
||||
RecordingExpiredVisitor v;
|
||||
prune_expired(cfg, Clock::time_point(NOW), false, v);
|
||||
|
||||
EXPECT_TRUE(v.records.empty());
|
||||
EXPECT_EQ(1u, v.summary_count);
|
||||
}
|
||||
|
||||
TEST_F(PruneTestFixture, prune_orphans_emits_only_unreferenced_credentials)
|
||||
{
|
||||
PublicKey ec_key = make_random_pubkey();
|
||||
PublicKey orphan_key = make_random_pubkey();
|
||||
|
||||
// EC cert references ec_key; orphan_key has no cert
|
||||
cfg.enrolment_credentials->store(build_stub_certificate(ec_key));
|
||||
// Both private keys are in credential storage
|
||||
cfg.credentials->store(ec_key, PrivateKey {});
|
||||
cfg.credentials->store(orphan_key, PrivateKey {});
|
||||
|
||||
RecordingOrphansVisitor v;
|
||||
prune_orphans(cfg, v);
|
||||
|
||||
ASSERT_EQ(1u, v.orphans.size());
|
||||
EXPECT_EQ(vanetza::security::canonical_hexstring(orphan_key), v.orphans[0]);
|
||||
EXPECT_EQ(1u, v.summary_count);
|
||||
}
|
||||
|
||||
TEST_F(PruneTestFixture, prune_orphans_empty_when_all_referenced)
|
||||
{
|
||||
PublicKey k = make_random_pubkey();
|
||||
cfg.enrolment_credentials->store(build_stub_certificate(k));
|
||||
cfg.credentials->store(k, PrivateKey {});
|
||||
|
||||
RecordingOrphansVisitor v;
|
||||
prune_orphans(cfg, v);
|
||||
|
||||
EXPECT_TRUE(v.orphans.empty());
|
||||
EXPECT_EQ(1u, v.summary_count);
|
||||
}
|
||||
|
||||
TEST_F(PruneTestFixture, prune_orphans_handles_empty_stores)
|
||||
{
|
||||
RecordingOrphansVisitor v;
|
||||
prune_orphans(cfg, v);
|
||||
|
||||
EXPECT_TRUE(v.orphans.empty());
|
||||
EXPECT_EQ(1u, v.summary_count);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using vanetza::ByteBuffer;
|
||||
using namespace vanetza::pki;
|
||||
|
||||
/**
|
||||
* Test vectors from IEEE 1609.2-2017 (Annex D)
|
||||
*/
|
||||
|
||||
class SecurityModuleTest : public ::testing::Test
|
||||
{
|
||||
public:
|
||||
void SetUp() override
|
||||
{
|
||||
credentials = std::make_shared<MockCredentialStorage>();
|
||||
security = std::make_shared<OpenSslSecurityModule>(credentials);
|
||||
}
|
||||
|
||||
protected:
|
||||
std::shared_ptr<CredentialStorage> credentials;
|
||||
std::shared_ptr<SecurityModule> security;
|
||||
};
|
||||
|
||||
TEST_F(SecurityModuleTest, mac_vector1)
|
||||
{
|
||||
const ByteBuffer key(20, 0x0B);
|
||||
const ByteBuffer msg { 0x48, 0x69, 0x20, 0x54, 0x68, 0x65, 0x72, 0x65 };
|
||||
const ByteBuffer tag {
|
||||
0xB0, 0x34, 0x4C, 0x61, 0xD8, 0xDB, 0x38, 0x53,
|
||||
0x5C, 0xA8, 0xAF, 0xCE, 0xAF, 0x0B, 0xF1, 0x2B
|
||||
};
|
||||
EXPECT_EQ(tag, security->calculate_hmac_sha256(key, msg));
|
||||
}
|
||||
|
||||
TEST_F(SecurityModuleTest, mac_vector2)
|
||||
{
|
||||
const ByteBuffer key { 0x4A, 0x65, 0x66, 0x65 };
|
||||
const ByteBuffer msg {
|
||||
0x77, 0x68, 0x61, 0x74, 0x20, 0x64, 0x6F, 0x20,
|
||||
0x79, 0x61, 0x20, 0x77, 0x61, 0x6E, 0x74, 0x20,
|
||||
0x66, 0x6F, 0x72, 0x20, 0x6E, 0x6F, 0x74, 0x68,
|
||||
0x69, 0x6E, 0x67, 0x3F };
|
||||
const ByteBuffer tag {
|
||||
0x5B, 0xDC, 0xC1, 0x46, 0xBF, 0x60, 0x75, 0x4E,
|
||||
0x6A, 0x04, 0x24, 0x26, 0x08, 0x95, 0x75, 0xC7
|
||||
};
|
||||
EXPECT_EQ(tag, security->calculate_hmac_sha256(key, msg));
|
||||
}
|
||||
|
||||
TEST_F(SecurityModuleTest, mac_vector3)
|
||||
{
|
||||
const ByteBuffer key(20, 0xAA);
|
||||
const ByteBuffer msg(50, 0xDD);
|
||||
const ByteBuffer tag {
|
||||
0x77, 0x3E, 0xA9, 0x1E, 0x36, 0x80, 0x0E, 0x46,
|
||||
0x85, 0x4D, 0xB8, 0xEB, 0xD0, 0x91, 0x81, 0xA7
|
||||
};
|
||||
EXPECT_EQ(tag, security->calculate_hmac_sha256(key, msg));
|
||||
}
|
||||
|
||||
TEST_F(SecurityModuleTest, mac_vector4)
|
||||
{
|
||||
const ByteBuffer key {
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
|
||||
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19 };
|
||||
const ByteBuffer msg(50, 0xCD);
|
||||
const ByteBuffer tag {
|
||||
0x82, 0x55, 0x8A, 0x38, 0x9A, 0x44, 0x3C, 0x0E,
|
||||
0xA4, 0xCC, 0x81, 0x98, 0x99, 0xF2, 0x08, 0x3A
|
||||
};
|
||||
EXPECT_EQ(tag, security->calculate_hmac_sha256(key, msg));
|
||||
}
|
||||
|
||||
TEST_F(SecurityModuleTest, mac_vector5)
|
||||
{
|
||||
const ByteBuffer key(20, 0x0C);
|
||||
const ByteBuffer msg {
|
||||
0x54, 0x65, 0x73, 0x74, 0x20, 0x57, 0x69, 0x74,
|
||||
0x68, 0x20, 0x54, 0x72, 0x75, 0x6E, 0x63, 0x61,
|
||||
0x74, 0x69, 0x6F, 0x6E };
|
||||
const ByteBuffer tag {
|
||||
0xA3, 0xB6, 0x16, 0x74, 0x73, 0x10, 0x0E, 0xE0,
|
||||
0x6E, 0x0C, 0x79, 0x6C, 0x29, 0x55, 0x55, 0x2B
|
||||
};
|
||||
EXPECT_EQ(tag, security->calculate_hmac_sha256(key, msg));
|
||||
}
|
||||
|
||||
TEST_F(SecurityModuleTest, mac_vector6)
|
||||
{
|
||||
const ByteBuffer key(131, 0xAA);
|
||||
const ByteBuffer msg {
|
||||
0x54, 0x65, 0x73, 0x74, 0x20, 0x55, 0x73, 0x69,
|
||||
0x6E, 0x67, 0x20, 0x4C, 0x61, 0x72, 0x67, 0x65,
|
||||
0x72, 0x20, 0x54, 0x68, 0x61, 0x6E, 0x20, 0x42,
|
||||
0x6C, 0x6F, 0x63, 0x6B, 0x2D, 0x53, 0x69, 0x7A,
|
||||
0x65, 0x20, 0x4B, 0x65, 0x79, 0x20, 0x2D, 0x20,
|
||||
0x48, 0x61, 0x73, 0x68, 0x20, 0x4B, 0x65, 0x79,
|
||||
0x20, 0x46, 0x69, 0x72, 0x73, 0x74 };
|
||||
const ByteBuffer tag {
|
||||
0x60, 0xE4, 0x31, 0x59, 0x1E, 0xE0, 0xB6, 0x7F,
|
||||
0x0D, 0x8A, 0x26, 0xAA, 0xCB, 0xF5, 0xB7, 0x7F
|
||||
};
|
||||
EXPECT_EQ(tag, security->calculate_hmac_sha256(key, msg));
|
||||
}
|
||||
|
||||
TEST_F(SecurityModuleTest, mac_vector7)
|
||||
{
|
||||
const ByteBuffer key(131, 0xAA);
|
||||
const ByteBuffer msg {
|
||||
0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20,
|
||||
0x61, 0x20, 0x74, 0x65, 0x73, 0x74, 0x20, 0x75,
|
||||
0x73, 0x69, 0x6E, 0x67, 0x20, 0x61, 0x20, 0x6C,
|
||||
0x61, 0x72, 0x67, 0x65, 0x72, 0x20, 0x74, 0x68,
|
||||
0x61, 0x6E, 0x20, 0x62, 0x6C, 0x6F, 0x63, 0x6B,
|
||||
0x2D, 0x73, 0x69, 0x7A, 0x65, 0x20, 0x6B, 0x65,
|
||||
0x79, 0x20, 0x61, 0x6E, 0x64, 0x20, 0x61, 0x20,
|
||||
0x6C, 0x61, 0x72, 0x67, 0x65, 0x72, 0x20, 0x74,
|
||||
0x68, 0x61, 0x6E, 0x20, 0x62, 0x6C, 0x6F, 0x63,
|
||||
0x6B, 0x2D, 0x73, 0x69, 0x7A, 0x65, 0x20, 0x64,
|
||||
0x61, 0x74, 0x61, 0x2E, 0x20, 0x54, 0x68, 0x65,
|
||||
0x20, 0x6B, 0x65, 0x79, 0x20, 0x6E, 0x65, 0x65,
|
||||
0x64, 0x73, 0x20, 0x74, 0x6F, 0x20, 0x62, 0x65,
|
||||
0x20, 0x68, 0x61, 0x73, 0x68, 0x65, 0x64, 0x20,
|
||||
0x62, 0x65, 0x66, 0x6F, 0x72, 0x65, 0x20, 0x62,
|
||||
0x65, 0x69, 0x6E, 0x67, 0x20, 0x75, 0x73, 0x65,
|
||||
0x64, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x65,
|
||||
0x20, 0x48, 0x4D, 0x41, 0x43, 0x20, 0x61, 0x6C,
|
||||
0x67, 0x6F, 0x72, 0x69, 0x74, 0x68, 0x6D, 0x2E };
|
||||
const ByteBuffer tag {
|
||||
0x9B, 0x09, 0xFF, 0xA7, 0x1B, 0x94, 0x2F, 0xCB,
|
||||
0x27, 0x63, 0x5F, 0xBC, 0xD5, 0xB0, 0xE9, 0x44
|
||||
};
|
||||
EXPECT_EQ(tag, security->calculate_hmac_sha256(key, msg));
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#include "signed_builder.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include "stub_certificate.hpp"
|
||||
#include "validation.hpp"
|
||||
#include <vanetza/asn1/security/SignedData.h>
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class SignedBuilderTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
SignedBuilderTest() : m_credentials(std::make_shared<MockCredentialStorage>()), m_security(m_credentials)
|
||||
{
|
||||
}
|
||||
|
||||
std::shared_ptr<MockCredentialStorage> m_credentials;
|
||||
OpenSslSecurityModule m_security;
|
||||
};
|
||||
|
||||
// create_external_signed produces an EtsiTs103097Data-SignedExternalPayload
|
||||
// envelope whose SignedData:
|
||||
// - tbsData.payload.extDataHash is set (not data)
|
||||
// - extDataHash carries SHA-256(external_payload) when hash_algo is SHA256
|
||||
// - signer = digest(cert) when a cert is provided
|
||||
// - signature verifies against the cert's verification key
|
||||
TEST_F(SignedBuilderTest, external_signed_carries_payload_hash_and_verifies)
|
||||
{
|
||||
PublicKey ec_key = m_security.create_key(KeyType::NistP256);
|
||||
Certificate ec_cert = build_stub_certificate(ec_key);
|
||||
ByteBuffer payload { 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE };
|
||||
|
||||
SignedData envelope = create_external_signed(payload, m_security, ec_key, HashAlgorithm::SHA256, &ec_cert);
|
||||
ASSERT_EQ(Vanetza_Security_Ieee1609Dot2Content_PR_signedData, envelope->content->present);
|
||||
Vanetza_Security_SignedData_t* sd = envelope->content->choice.signedData;
|
||||
ASSERT_NE(nullptr, sd);
|
||||
|
||||
// payload.extDataHash present, payload.data absent
|
||||
ASSERT_NE(nullptr, sd->tbsData);
|
||||
ASSERT_NE(nullptr, sd->tbsData->payload);
|
||||
ASSERT_NE(nullptr, sd->tbsData->payload->extDataHash);
|
||||
EXPECT_EQ(nullptr, sd->tbsData->payload->data);
|
||||
ASSERT_EQ(Vanetza_Security_HashedData_PR_sha256HashedData, sd->tbsData->payload->extDataHash->present);
|
||||
|
||||
// hash equals SHA-256(payload)
|
||||
Sha256Hash expected = m_security.calculate_sha256_hash(payload.data(), payload.size());
|
||||
const auto& hash_oct = sd->tbsData->payload->extDataHash->choice.sha256HashedData;
|
||||
ASSERT_EQ(32, hash_oct.size);
|
||||
EXPECT_EQ(0, std::memcmp(hash_oct.buf, expected.octets.data(), 32));
|
||||
|
||||
// signer = digest, matches HashedId8(ec_cert)
|
||||
ASSERT_EQ(Vanetza_Security_SignerIdentifier_PR_digest, sd->signer.present);
|
||||
HashedId8 ec_hid8 = ec_cert.calculate_hashed_id8(m_security);
|
||||
EXPECT_TRUE(sd->signer.choice.digest == ec_hid8.octets);
|
||||
|
||||
// signature verifies against ec_cert's verification key
|
||||
Sha256Hash digest = calculate_digest<Sha256Hash>(m_security, *sd->tbsData, &ec_cert.raw());
|
||||
EXPECT_TRUE(m_security.verify(digest, make_signature(sd->signature), ec_key));
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "keys.hpp"
|
||||
#include <vanetza/asn1/asn1c_wrapper.hpp>
|
||||
#include <vanetza/asn1/security/EtsiTs103097Certificate.h>
|
||||
#include <vanetza/common/clock.hpp>
|
||||
#include <vanetza/security/v3/basic_elements.hpp>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
// Minimal certificate built around a given verification key (and optional
|
||||
// encryption key). Not TS-compliant (no issuer chain, no permissions, no
|
||||
// signature) but just enough that get_public_key/get_encryption_key return our
|
||||
// keys and the cert OER-encodes so calculate_hashed_id8 is stable. Intended
|
||||
// for unit tests where the cert only needs to act as a key holder + digest
|
||||
// source.
|
||||
inline Certificate build_stub_certificate(const PublicKey& verify_key, const PublicKey* encryption_key = nullptr,
|
||||
Clock::time_point valid_since = Clock::time_point {}, std::chrono::hours validity = std::chrono::hours(24))
|
||||
{
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs103097Certificate_t>
|
||||
cert(asn_DEF_Vanetza_Security_EtsiTs103097Certificate);
|
||||
cert->version = 3;
|
||||
cert->type = Vanetza_Security_CertificateType_explicit;
|
||||
cert->issuer.present = Vanetza_Security_IssuerIdentifier_PR_self;
|
||||
cert->issuer.choice.self = Vanetza_Security_HashAlgorithm_sha256;
|
||||
|
||||
auto& tbs = cert->toBeSigned;
|
||||
tbs.id.present = Vanetza_Security_CertificateId_PR_name;
|
||||
OCTET_STRING_fromBuf(&tbs.id.choice.name, "test-cert", 9);
|
||||
tbs.cracaId.buf = static_cast<uint8_t*>(std::calloc(3, 1));
|
||||
tbs.cracaId.size = 3;
|
||||
tbs.crlSeries = 0;
|
||||
tbs.validityPeriod.start = security::v3::convert_time32(valid_since);
|
||||
tbs.validityPeriod.duration.present = Vanetza_Security_Duration_PR_hours;
|
||||
tbs.validityPeriod.duration.choice.hours = validity.count();
|
||||
tbs.verifyKeyIndicator.present = Vanetza_Security_VerificationKeyIndicator_PR_verificationKey;
|
||||
set_verification_key(tbs.verifyKeyIndicator.choice.verificationKey, verify_key);
|
||||
|
||||
if (encryption_key) {
|
||||
tbs.encryptionKey = asn1::allocate<Vanetza_Security_PublicEncryptionKey_t>();
|
||||
set_encryption_key(*tbs.encryptionKey, *encryption_key);
|
||||
}
|
||||
|
||||
Certificate result;
|
||||
ByteBuffer encoded = cert.encode();
|
||||
result.decode(encoded);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include "station_config.hpp"
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
// In-memory station configuration for unit tests.
|
||||
class StubStationConfiguration : public StationConfiguration
|
||||
{
|
||||
public:
|
||||
std::string get_canonical_identifier() const override
|
||||
{
|
||||
return m_id;
|
||||
}
|
||||
|
||||
void set_canonical_identifier(const std::string& s) override
|
||||
{
|
||||
m_id = s;
|
||||
}
|
||||
|
||||
boost::optional<HashedId8> get_ec_identifier() const override
|
||||
{
|
||||
return m_ec;
|
||||
}
|
||||
|
||||
void set_ec_identifier(const HashedId8& id) override
|
||||
{
|
||||
m_ec = id;
|
||||
}
|
||||
|
||||
boost::optional<HashedId8> get_root_ca() const override
|
||||
{
|
||||
return m_root_ca;
|
||||
}
|
||||
|
||||
void set_root_ca(const HashedId8& id) override
|
||||
{
|
||||
m_root_ca = id;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string m_id;
|
||||
boost::optional<HashedId8> m_ec;
|
||||
boost::optional<HashedId8> m_root_ca;
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,106 @@
|
||||
#include "time.hpp"
|
||||
#include <vanetza/common/clock.hpp>
|
||||
#include <boost/date_time/posix_time/posix_time_io.hpp>
|
||||
#include <boost/optional/optional_io.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace vanetza::pki;
|
||||
|
||||
TEST(Time, utc_tai_offset)
|
||||
{
|
||||
using boost::gregorian::date;
|
||||
using boost::posix_time::ptime;
|
||||
using boost::posix_time::seconds;
|
||||
using boost::posix_time::time_duration;
|
||||
|
||||
EXPECT_EQ(37, utc_tai_offset(ptime { date { 2021, 4, 30 } }).seconds());
|
||||
EXPECT_EQ(10, utc_tai_offset(ptime { date { 1972, 6, 30 } }).seconds());
|
||||
EXPECT_EQ(11, utc_tai_offset(ptime { date { 1972, 7, 1 } }).seconds());
|
||||
|
||||
// Last regular UTC second of the leap day still uses the old offset.
|
||||
EXPECT_EQ(10, utc_tai_offset(ptime { date { 1972, 6, 30 }, time_duration { 23, 59, 59 } }).seconds());
|
||||
// Offset at the ITS epoch must be 32 for current_time() to be correct.
|
||||
EXPECT_EQ(32, utc_tai_offset(vanetza::Clock::epoch()).seconds());
|
||||
}
|
||||
|
||||
TEST(Time, parse_duration_hours_units)
|
||||
{
|
||||
using std::chrono::hours;
|
||||
auto h = [](const char* s) {
|
||||
auto r = parse_duration_hours(s);
|
||||
EXPECT_TRUE(r) << "expected to parse '" << s << "'";
|
||||
return r ? r->count() : -1;
|
||||
};
|
||||
|
||||
EXPECT_EQ(7, h("7h"));
|
||||
EXPECT_EQ(168, h("168h"));
|
||||
EXPECT_EQ(24, h("1d"));
|
||||
EXPECT_EQ(168, h("1w"));
|
||||
EXPECT_EQ(336, h("2w"));
|
||||
|
||||
// m/s collapse to whole hours by truncation
|
||||
EXPECT_EQ(2, h("120m"));
|
||||
EXPECT_EQ(0, h("59m"));
|
||||
EXPECT_EQ(1, h("3600s"));
|
||||
EXPECT_EQ(0, h("0w"));
|
||||
}
|
||||
|
||||
TEST(Time, parse_duration_hours_rejects_garbage)
|
||||
{
|
||||
EXPECT_FALSE(parse_duration_hours(""));
|
||||
EXPECT_FALSE(parse_duration_hours("h")); // no number
|
||||
EXPECT_FALSE(parse_duration_hours("7")); // no unit
|
||||
EXPECT_FALSE(parse_duration_hours("7x")); // bad unit
|
||||
EXPECT_FALSE(parse_duration_hours("abc"));
|
||||
EXPECT_FALSE(parse_duration_hours("-1h")); // negative
|
||||
EXPECT_FALSE(parse_duration_hours("7xyzh")); // trailing garbage between number and unit
|
||||
}
|
||||
|
||||
TEST(Time, parse_validity_start_relative_uses_now_plus_offset)
|
||||
{
|
||||
auto now = current_time();
|
||||
|
||||
// +Nh/d/w forms are now() + N
|
||||
auto in_one_week = parse_validity_start("+1w");
|
||||
ASSERT_TRUE(in_one_week);
|
||||
auto delta_h = std::chrono::duration_cast<std::chrono::hours>(*in_one_week - now);
|
||||
// Allow a small skew because current_time() is sampled twice (here vs inside parse).
|
||||
EXPECT_NEAR(168, delta_h.count(), 1);
|
||||
|
||||
auto in_24h = parse_validity_start("+24h");
|
||||
ASSERT_TRUE(in_24h);
|
||||
EXPECT_NEAR(24, std::chrono::duration_cast<std::chrono::hours>(*in_24h - now).count(), 1);
|
||||
|
||||
auto in_3d = parse_validity_start("+3d");
|
||||
ASSERT_TRUE(in_3d);
|
||||
EXPECT_NEAR(72, std::chrono::duration_cast<std::chrono::hours>(*in_3d - now).count(), 1);
|
||||
}
|
||||
|
||||
TEST(Time, parse_validity_start_absolute_iso_extended)
|
||||
{
|
||||
using boost::gregorian::date;
|
||||
using boost::posix_time::ptime;
|
||||
using boost::posix_time::time_duration;
|
||||
|
||||
// Date-only form pins to 00:00:00 UTC.
|
||||
auto t0 = parse_validity_start("2026-05-02");
|
||||
ASSERT_TRUE(t0);
|
||||
EXPECT_EQ(vanetza::Clock::at(ptime { date { 2026, 5, 2 } }), *t0);
|
||||
|
||||
// ISO-extended form (T separator) and space form both accepted.
|
||||
auto t1 = parse_validity_start("2026-05-02T12:34:56");
|
||||
ASSERT_TRUE(t1);
|
||||
EXPECT_EQ(vanetza::Clock::at(ptime { date { 2026, 5, 2 }, time_duration { 12, 34, 56 } }), *t1);
|
||||
|
||||
auto t2 = parse_validity_start("2026-05-02 12:34:56");
|
||||
ASSERT_TRUE(t2);
|
||||
EXPECT_EQ(*t1, *t2);
|
||||
}
|
||||
|
||||
TEST(Time, parse_validity_start_rejects_garbage)
|
||||
{
|
||||
EXPECT_FALSE(parse_validity_start(""));
|
||||
EXPECT_FALSE(parse_validity_start("not-a-date"));
|
||||
EXPECT_FALSE(parse_validity_start("+xyz")); // bad relative
|
||||
EXPECT_FALSE(parse_validity_start("2026-13-01")); // bad month
|
||||
}
|
||||
Reference in New Issue
Block a user