#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 #include #include #include #include #include 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()), 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 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 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(), 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(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(*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(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(*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 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(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(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(*outer->content->choice.signedData); const auto& pop_sd = *mgmt->content.choice.enrolmentRequest.content->choice.signedData; asn1::asn1c_oer_wrapper 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(*outer->content->choice.signedData); const auto& pop_sd = *mgmt->content.choice.enrolmentRequest.content->choice.signedData; asn1::asn1c_oer_wrapper 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(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(m_security, *outer_sd.tbsData, nullptr), make_signature(outer_sd.signature), params.outer_signer_key)); MgmtData mgmt = decode_unsecured(outer_sd); const auto& pop_sd = *mgmt->content.choice.enrolmentRequest.content->choice.signedData; EXPECT_TRUE(m_security.verify(calculate_digest(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(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(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