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:
Ashin Walpola
2026-09-23 17:46:40 +02:00
parent 2f60623e18
commit 0e9525162d
9881 changed files with 1582523 additions and 17 deletions
@@ -0,0 +1,240 @@
cmake_minimum_required(VERSION 3.22)
get_filename_component(VIDF_ROOT "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE)
set(VIDF_PORT "${CMAKE_CURRENT_LIST_DIR}")
include("${CMAKE_CURRENT_LIST_DIR}/sources.cmake")
if(ESP_PLATFORM)
# IDF dependency discovery evaluates this file before Kconfig is available.
idf_component_register(SRCS "${VIDF_PORT}/src/access.cpp"
INCLUDE_DIRS "${VIDF_PORT}/include" "${VIDF_ROOT}" "${VIDF_ROOT}/vanetza/asn1/support"
REQUIRES esp-boost mbedtls
PRIV_REQUIRES esp_wifi esp_phy esp_event nvs_flash)
if(CMAKE_BUILD_EARLY_EXPANSION)
return()
endif()
if(NOT CONFIG_COMPILER_CXX_EXCEPTIONS OR NOT CONFIG_COMPILER_CXX_RTTI)
message(FATAL_ERROR "vanetza-idf requires CONFIG_COMPILER_CXX_EXCEPTIONS=y and CONFIG_COMPILER_CXX_RTTI=y")
endif()
set(VIDF_TARGET ${COMPONENT_LIB})
set(VIDF_NETWORK ${CONFIG_VANETZA_IDF_NETWORK})
set(VIDF_CAM ${CONFIG_VANETZA_IDF_CAM})
set(VIDF_DENM ${CONFIG_VANETZA_IDF_DENM})
set(VIDF_VAM ${CONFIG_VANETZA_IDF_VAM})
set(VIDF_HIL ${CONFIG_VANETZA_IDF_HIL})
set(VIDF_SECURITY ${CONFIG_VANETZA_IDF_SECURITY})
set(VIDF_SECURITY_VERIFY ${CONFIG_VANETZA_IDF_SECURITY_VERIFY})
set(VIDF_PKI ${CONFIG_VANETZA_IDF_PKI})
set(VIDF_NVS_CREDENTIALS ${CONFIG_VANETZA_IDF_NVS_CREDENTIALS})
else()
project(vanetza_idf LANGUAGES C CXX)
option(VIDF_NETWORK "GeoNetworking router, BTP and DCC" ON)
option(VIDF_CAM "Release 2 CAM codec/endpoint" ON)
option(VIDF_DENM "Release 2 DENM codec/endpoint" ON)
option(VIDF_VAM "Release 2 VAM codec/endpoint" ON)
option(VIDF_HIL "Hardware independent upper/lower tester boundary" ON)
option(VIDF_TESTS "Build port regression tests" ON)
option(VIDF_SECURITY "TS 103 097 signing security entity and TS 102 723-8 identifier-change service" ON)
option(VIDF_SECURITY_VERIFY "SN-DECAP verification of received secured packets (IEEE 1609.2 clause 5.2, TS 103 097 clause 7.1)" ON)
option(VIDF_PKI "TS 102 941 enrolment/authorization request-response core" OFF)
set(VIDF_MBEDTLS_ROOT "" CACHE PATH
"Host only: mbedTLS (>= 3.6 or 4.x/TF-PSA-Crypto) source tree; builds the PSA backend for testing against OpenSSL")
add_library(vanetza_idf STATIC src/access.cpp)
add_library(Vanetza::idf ALIAS vanetza_idf)
set(VIDF_TARGET vanetza_idf)
target_include_directories(${VIDF_TARGET} PUBLIC include "${VIDF_ROOT}" "${VIDF_ROOT}/vanetza/asn1/support")
if(VIDF_BOOST_ROOT)
target_include_directories(${VIDF_TARGET} SYSTEM PUBLIC "${VIDF_BOOST_ROOT}")
if(WIN32)
target_compile_definitions(${VIDF_TARGET} PUBLIC BOOST_NO_USER_CONFIG
BOOST_PLATFORM_CONFIG="boost/config/platform/win32.hpp")
endif()
else()
find_package(Boost 1.70 REQUIRED)
target_include_directories(${VIDF_TARGET} SYSTEM PUBLIC ${Boost_INCLUDE_DIRS})
endif()
endif()
target_compile_features(${VIDF_TARGET} PUBLIC cxx_std_17)
set_target_properties(${VIDF_TARGET} PROPERTIES C_STANDARD 11 CXX_STANDARD 17 CXX_STANDARD_REQUIRED YES)
if(VIDF_SECURITY AND NOT VIDF_NETWORK)
message(FATAL_ERROR "The security entity serves the GeoNetworking layer: VIDF_SECURITY requires VIDF_NETWORK")
endif()
if(VIDF_SECURITY_VERIFY AND NOT VIDF_SECURITY)
message(FATAL_ERROR "VIDF_SECURITY_VERIFY requires VIDF_SECURITY")
endif()
if(VIDF_PKI AND NOT VIDF_SECURITY)
message(FATAL_ERROR "VIDF_PKI requires VIDF_SECURITY")
endif()
target_compile_definitions(${VIDF_TARGET} PUBLIC
VIDF_NETWORK=$<BOOL:${VIDF_NETWORK}> VIDF_CAM=$<BOOL:${VIDF_CAM}>
VIDF_DENM=$<BOOL:${VIDF_DENM}> VIDF_VAM=$<BOOL:${VIDF_VAM}> VIDF_HIL=$<BOOL:${VIDF_HIL}>
VIDF_SECURITY=$<BOOL:${VIDF_SECURITY}> VIDF_SECURITY_VERIFY=$<BOOL:${VIDF_SECURITY_VERIFY}>
VIDF_PKI=$<BOOL:${VIDF_PKI}>)
set(_sources ${VIDF_BASE_SOURCES} "${VIDF_PORT}/src/its_g5_frame.cpp")
if(VIDF_NETWORK)
list(APPEND _sources ${VIDF_NETWORK_SOURCES} ${VIDF_SECURITY_ASN_SOURCES} "${VIDF_PORT}/src/stack.cpp"
"${VIDF_PORT}/src/management.cpp" "${VIDF_PORT}/src/sha.cpp")
if(NOT ESP_PLATFORM)
find_package(OpenSSL REQUIRED COMPONENTS Crypto)
target_link_libraries(${VIDF_TARGET} PRIVATE OpenSSL::Crypto)
endif()
endif()
if(VIDF_NETWORK OR VIDF_CAM OR VIDF_DENM OR VIDF_VAM)
list(APPEND _sources ${VIDF_ASN_SUPPORT_SOURCES})
endif()
foreach(_service CAM DENM VAM)
if(VIDF_${_service})
list(APPEND _sources ${VIDF_${_service}_SOURCES})
endif()
endforeach()
if(VIDF_CAM OR VIDF_DENM OR VIDF_VAM)
list(APPEND _sources "${VIDF_PORT}/src/facilities.cpp")
endif()
if(VIDF_HIL)
list(APPEND _sources "${VIDF_PORT}/src/hil.cpp")
endif()
if(VIDF_SECURITY)
list(APPEND _sources ${VIDF_SECURITY_PROVIDER_SOURCES} "${VIDF_PORT}/src/ecc.cpp"
"${VIDF_PORT}/src/security.cpp" "${VIDF_PORT}/src/identity_manager.cpp"
"${VIDF_PORT}/src/sign_header_policy.cpp" "${VIDF_PORT}/src/credentials.cpp")
if(ESP_PLATFORM)
list(APPEND _sources "${VIDF_PORT}/src/backend_mbedtls.cpp")
target_compile_definitions(${VIDF_TARGET} PUBLIC VIDF_BACKEND_MBEDTLS=1)
if(VIDF_NVS_CREDENTIALS)
# CredentialStore on nvs_flash (credentials.hpp); the application initialises NVS
list(APPEND _sources "${VIDF_PORT}/src/nvs_credential_store.cpp")
target_compile_definitions(${VIDF_TARGET} PUBLIC VIDF_NVS_CREDENTIALS=1)
endif()
else()
# persistence.hpp declares the OpenSSL loaders under this define: keep it PUBLIC.
list(APPEND _sources ${VIDF_SECURITY_HOST_SOURCES})
target_compile_definitions(${VIDF_TARGET} PUBLIC VANETZA_WITH_OPENSSL VIDF_BACKEND_OPENSSL=1)
target_compile_definitions(${VIDF_TARGET} PRIVATE OPENSSL_API_COMPAT=0x10101000L)
if(VIDF_MBEDTLS_ROOT)
set(ENABLE_PROGRAMS OFF CACHE BOOL "" FORCE)
set(ENABLE_TESTING OFF CACHE BOOL "" FORCE)
set(MBEDTLS_FATAL_WARNINGS OFF CACHE BOOL "" FORCE)
set(GEN_FILES OFF CACHE BOOL "" FORCE)
# ESP-IDF's mbedTLS fork includes "mbedtls/bignum.h"/"mbedtls/ecp.h" wrappers that only
# its own port directory provides (they wrap the TF-PSA-Crypto private headers and add
# hardware-acceleration hooks). Recreate the plain wrappers so the same tree builds here.
set(_vidf_mbedtls_shim "${CMAKE_BINARY_DIR}/mbedtls-shim/mbedtls")
foreach(_hdr bignum ecp)
if(EXISTS "${VIDF_MBEDTLS_ROOT}/tf-psa-crypto/drivers/builtin/include/mbedtls/private/${_hdr}.h"
AND NOT EXISTS "${VIDF_MBEDTLS_ROOT}/include/mbedtls/${_hdr}.h")
file(WRITE "${_vidf_mbedtls_shim}/${_hdr}.h"
"#pragma once
#define MBEDTLS_DECLARE_PRIVATE_IDENTIFIERS
#include \"mbedtls/private/${_hdr}.h\"
")
endif()
endforeach()
if(EXISTS "${_vidf_mbedtls_shim}")
file(WRITE "${CMAKE_BINARY_DIR}/mbedtls-shim/sdkconfig.h" "#pragma once
")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I${CMAKE_BINARY_DIR}/mbedtls-shim")
endif()
add_subdirectory("${VIDF_MBEDTLS_ROOT}" "${CMAKE_BINARY_DIR}/mbedtls" EXCLUDE_FROM_ALL)
if(TARGET tfpsacrypto)
set(_vidf_psa tfpsacrypto) # mbedTLS 4.x
else()
set(_vidf_psa mbedcrypto) # mbedTLS 3.x
endif()
list(APPEND _sources "${VIDF_PORT}/src/backend_mbedtls.cpp")
target_link_libraries(${VIDF_TARGET} PUBLIC ${_vidf_psa})
target_compile_definitions(${VIDF_TARGET} PUBLIC VIDF_BACKEND_MBEDTLS=1)
endif()
endif()
if(VIDF_PKI)
list(APPEND _sources ${VIDF_PKI_ASN_SOURCES} "${VIDF_PORT}/src/pki.cpp")
if(ESP_PLATFORM OR VIDF_MBEDTLS_ROOT)
list(APPEND _sources "${VIDF_PORT}/src/ecies_mbedtls.cpp")
endif()
if(NOT ESP_PLATFORM)
list(APPEND _sources "${VIDF_PORT}/src/ecies_openssl.cpp")
endif()
endif()
endif()
list(REMOVE_DUPLICATES _sources)
target_sources(${VIDF_TARGET} PRIVATE ${_sources})
if(ESP_PLATFORM AND CONFIG_VANETZA_IDF_RADIO_C5)
include("${VIDF_PORT}/radio_c5.cmake")
endif()
if(ESP_PLATFORM AND CONFIG_IDF_TARGET_ESP32C5)
# ROM UART0 clock repair for warm resets (see the source); kept by an undefined-symbol reference.
target_sources(${VIDF_TARGET} PRIVATE "${VIDF_PORT}/src/esp32c5_rom_uart_clock.c")
target_link_libraries(${VIDF_TARGET} INTERFACE "-u vidf_esp32c5_rom_uart_clock_link")
endif()
if(VIDF_NETWORK)
foreach(_module circular_buffer geometry heap rational multiprecision)
if(NOT EXISTS "${VIDF_PORT}/third_party/${_module}/include/boost")
message(FATAL_ERROR "Missing Boost.${_module}; run git submodule update --init --recursive")
endif()
target_include_directories(${VIDF_TARGET} SYSTEM PUBLIC "${VIDF_PORT}/third_party/${_module}/include")
endforeach()
# Last-resort fallback headers for older system Boost releases (see
# docs/idf/test-campaigns.md); searched after every real Boost include root.
target_include_directories(${VIDF_TARGET} SYSTEM PUBLIC "${VIDF_PORT}/compat/include")
endif()
# Iostreams stream devices are header-only here; esp-boost omits these headers.
if(VIDF_IOSTREAMS_ROOT)
target_include_directories(${VIDF_TARGET} SYSTEM PUBLIC "${VIDF_IOSTREAMS_ROOT}")
elseif(EXISTS "${CMAKE_CURRENT_LIST_DIR}/third_party/iostreams/include/boost/iostreams/stream.hpp")
target_include_directories(${VIDF_TARGET} SYSTEM PUBLIC "${CMAKE_CURRENT_LIST_DIR}/third_party/iostreams/include")
elseif(ESP_PLATFORM)
message(FATAL_ERROR "Missing Boost.Iostreams. Clone with --recurse-submodules or set VIDF_IOSTREAMS_ROOT.")
endif()
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_compile_options(${VIDF_TARGET} PRIVATE -ffunction-sections -fdata-sections
-Wno-error=deprecated-declarations)
if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 15)
target_compile_options(${VIDF_TARGET} PRIVATE -Wno-error=deprecated-variadic-comma-omission)
endif()
endif()
if(ESP_PLATFORM)
target_compile_definitions(${VIDF_TARGET} PUBLIC HAVE_NETINET_IN_H)
# The generated IOC table intentionally zero-initializes trailing fields.
set_source_files_properties("${VIDF_ROOT}/vanetza/asn1/its/r2/WrappedExtensionContainer.c"
PROPERTIES COMPILE_OPTIONS -Wno-error=missing-field-initializers)
# ESP-IDF 6 uses picolibc's native timegm, without a tm_gmtoff member.
# Do not confuse the missing struct member with timezone-mutating emulation.
if(IDF_VERSION_MAJOR GREATER_EQUAL 6)
set_source_files_properties("${VIDF_ROOT}/vanetza/asn1/support/GeneralizedTime.c"
PROPERTIES COMPILE_DEFINITIONS HAVE_TIMEGM)
endif()
target_compile_options(${VIDF_TARGET} PRIVATE
$<$<COMPILE_LANGUAGE:C>:-Wno-error=unused-function>
$<$<COMPILE_LANGUAGE:C>:-Wno-error=maybe-uninitialized>)
elseif(VIDF_TESTS)
enable_testing()
add_executable(vidf_tests tests/test_main.cpp tests/test_its_time.cpp)
if(VIDF_NETWORK)
target_sources(vidf_tests PRIVATE tests/test_management.cpp tests/test_dcc.cpp tests/test_dcc_net.cpp)
endif()
if(VIDF_SECURITY)
target_sources(vidf_tests PRIVATE tests/test_security.cpp tests/test_backend_kat.cpp
tests/test_security_entity.cpp tests/test_trust_domain.cpp tests/test_credentials.cpp)
if(VIDF_PKI)
target_sources(vidf_tests PRIVATE tests/test_pki.cpp tests/pki_authority.cpp)
endif()
target_link_libraries(vidf_tests PRIVATE OpenSSL::Crypto)
endif()
target_link_libraries(vidf_tests PRIVATE ${VIDF_TARGET})
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_link_options(vidf_tests PRIVATE -Wl,--gc-sections)
endif()
add_test(NAME vidf_regression COMMAND vidf_tests)
if(VIDF_NETWORK)
add_executable(vidf_sut tests/hil_sut.cpp tests/hil_stdio.cpp)
target_link_libraries(vidf_sut PRIVATE ${VIDF_TARGET})
endif()
if(VIDF_SECURITY)
# Isolated test trust domain in the ETSI framework's certificate pool layout (not a PKI).
add_executable(vidf_test_pool tests/trust_domain_pool.cpp tests/test_trust_domain.cpp)
target_link_libraries(vidf_test_pool PRIVATE ${VIDF_TARGET})
# Lab issuing under an existing (project) root: reads PEM keys through OpenSSL, host only.
add_executable(vidf_issue tests/issue_tool.cpp tests/test_trust_domain.cpp tests/pki_authority.cpp)
target_link_libraries(vidf_issue PRIVATE ${VIDF_TARGET} OpenSSL::Crypto)
target_compile_definitions(vidf_issue PRIVATE OPENSSL_API_COMPAT=0x10101000L)
endif()
endif()
@@ -0,0 +1,88 @@
menu "Vanetza-IDF"
choice VANETZA_IDF_PROFILE
prompt "Stack entry point"
default VANETZA_IDF_PROFILE_NETWORK
config VANETZA_IDF_PROFILE_ACCESS
bool "Access: AL_DATA / IN-SAP"
config VANETZA_IDF_PROFILE_NETWORK
bool "Network: BTP-DATA / NF-SAP"
config VANETZA_IDF_PROFILE_FACILITIES
bool "Facilities endpoints (CAM, DENM, VAM)"
endchoice
config VANETZA_IDF_NETWORK
bool
default y if !VANETZA_IDF_PROFILE_ACCESS
config VANETZA_IDF_CAM
bool "Release 2 CAM codec and endpoint"
default y if VANETZA_IDF_PROFILE_FACILITIES
depends on VANETZA_IDF_NETWORK
config VANETZA_IDF_DENM
bool "Release 2 DENM codec and endpoint"
default y if VANETZA_IDF_PROFILE_FACILITIES
depends on VANETZA_IDF_NETWORK
config VANETZA_IDF_VAM
bool "Release 2 VAM codec and endpoint"
default y if VANETZA_IDF_PROFILE_FACILITIES
depends on VANETZA_IDF_NETWORK
config VANETZA_IDF_SECURITY
bool "TS 103 097 signing security entity and identifier-change service"
default y
depends on VANETZA_IDF_NETWORK
help
SN-SAP security entity: signs outgoing GeoNetworking packets with
authorization tickets the application provisions (TS 103 097
V2.2.1, IEEE Std 1609.2) on the mbedTLS PSA backend and runs the
TS 102 723-8 clause 6.3 identifier-change two-phase commit that
the GN core, access adapter and facilities subscribe to. Without
it a stack with itsGnSecurity enabled fails closed.
config VANETZA_IDF_SECURITY_VERIFY
bool "SN-DECAP verification of received secured packets"
default y
depends on VANETZA_IDF_SECURITY
help
Verify received EtsiTs103097Data-Signed packets (IEEE 1609.2
clause 5.2 with the TS 103 097 clause 7.1 profile checks, the
certificate chain to the provisioned trust anchors, generation
time plausibility and replay detection). Without it SN-DECAP
reports Configuration_Problem and a strict GeoNetworking receiver
drops every secured packet (docs/idf/conformance.md GAP-SEC-001).
config VANETZA_IDF_NVS_CREDENTIALS
bool "Credential store on NVS"
default y
depends on VANETZA_IDF_SECURITY
help
NvsCredentialStore keeps the station's provisioned credentials
(root and CA certificates, authorization tickets with their
private keys, credentials.hpp bundle) as one blob in the
nvs_flash component's storage. The application initialises NVS
and decides on NVS encryption; without this option the
application supplies its own CredentialStore.
config VANETZA_IDF_PKI
bool "TS 102 941 enrolment and authorization request-response core"
default n
depends on VANETZA_IDF_SECURITY
help
Builds and parses EtsiTs102941Data enrolment/authorization
requests and responses (TS 102 941 V2.2.1 clause 6.2.3). The
transport to the EA/AA and credential storage are supplied by the
application.
config VANETZA_IDF_HIL
bool "Upper/lower tester hooks"
default n
help
Builds test control without enabling a UART, USB, BLE or network
transport. Wire transports explicitly in the application.
choice VANETZA_IDF_RADIO
prompt "Access adapter"
default VANETZA_IDF_RADIO_EXTERNAL
config VANETZA_IDF_RADIO_EXTERNAL
bool "Application-provided access adapter"
config VANETZA_IDF_RADIO_C5
bool "Experimental integrated ESP32-C5 ITS-G5 radio"
depends on IDF_TARGET_ESP32C5
help
Uses OpenTrafficMap's private driver transmit path. Only the
pinned ESP-IDF version is accepted. Hardware/RF and DCC
qualification remain separate acceptance requirements.
endchoice
endmenu
@@ -0,0 +1,27 @@
// Minimal compatibility shim: newer Boost.Geometry (karney_inverse.hpp) calls
// boost::core::invoke_swap(), which apt's libboost-dev 1.83 core headers do
// not yet provide (only the older boost::swap() in <boost/core/swap.hpp>).
// This is intentionally NOT a copy of upstream Boost's invoke_swap.hpp: that
// file redefines the boost_swap_impl::is_const helper that <boost/core/swap.hpp>
// already defines, which collides when both headers end up in one translation
// unit (as happens here, since vanetza/common/position_fix.hpp pulls in
// <boost/optional.hpp> -> <boost/core/swap.hpp> ahead of this one). Building
// invoke_swap on top of the already-available boost::swap() avoids that clash.
#ifndef BOOST_CORE_INVOKE_SWAP_HPP
#define BOOST_CORE_INVOKE_SWAP_HPP
#include <boost/core/swap.hpp>
namespace boost {
namespace core {
template<class T>
inline void invoke_swap(T& left, T& right)
{
boost::swap(left, right);
}
} // namespace core
} // namespace boost
#endif // BOOST_CORE_INVOKE_SWAP_HPP
@@ -0,0 +1,123 @@
#pragma once
#include <vanetza/common/byte_buffer.hpp>
#include <vanetza/net/mac_address.hpp>
#include <cstdint>
#include <memory>
#include <optional>
#if VIDF_NETWORK
#include <vanetza/common/runtime.hpp>
#include <vanetza/dcc/channel_load.hpp>
#endif
namespace vanetza_idf {
// Implementation result, NOT an additional ETSI AL-DATA.confirm primitive.
enum class Result { accepted, invalid_argument, unsupported, wrong_entry_point,
security_unavailable, resource_limit, rejected, time_regression,
identity_change_pending /*< TS 102 723-8 clause 6.3.1.3: between PREPARE and COMMIT */ };
// Named PHY modes avoid confusing legacy OFDM coding schemes with the SDK's
// rate indices or HT/NGV MCS indices. EN 303 797 clauses 4.3.1 and 4.3.2.
enum class OfdmMcs : std::uint8_t {
bpsk_1_2, bpsk_3_4, qpsk_1_2, qpsk_3_4,
qam16_1_2, qam16_3_4, qam64_2_3, qam64_3_4
};
/** Technology-specific AL_DATA.request: EN 303 797 V2.1.1, Annex B.2.
* See docs/idf/standards.md (IF-IN-001). This is the AL_DATA binding,
* not the separate IN-UNITDATA interface of TS 102 723-10.
* This C++ binding is local: the standard does not prescribe a C++/BLE ABI.
* Owns the GNPDU bytes; they exclude LLC, MAC and PHY headers.
*/
struct AlDataRequest {
vanetza::MacAddress source;
vanetza::MacAddress destination;
std::uint8_t priority = 0; // IEEE 802.1D user priority 0..7, not EDCA ordinal
double transmit_power_dbm = 0.0;
OfdmMcs mcs = OfdmMcs::qpsk_1_2; // 6 Mbit/s at 10 MHz
std::uint16_t bandwidth_mhz = 10;
std::uint16_t channel_number = 180;
std::uint8_t transceiver_id = 0;
std::optional<std::uint8_t> transceiver_mode;
std::optional<std::uint32_t> datastream_id;
vanetza::ByteBuffer data;
};
/** AL_DATA.indication, EN 303 797 V2.1.1 Annex B.2 (IF-IN-002).
* Optional radio measurements are absent when the backend cannot measure them;
* zero must never stand for a made-up measurement.
*/
struct AlDataIndication {
vanetza::MacAddress source;
vanetza::MacAddress destination;
std::optional<double> channel_busy_ratio;
std::optional<double> received_power_dbm;
std::uint16_t channel_number = 180;
std::uint8_t receiver_id = 0;
std::optional<std::uint8_t> receiver_mode;
vanetza::ByteBuffer data;
};
/** Device-independent access adapter. Backend must reject unsupported controls,
* apply DCC when allocated to access, and report reception via Stack::indicate.
* request takes ownership even on failure. No callback from an interrupt.
*/
class Access {
public:
virtual Result request(AlDataRequest) = 0;
virtual ~Access() = default;
};
Result validate(const AlDataRequest&, std::size_t maximum_gnpdu);
/** Access profile has no BTP/GN state and preserves the AL_DATA parameters.
*
* On a VIDF_NETWORK build, optionally runs DCC_ACC (TS 102 687 V1.2.1 clause 5.4 Adaptive
* approach, gated by the Annex B budget/gate-keeper) in front of the backend, per
* SYS-DCC-001/GAP-DCC-001. Disabled by default so existing callers (host tests, other
* applications) are unaffected; the ESP32-C5 firmware enables it once it has a real CBR
* source (SYS-DCC-002). The DCC methods below do not exist on an access-only build (no
* VIDF_NETWORK): the Adaptive approach lives in vanetza/dcc, which that configuration does
* not compile, matching the documented access-only deployment (docs/idf/conformance.md).
*/
class AccessStack {
public:
explicit AccessStack(Access& access, std::size_t maximum_gnpdu = 4096);
~AccessStack();
AccessStack(const AccessStack&) = delete;
AccessStack& operator=(const AccessStack&) = delete;
#if VIDF_NETWORK
/** Enable the Adaptive DCC_ACC gate. cbr_target defaults to TS 103 836-4-2's
* itsGNCBRTarget = 0.62, the value the project's SYS-DCC-001/002/003 use consistently
* (not TS 102 687 Table 3's older, superseded 0.68 default).
* runtime must outlive this AccessStack and must be the same Runtime driving the caller's
* periodic advance()/trigger() (e.g. the Stack's ManualRuntime), since the Adaptive approach
* reschedules itself every 200 ms of that runtime's own clock.
* Call once, before the first request().
*/
void enable_dcc(vanetza::Runtime& runtime, vanetza::dcc::ChannelLoad cbr_target = vanetza::dcc::ChannelLoad(0.62));
/** Feed the latest channel-busy-ratio measurement at roughly the T_Cbr cadence (100 ms):
* local LCBR (SYS-DCC-002), or CBR_G when a Release-2 DCC_NET has one available
* (SYS-DCC-001: "consume Release-2 CBR_G when available, otherwise LCBR"). No-op unless
* enable_dcc was called.
*/
void report_channel_load(vanetza::dcc::ChannelLoad);
/** Permitted duty cycle (delta) from the last periodic Adaptive-approach update.
* Returns UnitInterval(1.0) (unrestricted) when DCC is not enabled.
*/
vanetza::UnitInterval permitted_duty_cycle() const;
#endif
Result request(AlDataRequest);
private:
#if VIDF_NETWORK
class Dcc;
std::unique_ptr<Dcc> dcc_;
#endif
Access& access_;
std::size_t maximum_gnpdu_;
};
}
@@ -0,0 +1,60 @@
#pragma once
#include <vanetza/security/backend.hpp>
#include <memory>
namespace vanetza_idf {
/** vanetza::security::Backend on the PSA Crypto API of mbedTLS.
*
* ESP-IDF 6.0.2 ships mbedTLS 4.1.0 (TF-PSA-Crypto), where the classic
* ecp/ecdsa/bignum headers are private; the PSA API (psa/crypto.h) is the only
* public cryptography interface and is also present in mbedTLS 3.6 (ESP-IDF 5.x).
*
* Algorithms are those TS 103 097 V2.2.1 clause 5.2 selects through IEEE Std
* 1609.2: ECDSA over NIST P-256, brainpoolP256r1 (SHA-256) and brainpoolP384r1
* (SHA-384), clauses 5.3.1, 5.3.3, 6.3.38/6.3.39. ECDSA is randomized
* (PSA_ALG_ECDSA(SHA-256/SHA-384) over the caller-supplied digest of that
* length, which lets ESP-IDF's PSA driver use the ESP32-C5 ECDSA peripheral for
* verification; IEEE Std 1609.2 does not require RFC 6979). Compressed points
* are recovered by vanetza_idf::ecc, since PSA imports Weierstrass public keys
* only in SEC 1 uncompressed form.
*
* Private keys are imported as volatile PSA keys the first time they are used
* and kept in a small bounded cache until the backend is destroyed. Every
* failure raises std::runtime_error or returns false; nothing is faked.
*/
class BackendMbedTls : public vanetza::security::Backend {
public:
static constexpr auto backend_name = "mbedTLS-PSA";
BackendMbedTls();
~BackendMbedTls() override;
BackendMbedTls(const BackendMbedTls&) = delete;
BackendMbedTls& operator=(const BackendMbedTls&) = delete;
/// Legacy NIST P-256 + SHA-256 signature of data (TS 103 097 v1.x, IEEE 1609.2 5.3.1)
vanetza::security::EcdsaSignature sign_data(const vanetza::security::ecdsa256::PrivateKey&,
const vanetza::ByteBuffer& data) override;
/// ECDSA over a precomputed digest; curve follows the key type
vanetza::security::Signature sign_digest(const vanetza::security::PrivateKey&,
const vanetza::ByteBuffer& digest) override;
bool verify_data(const vanetza::security::ecdsa256::PublicKey&, const vanetza::ByteBuffer& data,
const vanetza::security::EcdsaSignature&) override;
bool verify_digest(const vanetza::security::PublicKey&, const vanetza::ByteBuffer& digest,
const vanetza::security::Signature&) override;
/// NIST P-256 only, matching the upstream OpenSSL backend (EccPoint carries no curve)
boost::optional<vanetza::security::Uncompressed> decompress_point(const vanetza::security::EccPoint&) override;
vanetza::ByteBuffer calculate_hash(vanetza::security::HashAlgorithm, const vanetza::ByteBuffer&) override;
/// Fresh NIST P-256 key pair from the PSA random generator
vanetza::security::ecdsa256::KeyPair generate_key_pair() override;
/** Maximum number of imported private keys kept resident (default 4:
* the current and next authorization tickets plus enrolment material). */
void set_key_cache_size(std::size_t);
private:
class Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace vanetza_idf
@@ -0,0 +1,45 @@
#pragma once
#include <sdkconfig.h>
#if !defined(CONFIG_IDF_TARGET_ESP32C5) || !defined(CONFIG_VANETZA_IDF_RADIO_C5)
#error "C5Radio requires ESP32-C5 and CONFIG_VANETZA_IDF_RADIO_C5"
#endif
#include <vanetza_idf/access.hpp>
#include <esp_err.h>
#include <functional>
#include <memory>
namespace vanetza_idf {
struct C5RadioConfig {
std::uint16_t channel_number = 180;
double transmit_power_dbm = 10.0;
unsigned receive_queue_length = 8;
// Explicit laboratory mode until real CBR/DCC enforcement is integrated.
// The default supports a receive-only second C5 without any packet TX.
bool laboratory_transmission = false;
};
/** ITS-G5 AL_DATA device binding, EN 303 797 Annex B.2.
* This adapter owns the Wi-Fi driver while started. Invoke start/stop/request/
* poll in one application task. Radio callbacks only copy into a bounded queue.
* No MQTT, Ethernet, storage, facilities service or test runtime is included.
*/
class C5Radio final : public Access {
public:
using Receive = std::function<void(AlDataIndication)>;
using Capture = std::function<void(const vanetza::ByteBuffer&, int, std::uint32_t)>;
explicit C5Radio(C5RadioConfig = {});
~C5Radio();
C5Radio(const C5Radio&) = delete;
C5Radio& operator=(const C5Radio&) = delete;
esp_err_t start();
void stop();
Result request(AlDataRequest) override;
// Dispatch reception in the owning task. Capture contains the original
// MPDU including FCS, before LLC/MAC removal; timestamp is radio-local us.
void poll(const Receive&, const Capture& = {});
std::uint32_t dropped_frames() const;
private:
class Impl;
std::unique_ptr<Impl> impl_;
};
}
@@ -0,0 +1,79 @@
#pragma once
#include <vanetza_idf/security.hpp>
#include <string>
#include <vector>
/** A station's provisioned credentials and how they are kept.
*
* TS 102 940 V2.1.1 clause 6 (trust model: root CA, EA/AA, tickets) and TS 102 941
* V2.2.1 (credential life cycle) leave the obtaining and keeping of certificates to
* the station; TS 102 723-8 has no primitive for it. The security entity takes
* them as a TrustConfiguration and a CertificatePool (security.hpp). This header
* fixes the octets in between: what a provisioning path of the application
* (a file, a serial link, a wireless link, a TS 102 941 client) hands over and
* what a storage keeps across resets. Certificates are COER EtsiTs103097Certificate
* values (TS 103 097 V2.2.1 clause 6), ticket keys the raw private scalar of the
* verification key (IEEE Std 1609.2 clause 6.4.36 curves), nothing else.
*
* Bundle format (this library's definition, version 1): the ASCII magic "VCR1"
* followed by records [type: 1 octet][length: 2 octets, big-endian][payload].
* Types: 1 root CA certificate, 2 subordinate CA certificate (AA/EA), 3 ticket
* certificate, 4 ticket private key ([curve: 1 octet, 1 = NIST P-256, 2 =
* brainpoolP256r1, 3 = brainpoolP384r1][scalar]) which follows its ticket
* certificate. Decoding fails closed on any deviation. Keys travel and rest in
* the clear: a transport must be a trusted one and a storage the application's
* encrypted one (ESP-IDF NVS encryption for NvsCredentialStore).
*/
namespace vanetza_idf::security {
struct Credentials {
struct Ticket {
ByteBuffer certificate; // COER, TS 103 097 clause 7.2.1 profile
PrivateKey key; // private scalar of verifyKeyIndicator
};
std::vector<ByteBuffer> roots; // clause 7.2.3, self-signed
std::vector<ByteBuffer> authorities; // clause 7.2.4, issued by a root or another CA
std::vector<Ticket> tickets; // clause 7.2.1, with keys
bool empty() const { return roots.empty() && authorities.empty() && tickets.empty(); }
};
/// bundle octets of the credentials (format above)
ByteBuffer encode(const Credentials&);
/// parse a bundle; false (and an untouched output) when the octets are not a well-formed bundle
bool decode(const ByteBuffer& bundle, Credentials& out);
/** Result of feeding credentials into the trust configuration and the pool: how many
* of each were accepted and the first refusal (Result::accepted when everything was). */
struct ApplyReport {
std::size_t roots = 0, authorities = 0, tickets = 0;
Result result = Result::accepted;
};
/** Add every root, then every authority, then every ticket, each through the checks of
* TrustConfiguration::add_* and CertificatePool::add (TS 103 097 profiles, key/certificate
* match); stops at the first refusal so that a broken item is not silently skipped. */
ApplyReport apply(const Credentials&, TrustConfiguration&, CertificatePool&);
/** Storage the application chooses: keeps one bundle. load() returns Result::rejected
* when nothing is stored and Result::invalid_argument when the stored octets do not
* decode; save() replaces what was there; erase() removes it. */
class CredentialStore {
public:
virtual ~CredentialStore() = default;
virtual Result save(const Credentials&) = 0;
virtual Result load(Credentials&) = 0;
virtual Result erase() = 0;
};
/// a bundle in one file (hosts, or an ESP-IDF VFS the application mounted)
class FileCredentialStore final : public CredentialStore {
public:
explicit FileCredentialStore(std::string path) : path_(std::move(path)) {}
Result save(const Credentials&) override;
Result load(Credentials&) override;
Result erase() override;
private:
std::string path_;
};
} // namespace vanetza_idf::security
@@ -0,0 +1,39 @@
#pragma once
#include <vanetza/common/byte_buffer.hpp>
#include <vanetza/security/ecc_point.hpp>
#include <vanetza/security/key_type.hpp>
#include <boost/optional/optional.hpp>
#include <cstddef>
namespace vanetza_idf::ecc {
/** Short Weierstrass domain parameters y^2 = x^3 + a*x + b (mod p), big-endian octets.
* NIST P-256 (FIPS 186-4 D.1.2.3), brainpoolP256r1 and brainpoolP384r1 (RFC 5639
* clause 3.4/3.6): the three curves TS 103 097 V2.2.1 clause 6 admits for
* verification keys (IEEE Std 1609.2 clause 6.4.36 PublicVerificationKey).
* All three primes satisfy p = 3 (mod 4).
*/
struct CurveParameters {
vanetza::security::KeyType type;
std::size_t octets; // field element length
const char* p;
const char* a;
const char* b;
};
/** Domain parameters for a key type, nullptr for KeyType::Unspecified. */
const CurveParameters* curve(vanetza::security::KeyType);
/** Recover an affine point from its x coordinate and the parity of y.
* IEEE Std 1609.2 clause 6.3.23 EccP256CurvePoint / 6.3.24 EccP384CurvePoint
* compressed-y-0 / compressed-y-1 forms (SEC 1 clause 2.3.4). Pure integer
* arithmetic (Boost.Multiprecision), so it is independent of the crypto backend.
* \return uncompressed point, or none if x has the wrong length or is not on the curve
*/
boost::optional<vanetza::security::Uncompressed>
decompress(vanetza::security::KeyType, const vanetza::ByteBuffer& x, bool y_odd);
/** SEC 1 clause 2.3.3 octet-string point encoding 0x04 || X || Y. */
vanetza::ByteBuffer encode_uncompressed(const vanetza::security::Uncompressed&);
} // namespace vanetza_idf::ecc
@@ -0,0 +1,23 @@
#pragma once
#include <vanetza_idf/pki.hpp>
namespace vanetza_idf::pki {
/** EciesBackend on the PSA Crypto API of mbedTLS (see backend_mbedtls.hpp for the
* version rationale): psa_raw_key_agreement(PSA_ALG_ECDH) for the shared x
* coordinate, psa_mac_compute(PSA_ALG_HMAC(SHA-256)), psa_generate_random and
* psa_aead_encrypt/decrypt(PSA_ALG_CCM) with a 16-octet tag. */
class EciesMbedTls : public EciesBackend {
public:
EciesMbedTls(); // psa_crypto_init, throws on failure
KeyPair generate_key(KeyType) override;
std::optional<ByteBuffer> ecdh_x(const PrivateKey& own, const PublicKey& peer) override;
ByteBuffer hmac_sha256(const ByteBuffer& key, const ByteBuffer& data) override;
ByteBuffer random(std::size_t octets) override;
bool aes_ccm_encrypt(const std::array<std::uint8_t, 16>& key, const std::array<std::uint8_t, 12>& nonce,
const ByteBuffer& plaintext, ByteBuffer& ciphertext_and_tag) override;
bool aes_ccm_decrypt(const std::array<std::uint8_t, 16>& key, const std::array<std::uint8_t, 12>& nonce,
const ByteBuffer& ciphertext_and_tag, ByteBuffer& plaintext) override;
};
} // namespace vanetza_idf::pki
@@ -0,0 +1,19 @@
#pragma once
#include <vanetza_idf/pki.hpp>
namespace vanetza_idf::pki {
/// EciesBackend on OpenSSL (host builds only): ECDH via EC_KEY, HMAC-SHA256, RAND_bytes, EVP AES-128-CCM.
class EciesOpenSsl : public EciesBackend {
public:
KeyPair generate_key(KeyType) override;
std::optional<ByteBuffer> ecdh_x(const PrivateKey& own, const PublicKey& peer) override;
ByteBuffer hmac_sha256(const ByteBuffer& key, const ByteBuffer& data) override;
ByteBuffer random(std::size_t octets) override;
bool aes_ccm_encrypt(const std::array<std::uint8_t, 16>& key, const std::array<std::uint8_t, 12>& nonce,
const ByteBuffer& plaintext, ByteBuffer& ciphertext_and_tag) override;
bool aes_ccm_decrypt(const std::array<std::uint8_t, 16>& key, const std::array<std::uint8_t, 12>& nonce,
const ByteBuffer& ciphertext_and_tag, ByteBuffer& plaintext) override;
};
} // namespace vanetza_idf::pki
@@ -0,0 +1,80 @@
#pragma once
#include <vanetza_idf/access.hpp>
#include <vanetza/asn1/asn1c_wrapper.hpp>
#include <vanetza/asn1/support/uper_decoder.h>
#if VIDF_CAM
#include <vanetza/asn1/its/r2/CAM.h>
#endif
#if VIDF_DENM
#include <vanetza/asn1/its/r2/DENM.h>
#endif
#if VIDF_VAM
#include <vanetza/asn1/its/r2/VAM.h>
#endif
namespace vanetza_idf::facilities {
template<class T>
struct CompletePerMessage : vanetza::asn1::asn1c_per_wrapper<T> {
using vanetza::asn1::asn1c_per_wrapper<T>::asn1c_per_wrapper;
bool decode_exact(const vanetza::ByteBuffer& bytes) {
if (bytes.empty()) return false;
// X.691 complete encoding: consume the PDU and only its zero padding.
// Comparing a re-encoding would incorrectly reject unknown extensions
// which an extensible ASN.1 decoder is permitted to skip.
void* decoded = nullptr;
asn_codec_ctx_t context{};
context.max_stack_size = 16384;
const auto result = uper_decode(&context, &this->m_type, &decoded,
bytes.data(), bytes.size(), 0, 0);
const bool complete = result.code == RC_OK && result.consumed > 0 &&
(result.consumed + 7) / 8 == bytes.size();
const unsigned padding = complete ? (8 - result.consumed % 8) % 8 : 0;
if (!complete || (bytes.back() & ((1u << padding) - 1u))) {
vanetza::asn1::free(this->m_type, decoded);
return false;
}
vanetza::asn1::free(this->m_type, this->m_struct);
this->m_struct = static_cast<T*>(decoded);
return true;
}
};
// Explicit R2 aliases avoid Vanetza's backwards-compatible R1 Cam/Denm aliases.
// IF-FAC-001: TS 103 900 V2.3.1, TS 103 831 V2.3.1,
// TS 103 300-3 V2.3.1 ASN.1; CDD TS 102 894-2 V2.4.1.
#if VIDF_CAM
struct Cam : CompletePerMessage<Vanetza_ITS2_CAM_t> {
Cam() : CompletePerMessage(asn_DEF_Vanetza_ITS2_CAM) {}
};
#endif
#if VIDF_DENM
struct Denm : CompletePerMessage<Vanetza_ITS2_DENM_t> {
Denm() : CompletePerMessage(asn_DEF_Vanetza_ITS2_DENM) {}
};
#endif
#if VIDF_VAM
struct Vam : CompletePerMessage<Vanetza_ITS2_VAM_t> {
Vam() : CompletePerMessage(asn_DEF_Vanetza_ITS2_VAM) {}
};
#endif
enum class Kind { cam, denm, vam };
struct Descriptor { std::uint16_t port; unsigned message_id; unsigned protocol_version; };
Descriptor descriptor(Kind);
/** Decode + constraints + identity + exact UPER consumption.
* This boundary validates an encoded PDU. It is NOT a complete CA, DEN or VRU
* Basic Service: generation rules/lifecycle/SSP decisions belong to that service.
*/
Result validate_pdu(Kind, const vanetza::ByteBuffer&, std::size_t maximum = 1394);
/** Application owns generation and lifecycle; this function selects the standard
* BTP port and validates the Release 2 PDU before entering the NF-SAP.
*/
#if VIDF_NETWORK
}
#include <vanetza_idf/stack.hpp>
namespace vanetza_idf::facilities {
Result send(Stack&, Kind, vanetza::ByteBuffer, BtpRequest transport);
#endif
}
@@ -0,0 +1,33 @@
#pragma once
#include <vanetza_idf/access.hpp>
#include <functional>
#include <optional>
namespace vanetza_idf::hil {
/** Project test transport, NOT an ETSI on-air protocol or TTCN verdict.
* VID1 | channel:u8 | sequence:u32be | length:u16be | payload | CRC32:u32be.
* Payload on upper channel is unchanged ETSI adapter UT codec data.
* Wire decoding is bounded and tolerates fragmentation and corrupt frames.
*/
enum class Channel : std::uint8_t { upper = 1, lower = 2, diagnostic = 3 };
struct Frame { Channel channel; std::uint32_t sequence; vanetza::ByteBuffer payload; };
vanetza::ByteBuffer encode(const Frame&, std::size_t maximum = 4096);
class Decoder {
public:
using Handler = std::function<void(Frame)>;
explicit Decoder(std::size_t maximum = 4096);
void feed(const std::uint8_t*, std::size_t, const Handler&);
std::size_t buffered() const { return buffer_.size(); }
private:
std::size_t maximum_;
vanetza::ByteBuffer buffer_;
void process(const Handler&);
};
/** Actual SUT hooks: unknown/unimplemented commands have no successful reply.
* A handler may return no response for unsupported commands; the TTCN testcase
* then times out or receives its specified negative result from the handler.
* No ACK is fabricated by the framing/transport code.
*/
using UpperTester = std::function<std::optional<vanetza::ByteBuffer>(const vanetza::ByteBuffer&)>;
}
@@ -0,0 +1,87 @@
#pragma once
#include <vanetza_idf/access.hpp>
#include <vanetza/common/byte_buffer.hpp>
#include <vanetza/security/hashed_id.hpp>
#include <cstdint>
#include <functional>
#include <memory>
namespace vanetza_idf::security {
/** Identifier-change service of the security entity.
*
* TS 102 723-8 V2.0.0 clause 5 incorporates V1.1.1: clauses 5.2.5 to 5.2.10
* (SN-IDCHANGE-SUBSCRIBE/-EVENT/-UNSUBSCRIBE/-TRIGGER, SN-ID-LOCK/-UNLOCK) and
* clause 6.3 (ID management: hook function, two-phase commit, lock, trigger).
* TS 102 723-9 V1.1.1 clauses 5.2.5 to 5.2.10 define the identical SF-SAP
* primitives (Tables 10 to 21 carry the same names and ranges), and clause
* 4.1.5 allows one security entity to serve several layers, so one service
* instance is exposed through both sn_sap.hpp and sf_sap.hpp. TS 102 940
* V2.1.1 clause 6.5 requires every layer holding an identifier to subscribe
* and derive its identifier from the HashedId8 given in the event.
*
* A subscriber may live in the same task, another task or process, or another
* device: the hook is an ordinary callback and the response object may be used
* inside the callback or later. The library defines no transport for that.
*/
/// SN-IDCHANGE-EVENT.indication command (TS 102 723-8 V1.1.1 Table 12; clause 6.3.1.2 a-d)
enum class IdChangeCommand : std::uint8_t { PREPARE, COMMIT, ABORT, DEREG };
/// Table 11 / Table 14 subscription: INTEGER 0 to 2^64-1
using SubscriptionHandle = std::uint64_t;
/// Table 19 / Table 20 lock_handle: INTEGER 0 to 2^64-1
using LockHandle = std::uint64_t;
/// Table 12 id: OCTET STRING of 8 octets, the HashedId8 of the authorization ticket (TS 102 940 clause 6.5)
using Identifier = vanetza::security::HashedId8;
/** SN-IDCHANGE-EVENT.response (Table 13 return_code).
* Hand the object back within the hook call for a local subscriber, or keep it
* and respond once the remote party has answered. Responding twice, or after
* the phase finished (timeout, abort, unsubscribe), is ignored.
*/
class IdChangeResponder {
public:
virtual void respond(bool return_code) = 0;
virtual ~IdChangeResponder() = default;
};
/** Hook function (clause 6.3.1.2): command, id to be set and the subscriber_data
* given at subscription (Table 12). For PREPARE and COMMIT the service waits
* for the response; ABORT and DEREG expect no response (responder is null). */
using IdChangeHook = std::function<void(IdChangeCommand, const Identifier&, const vanetza::ByteBuffer& subscriber_data,
std::shared_ptr<IdChangeResponder>)>;
/** The identifier-change service of the security entity (TS 102 723-8 V1.1.1 clause 6.3
* "ID management"): SN-IDCHANGE-SUBSCRIBE/-UNSUBSCRIBE/-TRIGGER and SN-ID-LOCK/-UNLOCK
* (clauses 5.2.5, 5.2.7 to 5.2.10) as virtual calls; the same instance serves the
* SF-SAP (TS 102 723-9 V1.1.1 clauses 5.2.5 to 5.2.10). */
class IdChangeService {
public:
virtual ~IdChangeService() = default;
/// SN-IDCHANGE-SUBSCRIBE.request (Table 10) -> .confirm subscription (Table 11)
virtual SubscriptionHandle subscribe(IdChangeHook idchange_event_hook, vanetza::ByteBuffer subscriber_data = {}) = 0;
/// SN-IDCHANGE-UNSUBSCRIBE.request (Table 14); .confirm carries no parameter (Table 15)
virtual Result unsubscribe(SubscriptionHandle subscription) = 0;
/** SN-IDCHANGE-TRIGGER.request (Table 16). Queues an identifier change; clause 6.3.3 NOTE:
* this does not lead to an immediate change, the two-phase commit is invoked, after any
* lock is released. Result::rejected when no other valid authorization ticket exists. */
virtual Result trigger() = 0;
/// SN-ID-LOCK.request Duration 0..255 seconds (Table 18) -> lock_handle (Table 19); clause 6.3.2
virtual LockHandle lock(std::uint8_t duration_seconds) = 0;
/// SN-ID-UNLOCK.request (Table 20); .confirm carries no parameter (Table 21)
virtual Result unlock(LockHandle lock_handle) = 0;
/// HashedId8 of the authorization ticket currently in use (TS 102 940 clause 6.5)
virtual Identifier current_identifier() const = 0;
/// true while PREPARE has been issued and COMMIT/ABORT is outstanding (clause 6.3.1.3 item 2)
virtual bool change_pending() const = 0;
};
} // namespace vanetza_idf::security
@@ -0,0 +1,16 @@
#pragma once
#include <vanetza_idf/access.hpp>
namespace vanetza_idf::its_g5 {
// IEEE 802.11 QoS Data header (26 octets) and IEEE 802 LLC/SNAP (8 octets).
// EN 303 797 clauses 4.4/4.5 and Annex B.2; GeoNetworking EtherType 0x8947.
constexpr std::size_t maximum_gnpdu = 2296;
Result encode_frame(const AlDataRequest&, std::uint16_t sequence, vanetza::ByteBuffer&);
// Caller declares FCS presence; this function never guesses from payload bytes.
// Receive PHY/FCS status must be checked by the radio before calling this:
// on ESP32-C5, any frame reaching the promiscuous callback has already passed
// a real hardware FCS check (WIFI_PROMIS_FILTER_MASK_FCSFAIL is never set),
// and the 4 trailing bytes this flag strips are not a software-recoverable
// copy of that FCS -- see c5_radio.cpp's receive() for the measured evidence.
Result decode_frame(const std::uint8_t*, std::size_t, bool includes_fcs, AlDataIndication&);
}
@@ -0,0 +1,81 @@
#pragma once
#include <array>
#include <chrono>
#include <cstddef>
#include <cstdint>
/** ITS time base: TAI microseconds since the ITS epoch 2004-01-01T00:00:00.000 UTC.
*
* TS 102 894-2 V2.4.1 DE_TimestampIts: "elapsed milliseconds since the ITS epoch
* ... TAI is a continuous time scale. UTC has discontinuities, as it is occasionally
* adjusted by leap seconds. As of 1 January, 2022, TimestampIts is 5 seconds ahead
* of UTC" (example: 2007-01-01T00:00:00.000Z is 94 694 401 000 ms, one leap second
* since the epoch). IEEE Std 1609.2 Time64 is the same scale in microseconds and
* Time32 in seconds ("the number of (TAI) microseconds/seconds since 00:00:00 UTC,
* 1 January, 2004", Ieee1609Dot2BaseTypes ASN.1 module as published by ETSI);
* TS 103 097 V2.2.1 generationTime and certificate validityPeriod use them. vanetza::Clock::time_point counts these
* microseconds; upstream's Clock::at(posix_time) subtracts the epoch in UTC without
* leap seconds and is therefore 5 s behind since 2017: convert wall-clock time with
* this header.
*
* Leap seconds are announced by the IERS at most six months ahead; the table ends
* with the insertion of 2016-12-31 (Bulletin C 52). It must be extended when a new
* one is announced, the same as any TAI-UTC table (tzdata leap-seconds.list).
*/
namespace vanetza_idf::its_time {
/// Unix time (seconds since 1970-01-01T00:00:00Z, no leap seconds) of the ITS epoch.
constexpr std::int64_t epoch_unix_seconds = 1072915200;
/// Unix instants at which UTC gained a leap second after the ITS epoch: 2006-01-01,
/// 2009-01-01, 2012-07-01, 2015-07-01, 2017-01-01 (each 00:00:00Z, the first second
/// after the inserted 23:59:60). TAI-UTC was 32 s at the epoch and is 37 s since 2017.
constexpr std::array<std::int64_t, 5> leap_second_unix_seconds = {
1136073600, 1230768000, 1341100800, 1435708800, 1483228800};
/// Number of leap seconds inserted between the ITS epoch and the given Unix instant.
constexpr int leap_seconds_since_epoch(std::int64_t unix_seconds) {
int count = 0;
for (auto at : leap_second_unix_seconds) if (unix_seconds >= at) ++count;
return count;
}
/// TAI microseconds since the ITS epoch for a Unix instant given as seconds and an
/// additional sub-second part in microseconds.
constexpr std::int64_t microseconds_since_epoch(std::int64_t unix_seconds, std::int64_t sub_second_microseconds = 0) {
return (unix_seconds - epoch_unix_seconds + leap_seconds_since_epoch(unix_seconds)) * 1000000 + sub_second_microseconds;
}
/// The same for a std::chrono system clock instant (Unix time on every platform this library targets).
inline std::chrono::microseconds since_epoch(std::chrono::system_clock::time_point at) {
// ("unix" itself is a predefined macro on GNU/Linux)
const auto unix_us = std::chrono::duration_cast<std::chrono::microseconds>(at.time_since_epoch()).count();
const std::int64_t seconds = unix_us >= 0 ? unix_us / 1000000 : -((-unix_us + 999999) / 1000000);
return std::chrono::microseconds(microseconds_since_epoch(seconds, unix_us - seconds * 1000000));
}
/// TS 102 894-2 TimestampIts (milliseconds) for a system clock instant.
inline std::uint64_t timestamp_its(std::chrono::system_clock::time_point at) {
return static_cast<std::uint64_t>(since_epoch(at).count() / 1000);
}
/// IEEE Std 1609.2 Time32 (seconds) for a system clock instant.
inline std::uint32_t time32(std::chrono::system_clock::time_point at) {
return static_cast<std::uint32_t>(since_epoch(at).count() / 1000000);
}
/// Inverse: Unix microseconds for TAI microseconds since the ITS epoch. An instant inside an
/// inserted leap second (UTC 23:59:60) has no Unix second of its own and maps onto the
/// following 00:00:00.
constexpr std::int64_t unix_microseconds(std::int64_t microseconds_since_its_epoch) {
const std::int64_t tai_seconds = microseconds_since_its_epoch / 1000000;
int inserted = 0;
for (std::size_t i = 0; i < leap_second_unix_seconds.size(); ++i) {
// TAI value (since the ITS epoch) of the first second after the i-th insertion
const std::int64_t tai_of_leap = leap_second_unix_seconds[i] - epoch_unix_seconds + static_cast<std::int64_t>(i + 1);
if (tai_seconds >= tai_of_leap) ++inserted;
}
return (tai_seconds + epoch_unix_seconds - inserted) * 1000000 + microseconds_since_its_epoch % 1000000;
}
} // namespace vanetza_idf::its_time
@@ -0,0 +1,66 @@
#pragma once
#include <vanetza_idf/access.hpp>
#include <vanetza_idf/mn_sap.hpp>
#include <cstdint>
#include <vector>
/** MF-SAP language binding: management entity -> facilities layer.
*
* - TS 102 723-5 V2.0.0 clause 5 incorporates V1.1.1 altogether, which (like
* TS 102 723-4 for MN) provides MF-COMMAND and MF-REQUEST with the generic
* envelope of TS 102 723-1: entity identifier, CommandRef, command/request
* number and value, ErrStatus in the confirm.
* - TS 103 175 V1.1.1 clause 8.4: MF-SET.request/.confirm (Tables 11/12) with
* the DCC F-Params of Table 13, i.e. the channel-load feedback the DCC
* management entity gives the facilities layer (clause 6.5 DCC_CROSS_Facilities).
* The facilities layer implements FacilitiesParameterSink; it may live in
* another task, process or device, the library defines no transport.
*/
namespace vanetza_idf::MF_SAP {
using MN_SAP::ErrStatus;
/// TS 103 175 V1.1.1 Table 13: F-Param.No for the DCC interface at the MF-SAP
enum class F_Param_No : std::uint8_t {
CHANNEL_NUMBER = 0, // R/W, 1 octet 1..7 (selects the channel for consecutive reads/writes)
AVAILABLE_RESOURCE = 1, // R/W, 2 octets, reciprocal value of CBRa on the selected channel
};
struct F_Param { F_Param_No no; std::uint32_t value; };
struct F_Error { F_Param_No f_param_no; ErrStatus err_status; };
/// Table 11: MF-SET.request
struct MF_SET_request { std::uint32_t fac_id; std::uint8_t command_ref; std::vector<F_Param> f_param; };
/// Table 12: MF-SET.confirm (Errors optional)
struct MF_SET_confirm { std::uint32_t fac_id; std::uint8_t command_ref; std::vector<F_Error> errors; };
/// TS 102 723-1 envelope (through TS 102 723-5): MF-COMMAND.request/.confirm, MF-REQUEST.request/.confirm.
/// Command and request numbers are entity specific (annexes of the respective part); opaque here.
struct MF_COMMAND_request { std::uint32_t fac_id; std::uint8_t command_ref; std::uint32_t command_no; vanetza::ByteBuffer value; };
struct MF_COMMAND_confirm { std::uint32_t fac_id; std::uint8_t command_ref; ErrStatus err_status; };
struct MF_REQUEST_request { std::uint32_t fac_id; std::uint8_t command_ref; std::uint32_t request_no; vanetza::ByteBuffer value; };
struct MF_REQUEST_confirm { std::uint32_t fac_id; std::uint8_t command_ref; ErrStatus err_status; vanetza::ByteBuffer value; };
/** Implemented by the facilities layer (e.g. the message generation services that
* adapt their rate to the available resource). */
class FacilitiesParameterSink {
public:
virtual ~FacilitiesParameterSink() = default;
virtual ErrStatus set(F_Param_No, std::uint32_t value) = 0;
/// MF-COMMAND / MF-REQUEST are delivered unchanged; a sink that knows no such number
/// answers INVALID_COMMAND_REQUEST_NUMBER (TS 102 723-3 clause 5.2.3 wording).
virtual MF_COMMAND_confirm command(const MF_COMMAND_request& request) {
return {request.fac_id, request.command_ref, ErrStatus::INVALID_COMMAND_REQUEST_NUMBER};
}
virtual MF_REQUEST_confirm request(const MF_REQUEST_request& request) {
return {request.fac_id, request.command_ref, ErrStatus::INVALID_COMMAND_REQUEST_NUMBER, {}};
}
};
bool f_param_value_in_format(F_Param_No, std::uint32_t value);
/** MF-SET over a sink; format checked first (Table 13), a null sink answers UNSUPPORTED. */
MF_SET_confirm MF_SET_request_submit(FacilitiesParameterSink*, const MF_SET_request&);
MF_COMMAND_confirm MF_COMMAND_request_submit(FacilitiesParameterSink*, const MF_COMMAND_request&);
MF_REQUEST_confirm MF_REQUEST_request_submit(FacilitiesParameterSink*, const MF_REQUEST_request&);
} // namespace vanetza_idf::MF_SAP
@@ -0,0 +1,56 @@
#pragma once
#include <vanetza_idf/mn_sap.hpp>
#include <cstdint>
#include <vector>
/** MI-SAP language binding: management entity <-> access layer, DCC subset.
*
* - TS 102 723-3 V1.1.1 clauses 7 and 8: MI-SET.request/.confirm (Tables 5/6)
* and MI-GET.request/.confirm (Tables 7/8) with MAC-ID, CommandRef and a
* sequence of I-Param; clause 5.2.3: an undefined command is acknowledged
* with ErrStatus 5 "INVALID COMMAND/REQUEST NUMBER".
* - TS 103 175 V1.1.1 clause 8.2: the DCC I-Params of Table 5 (clauses 6.2 and
* 6.3 DCC_CROSS_Access).
* Binding only: the access adapter of the application may implement
* AccessParameterProvider; the library reads channel load and transmit
* timing from nowhere else and invents no values.
*/
namespace vanetza_idf::MI_SAP {
using MN_SAP::ErrStatus;
/// TS 103 175 V1.1.1 Table 5: I-Param.No for the DCC interface at the MI-SAP
enum class I_Param_No : std::uint8_t {
CHANNEL_NUMBER = 52, // R/W, 1 octet 1..7
LOCAL_CBR = 53, // R, 1 octet 0..100 (CL measurement of TS 102 687)
MESSAGE_LENGTH = 54, // R, 1 octet, granularity 1 OFDM symbol = 8 us (air time Ton)
LAST_TRANSMIT_TIME = 55, // R, 4 octets, granularity 1 OFDM symbol = 8 us
IDLE_TIME = 56, // R/W, 2 octets in ms (Toff)
TX_POWER_LEVEL_LIMIT = 57, // R/W, 1 octet, bits 0..4 EIRP 0..31 dBm, bits 5..7 reserved
};
struct I_Param { I_Param_No no; std::uint32_t value; };
struct I_Error { I_Param_No i_param_no; ErrStatus err_status; };
/// TS 102 723-3 Table 7 / TS 103 175 Table 1: MI-GET.request (MAC-ID structure of TS 102 723-1 reduced to an id)
struct MI_GET_request { std::uint32_t mac_id; std::uint8_t command_ref; std::vector<I_Param_No> i_param_no; };
/// TS 102 723-3 Table 8 / TS 103 175 Table 2: MI-GET.confirm
struct MI_GET_confirm { std::uint32_t mac_id; std::uint8_t command_ref; std::vector<I_Param> i_param; std::vector<I_Error> errors; };
/// TS 102 723-3 Table 5 / TS 103 175 Table 3: MI-SET.request
struct MI_SET_request { std::uint32_t mac_id; std::uint8_t command_ref; std::vector<I_Param> i_param; };
/// TS 102 723-3 Table 6 / TS 103 175 Table 4: MI-SET.confirm (Errors optional)
struct MI_SET_confirm { std::uint32_t mac_id; std::uint8_t command_ref; std::vector<I_Error> errors; };
class AccessParameterProvider {
public:
virtual ~AccessParameterProvider() = default;
virtual ErrStatus get(I_Param_No, std::uint32_t& value) = 0;
virtual ErrStatus set(I_Param_No, std::uint32_t value) = 0;
};
bool i_param_value_in_format(I_Param_No, std::uint32_t value);
bool i_param_writable(I_Param_No);
MI_GET_confirm MI_GET_request_submit(AccessParameterProvider*, const MI_GET_request&);
MI_SET_confirm MI_SET_request_submit(AccessParameterProvider*, const MI_SET_request&);
} // namespace vanetza_idf::MI_SAP
@@ -0,0 +1,101 @@
#pragma once
#include <vanetza_idf/stack.hpp>
#include <vanetza/common/clock.hpp>
#include <vanetza/common/position_fix.hpp>
#include <vanetza/geonet/address.hpp>
#include <cstdint>
#include <optional>
#include <vector>
/** MN-SAP language binding: management entity <-> networking & transport layer.
*
* - TS 102 723-4 V1.1.1 clause 5: the MN-SAP provides MN-COMMAND and MN-REQUEST
* whose primitives are specified in TS 102 723-1 (not reproduced here; the
* generic envelope is the one of TS 102 723-3 Tables 1 to 4: id, CommandRef,
* command/request number and value, ErrStatus).
* - TS 103 836-4-1 V2.2.1 Annex K: CORE_MMT.request/.response carry time, the
* local position vector, the GeoNetworking address and the TC mapping from
* the N&T management entity to the GN core; EN 302 890-2 V2.1.1 clause
* 5.5.2 names the PoTi minimum data set (timestamp, latitude, longitude,
* horizontal confidence) a networking function receives.
* - TS 103 175 V1.1.1 clause 8.3: MN-GET/MN-SET for the DCC N-Params of
* Table 10, served by the access/DCC adapter of the application.
* The management entity may run in the same task or elsewhere; the library
* only applies what it receives and forwards what it cannot answer itself.
*/
namespace vanetza_idf::MN_SAP {
/// TS 103 836-4-1 V2.2.1 clause K.2: CORE_MMT.request Request cause
enum class RequestCause : std::uint8_t { TIME, POSITION_VECTOR, GN_ADDRESS_INITIAL, GN_ADDRESS_DAD, TC_MAPPING };
struct CORE_MMT_request { RequestCause request_cause; };
/// Clause K.3: CORE_MMT.response, all parameters optional, at least one present
struct CORE_MMT_response {
std::optional<vanetza::Clock::time_point> time; // reference time for freshness of received packets
std::optional<vanetza::PositionFix> local_position_vector; // position, speed, heading, timestamp, accuracy
std::optional<vanetza::geonet::Address> geonetworking_address;
std::optional<std::vector<std::uint8_t>> tc_mapping; // Annex G traffic class parameters (opaque here)
};
/** Apply a CORE_MMT.response to the GN core. Time -> Stack::advance, position ->
* Stack::update_position. The GeoNetworking address is accepted only with
* itsGnLocalAddrConfMethod == Managed (clause 10.2.1.3.3: the GN core updates the
* MID with an unsolicited CORE_MMT.response); with Auto (10.2.1.2: the address is
* not changed) or Anonymous (10.2.1.4: the security entity owns the identifier) it
* is refused with Result::unsupported. TC mapping is refused as unsupported: the
* port uses the fixed TS 102 687 profile-to-access-category mapping. An empty
* response is Result::invalid_argument; the first failing parameter stops. */
Result CORE_MMT_response_apply(Stack&, const CORE_MMT_response&);
/// TS 102 723-3 V1.1.1 Table 2 ErrStatus is "specified in TS 102 723-1"; that text is not
/// available to this library, so only the values named in TS 102 723-3 clause 5.2.3
/// (5 = INVALID COMMAND/REQUEST NUMBER) and this library's outcomes are enumerated.
enum class ErrStatus : std::uint8_t {
SUCCESS = 0,
INVALID_COMMAND_REQUEST_NUMBER = 5,
UNSUPPORTED = 250, // library: no provider for this parameter
READ_ONLY = 251, // library: TS 103 175 Table 10 access R
INVALID_VALUE = 252, // library: outside the Table 10 format
FAILED = 253 // library: provider reported a failure
};
/// TS 103 175 V1.1.1 Table 10: N-Param.No for the DCC interface at the MN-SAP
enum class N_Param_No : std::uint8_t {
GLOBAL_CBR = 0, // R, 1 octet 0..100 (TS 102 636-4-2 global CBR of the selected channel)
CHANNEL_NUMBER = 1, // R/W, 1 octet 1..7 (selects the channel for consecutive reads/writes)
LOCAL_CBR = 2, // R, 1 octet 0..100 (from the MI-SAP for the selected channel)
AVAILABLE_RESOURCE = 3, // R, 2 octets, reciprocal value of CBRa
LAST_TRANSMIT_TIME = 4, // R, 4 octets, granularity 1 OFDM symbol = 8 us
IDLE_TIME = 5, // R/W, 2 octets in ms (Toff per radio channel)
TX_POWER_LEVEL_LIMIT = 6, // R/W, 1 octet, bits 0..4 EIRP 0..31 dBm, bits 5..7 reserved
};
struct N_Param { N_Param_No no; std::uint32_t value; };
struct N_Error { N_Param_No n_param_no; ErrStatus err_status; };
/// Table 6: MN-GET.request
struct MN_GET_request { std::uint32_t nt_id; std::uint8_t command_ref; std::vector<N_Param_No> n_param_no; };
/// Table 7: MN-GET.confirm
struct MN_GET_confirm { std::uint32_t nt_id; std::uint8_t command_ref; std::vector<N_Param> n_param; std::vector<N_Error> errors; };
/// Table 8: MN-SET.request
struct MN_SET_request { std::uint32_t nt_id; std::uint8_t command_ref; std::vector<N_Param> n_param; };
/// Table 9: MN-SET.confirm (Errors optional)
struct MN_SET_confirm { std::uint32_t nt_id; std::uint8_t command_ref; std::vector<N_Error> errors; };
/** Implemented by the access/DCC adapter of the application: the only source of
* channel load, transmit times and power limits. The stack never invents these. */
class NetworkParameterProvider {
public:
virtual ~NetworkParameterProvider() = default;
virtual ErrStatus get(N_Param_No, std::uint32_t& value) = 0;
virtual ErrStatus set(N_Param_No, std::uint32_t value) = 0;
};
/// Format limits of Table 10; a value outside is INVALID_VALUE before the provider sees it
bool n_param_value_in_format(N_Param_No, std::uint32_t value);
bool n_param_writable(N_Param_No);
/** MN-GET/MN-SET over a provider; a null provider answers UNSUPPORTED for every parameter. */
MN_GET_confirm MN_GET_request_submit(NetworkParameterProvider*, const MN_GET_request&);
MN_SET_confirm MN_SET_request_submit(NetworkParameterProvider*, const MN_SET_request&);
} // namespace vanetza_idf::MN_SAP
@@ -0,0 +1,89 @@
#pragma once
#include <vanetza_idf/stack.hpp>
#include <utility>
namespace vanetza_idf::NF_SAP {
enum class SecurityProfile { UNSECURED, SECURED };
/** BTP-DATA.request language binding.
* TS 102 723-11 V2.0.0 clause 5 incorporates V1.1.1; the complete BTP
* parameter contract is TS 103 836-5-1 V2.1.1 Annex A.2.
* '-' and '.' in primitive names become '_' in C++ identifiers.
* Parameters retain the extracted snake_case names. length counts octets;
* gn_maximum_packet_lifetime uses the GN lifetime value, not an unlabelled int.
*/
struct BTP_DATA_request {
vanetza::ByteBuffer fl_sdu;
std::size_t length = 0;
BtpType btp_type = BtpType::b;
std::uint16_t destination_port = 0;
std::optional<std::uint16_t> destination_port_info;
std::optional<std::uint16_t> source_port;
vanetza::geonet::TransportType gn_packet_transport_type = vanetza::geonet::TransportType::SHB;
vanetza::geonet::CommunicationProfile gn_communication_profile = vanetza::geonet::CommunicationProfile::ITS_G5;
std::optional<SecurityProfile> gn_security_profile;
vanetza::geonet::TrafficClass gn_traffic_class;
std::optional<vanetza::geonet::Lifetime> gn_maximum_packet_lifetime;
vanetza::geonet::DestinationVariant gn_destination_address = nullptr;
std::optional<unsigned> gn_maximum_hop_limit;
std::optional<vanetza::geonet::DataRequest::Repetition> gn_repetition;
vanetza::ItsAid its_aid = 0;
vanetza::ByteBuffer permissions;
// SN-ENCAP context_information (TS 102 723-8 V1.1.1 Table 24), forwarded by GN
// as the TRANSP_CORE.request Security context information (TS 103 836-4-1 Annex J.2)
vanetza::ByteBuffer context_information;
};
/** BTP-DATA.indication, TS 103 836-5-1 V2.1.1 Annex A.3.
* received_fl_sdu is the facilities payload; gn preserves the complete
* router indication, including source position and security report metadata.
*/
struct BTP_DATA_indication {
vanetza::ByteBuffer received_fl_sdu;
std::size_t length;
BtpType btp_type;
std::uint16_t destination_port;
std::optional<std::uint16_t> destination_port_info;
std::optional<std::uint16_t> source_port;
vanetza::geonet::DataIndication gn;
std::optional<vanetza::ByteBuffer> certificate_id;
};
inline BTP_DATA_indication BTP_DATA_indication_from(BtpIndication indication) {
const auto length = indication.data.size();
return {std::move(indication.data), length, indication.type,
indication.destination_port, indication.destination_port_info,
indication.source_port, std::move(indication.gn),
std::move(indication.certificate_id)};
}
inline Result BTP_DATA_request_submit(Stack& stack, BTP_DATA_request primitive) {
if (primitive.length != primitive.fl_sdu.size()) return Result::invalid_argument;
if (primitive.gn_security_profile) {
if (*primitive.gn_security_profile != SecurityProfile::SECURED &&
*primitive.gn_security_profile != SecurityProfile::UNSECURED)
return Result::invalid_argument;
const bool secured = *primitive.gn_security_profile == SecurityProfile::SECURED;
// A per-request profile cannot silently change station security policy.
if (secured != stack.config().mib.itsGnSecurity) return Result::unsupported;
}
BtpRequest request;
request.type = primitive.btp_type;
request.source_port = primitive.source_port;
request.destination_port = primitive.destination_port;
request.destination_port_info = primitive.destination_port_info;
request.transport = primitive.gn_packet_transport_type;
request.communication_profile = primitive.gn_communication_profile;
request.traffic_class = primitive.gn_traffic_class;
request.maximum_lifetime = primitive.gn_maximum_packet_lifetime;
request.destination = primitive.gn_destination_address;
request.maximum_hop_limit = primitive.gn_maximum_hop_limit;
request.repetition = primitive.gn_repetition;
request.its_aid = primitive.its_aid;
request.permissions = std::move(primitive.permissions);
request.security_context = std::move(primitive.context_information);
request.data = std::move(primitive.fl_sdu);
return stack.request(std::move(request));
}
}
@@ -0,0 +1,29 @@
#pragma once
#include <vanetza_idf/credentials.hpp>
#include <string>
/** CredentialStore on ESP-IDF non-volatile storage (the nvs_flash component).
*
* One bundle (credentials.hpp) as one NVS blob under a namespace and key of the
* application's choosing. The application initialises NVS itself
* (nvs_flash_init or nvs_flash_secure_init) before using the store; whether the
* private keys rest encrypted is the application's NVS/flash encryption
* configuration (ESP-IDF "NVS Encryption"), not this class's doing. Built with
* CONFIG_VANETZA_IDF_NVS_CREDENTIALS (default on with the security entity).
*/
namespace vanetza_idf::security {
class NvsCredentialStore final : public CredentialStore {
public:
/// namespace_name up to 15 characters, key up to 15 characters (NVS limits)
explicit NvsCredentialStore(std::string namespace_name = "vanetza_idf", std::string key = "credentials") :
namespace_(std::move(namespace_name)), key_(std::move(key)) {}
Result save(const Credentials&) override;
Result load(Credentials&) override;
Result erase() override;
private:
std::string namespace_;
std::string key_;
};
} // namespace vanetza_idf::security
@@ -0,0 +1,245 @@
#pragma once
#include <vanetza_idf/access.hpp>
#include <vanetza_idf/security.hpp>
#include <vanetza/common/byte_buffer.hpp>
#include <vanetza/common/clock.hpp>
#include <vanetza/common/its_aid.hpp>
#include <vanetza/security/backend.hpp>
#include <vanetza/security/hash_algorithm.hpp>
#include <vanetza/security/hashed_id.hpp>
#include <vanetza/security/private_key.hpp>
#include <vanetza/security/public_key.hpp>
#include <vanetza/security/v3/certificate.hpp>
#include <array>
#include <cstdint>
#include <optional>
#include <string>
#include <utility>
#include <vector>
/** TS 102 941 V2.2.1 enrolment and authorization request/response core.
*
* Clause 6.2.3.2 (EnrolmentRequest/Response) and clause 6.2.3.3
* (AuthorizationRequest/Response with proof of possession), message formats
* of Annex A.2, signed structures per TS 103 097 V2.2.1 clause 5.2 with
* psid = secured certificate request (TS 102 965 V2.4.1 Table A.1, 623),
* encryption per TS 103 097 clause 5.3: ECIES (IEEE Std 1609.2 clause 5.3.5,
* KDF2 of IEEE Std 1363a, HMAC-SHA256 tag truncated to 128 bit, key
* derivation parameter = SHA-256 of the recipient certificate) and
* AES-128-CCM (IEEE Std 1609.2 clause 5.3.8: 12-octet nonce, 16-octet tag
* appended to the ciphertext).
*
* The library builds and parses the messages only. Transport to the EA/AA
* (TS 102 941 clause 6.1 reference points S3/S4, HTTP in practice) and the
* retrieval of CTL and CRL from a distribution centre (clause 6.3.5, Annex D:
* GET <dc>/getctl/<HashedId8>, GET <dc>/getcrl/<HashedId8>) are supplied by the
* application; the lists themselves are built, verified and read here (clause
* 6.3.2 to 6.3.6, formats of clause A.2.7 as compiled from the TS 102 941
* V1.3.1 module, unchanged for these types in V2.2.1); butterfly keys (clause
* 6.2.3.5) are not implemented.
*/
namespace vanetza_idf::pki {
using vanetza::ByteBuffer;
using vanetza::security::HashAlgorithm;
using vanetza::security::HashedId8;
using vanetza::security::KeyType;
using vanetza::security::PrivateKey;
using vanetza::security::PublicKey;
using vanetza::security::v3::Certificate;
/// A verification or encryption key pair (IEEE Std 1609.2 PublicVerificationKey /
/// BasePublicEncryptionKey, TS 103 097 V2.2.1 clause 6): private scalar and the
/// uncompressed public point of the same curve.
struct KeyPair {
PrivateKey priv;
PublicKey pub; // uncompressed
};
/** Primitives beyond vanetza::security::Backend that ECIES and AES-CCM need.
* Implemented on OpenSSL (host) and the PSA Crypto API (device). */
class EciesBackend {
public:
virtual ~EciesBackend() = default;
/// fresh key pair on the given curve (NIST P-256 or brainpoolP256r1 for ECIES)
virtual KeyPair generate_key(KeyType) = 0;
/// x coordinate of the ECDH shared point (IEEE 1609.2 5.3.5: the shared secret value)
virtual std::optional<ByteBuffer> ecdh_x(const PrivateKey& own, const PublicKey& peer) = 0;
/// HMAC-SHA256 (FIPS PUB 198-1), full 32-octet tag
virtual ByteBuffer hmac_sha256(const ByteBuffer& key, const ByteBuffer& data) = 0;
/// cryptographically strong random octets
virtual ByteBuffer random(std::size_t octets) = 0;
/// AES-128-CCM, 12-octet nonce, no associated data; output = ciphertext || 16-octet tag
virtual bool aes_ccm_encrypt(const std::array<std::uint8_t, 16>& key, const std::array<std::uint8_t, 12>& nonce,
const ByteBuffer& plaintext, ByteBuffer& ciphertext_and_tag) = 0;
virtual bool aes_ccm_decrypt(const std::array<std::uint8_t, 16>& key, const std::array<std::uint8_t, 12>& nonce,
const ByteBuffer& ciphertext_and_tag, ByteBuffer& plaintext) = 0;
};
/// IEEE 1609.2 clause 6.3.36 EciesP256EncryptedKey: ephemeral point v, wrapped key c, tag t
struct EncryptedKey {
PublicKey v;
std::array<std::uint8_t, 16> c;
std::array<std::uint8_t, 16> t;
};
/// KDF2 (IEEE 1363a-2004 clause 13.2 with SHA-256): counter starts at 1, big-endian
ByteBuffer kdf2_sha256(vanetza::security::Backend&, const ByteBuffer& shared_secret, const ByteBuffer& kdp, std::size_t length);
/// ECIES encryption of a 16-octet AES key for a recipient public key; p1 = key derivation parameter
std::optional<EncryptedKey> ecies_encrypt_key(vanetza::security::Backend&, EciesBackend&, const PublicKey& recipient,
const ByteBuffer& p1, const std::array<std::uint8_t, 16>& aes_key);
/// ECIES decryption with the recipient private key; none when the tag does not verify
std::optional<std::array<std::uint8_t, 16>> ecies_decrypt_key(vanetza::security::Backend&, EciesBackend&,
const PrivateKey& recipient, const ByteBuffer& p1,
const EncryptedKey&);
/** State kept from a request for reading its response: the AES key (the response is
* encrypted with it, referenced by pskRecipInfo) and the request hash the response
* must echo (left-most 16 octets of SHA-256 of the encrypted request). */
struct RequestContext {
std::array<std::uint8_t, 16> aes_key {};
std::array<std::uint8_t, 16> request_hash {};
};
/// requested appPermissions (IEEE Std 1609.2 SequenceOfPsidSsp: ITS-AID and opaque SSP),
/// TS 102 941 V2.2.1 clause 6.2.3.2/6.2.3.3 request parameters
using Permissions = std::vector<std::pair<vanetza::ItsAid, ByteBuffer>>;
/// Clause 6.2.3.2.1 inputs
struct EnrolmentRequestParameters {
ByteBuffer its_id; // canonical identifier (initial) or HashedId8 of the current EC (re-enrolment)
KeyPair verification_key; // new key pair; the private key signs the proof of possession
Permissions app_permissions; // requestedSubjectAttributes.appPermissions
PrivateKey outer_signer_key; // canonical private key (initial) or current EC private key
const Certificate* current_ec = nullptr; // set for re-enrolment: outer signer = digest of the EC
HashAlgorithm hash = HashAlgorithm::SHA256;
};
/** Build the EtsiTs103097Data-Encrypted EnrolmentRequest for the EA whose certificate
* carries the encryption key. Result::invalid_argument for missing keys or an EA
* certificate without encryptionKey, security_unavailable on backend failure. */
Result build_enrolment_request(vanetza::security::Backend&, EciesBackend&, vanetza::Clock::time_point now,
const EnrolmentRequestParameters&, const Certificate& ea, ByteBuffer& encoded,
RequestContext&);
/// Clause 6.2.3.2.2 InnerEcResponse
struct EnrolmentResponse {
std::uint8_t response_code = 0; // EnrolmentResponseCode, 0 = ok
std::optional<Certificate> certificate;
};
/** Decrypt with the request context (pskRecipInfo must match), verify the EA signature
* (signer digest = HashedId8 of ea, psid 623), check the requestHash, decode. Result::rejected
* when any check fails; the response is never trusted on decoding alone. */
Result parse_enrolment_response(vanetza::security::Backend&, EciesBackend&, const RequestContext&, const Certificate& ea,
const ByteBuffer& encoded, EnrolmentResponse&);
/// Clause 6.2.3.3.1 inputs
struct AuthorizationRequestParameters {
KeyPair verification_key; // new AT key pair
std::optional<KeyPair> encryption_key; // optional AT encryption key
Permissions app_permissions; // shall be present
std::optional<std::pair<vanetza::Clock::time_point, std::uint16_t>> validity_period; // start, hours
const Certificate* ec = nullptr; // enrolment credential signing SharedAtRequest
PrivateKey ec_key;
bool privacy = true; // [Itss_WithPrivacy]: EC signature encrypted for the EA
bool include_pop = true; // AuthorizationRequestMessageWithPop
HashAlgorithm hash = HashAlgorithm::SHA256;
};
Result build_authorization_request(vanetza::security::Backend&, EciesBackend&, vanetza::Clock::time_point now,
const AuthorizationRequestParameters&, const Certificate& ea, const Certificate& aa,
ByteBuffer& encoded, RequestContext&);
/// Clause 6.2.3.3.2 InnerAtResponse
struct AuthorizationResponse {
std::uint8_t response_code = 0; // AuthorizationResponseCode, 0 = ok
std::optional<Certificate> certificate;
};
Result parse_authorization_response(vanetza::security::Backend&, EciesBackend&, const RequestContext&, const Certificate& aa,
const ByteBuffer& encoded, AuthorizationResponse&);
// ---- Building blocks shared with tests and authority-side tooling ---------------------
/// EtsiTs103097Data-Signed over a payload: signer self (signer_cert null) or digest of signer_cert
std::optional<ByteBuffer> sign_data(vanetza::security::Backend&, vanetza::Clock::time_point now, HashAlgorithm,
const ByteBuffer& payload, const PrivateKey& key, const Certificate* signer_cert);
/// EtsiTs103097Data-SignedExternalPayload: extDataHash = SHA-256 of external_payload
std::optional<ByteBuffer> sign_external(vanetza::security::Backend&, vanetza::Clock::time_point now,
const ByteBuffer& external_payload, const PrivateKey& key,
const Certificate& signer_cert);
/// EtsiTs103097Data-Encrypted for one certificate recipient; aes_key/nonce generated, key returned
std::optional<ByteBuffer> encrypt_for(vanetza::security::Backend&, EciesBackend&, const Certificate& recipient,
const ByteBuffer& plaintext, std::array<std::uint8_t, 16>& aes_key);
/// EtsiTs103097Data-Encrypted with pskRecipInfo for a known symmetric key
std::optional<ByteBuffer> encrypt_with_psk(EciesBackend&, const std::array<std::uint8_t, 16>& aes_key, const ByteBuffer& plaintext);
/// Decrypt an EtsiTs103097Data-Encrypted addressed to a certificate (certRecipInfo) with its private encryption key
std::optional<ByteBuffer> decrypt_as_recipient(vanetza::security::Backend&, EciesBackend&, const Certificate& recipient,
const PrivateKey& encryption_key, const ByteBuffer& encoded,
std::array<std::uint8_t, 16>* aes_key = nullptr);
/// Decrypt an EtsiTs103097Data-Encrypted whose pskRecipInfo matches aes_key
std::optional<ByteBuffer> decrypt_with_psk(EciesBackend&, const std::array<std::uint8_t, 16>& aes_key, const ByteBuffer& encoded);
/// Verify an EtsiTs103097Data-Signed: self-signed with public_key, or by signer_cert when given; returns the payload
std::optional<ByteBuffer> verify_signed(vanetza::security::Backend&, const ByteBuffer& encoded, const PublicKey* self_key,
const Certificate* signer_cert, vanetza::ItsAid expected_psid);
// ---- TS 102 941 V2.2.1 clause 6.3: trust list and revocation list of a root CA ----------
//
// RcaCertificateTrustListMessage (clause 6.3.2, 6.3.4, A.2.7): EtsiTs103097Data-Signed over an
// EtsiTs102941Data{certificateTrustListRca ToBeSignedRcaCtl}, signed with the RCA's key, the
// signer carrying the RCA certificate, psid = CTL service (TS 102 965 Table A.1, 624), the RCA
// certificate holding the CTL appPermissions (TS 102 941 Table B.3: 0138 for a root CTL).
// CertificateRevocationListMessage (clause 6.3.3): the same over
// EtsiTs102941Data{certificateRevocationList ToBeSignedCrl}, psid = CRL service (622).
// Clause 6.3.6: an ITS-S accepts either only when it verifies as signed by its RCA; it then
// takes the EA/AA entries as trusted issuers and the CRL entries as revoked.
using Time32 = std::uint32_t; // IEEE Std 1609.2 Time32, seconds since 2004-01-01 00:00:00 TAI
struct TrustListEntries {
struct Authority { ByteBuffer certificate; std::string access_point; }; // EaEntry / AaEntry
struct DistributionCentre { std::string url; std::vector<HashedId8> certificates; }; // DcEntry
std::vector<Authority> ea, aa;
std::vector<DistributionCentre> dc;
};
/// FullCtl (isFullCtl true, ctlCommands add only) of an RCA, version 1; ctl_sequence 0..255
std::optional<ByteBuffer> build_rca_ctl(vanetza::security::Backend&, vanetza::Clock::time_point now, const Certificate& rca,
const PrivateKey& rca_key, const TrustListEntries&, Time32 next_update,
std::uint8_t ctl_sequence);
/// CRL of an RCA, version 1
std::optional<ByteBuffer> build_crl(vanetza::security::Backend&, vanetza::Clock::time_point now, const Certificate& rca,
const PrivateKey& rca_key, const std::vector<HashedId8>& revoked, Time32 this_update,
Time32 next_update);
struct RcaTrustList {
std::uint8_t sequence = 0;
Time32 next_update = 0;
bool full = true;
std::vector<Certificate> ea, aa; // added entries (issuer of each: the RCA, checked)
std::vector<TrustListEntries::DistributionCentre> dc;
std::vector<HashedId8> deleted; // DeltaCtl delete commands (certificates)
std::vector<std::string> deleted_dc;
};
/// Clause 6.3.6: the message verifies as signed by rca (signer certificate equal to rca,
/// psid 624, rca permitted for the CTL service), decodes as an RCA CTL of version 1 and every
/// EA/AA entry is issued by rca with a verifying signature (IEEE Std 1609.2 clause 5.3.1).
std::optional<RcaTrustList> parse_rca_ctl(vanetza::security::Backend&, const ByteBuffer& message, const Certificate& rca);
struct RevocationList {
Time32 this_update = 0, next_update = 0;
std::vector<HashedId8> revoked;
};
/// the same for a CRL (psid 622, rca permitted for the CRL service)
std::optional<RevocationList> parse_crl(vanetza::security::Backend&, const ByteBuffer& message, const Certificate& rca);
/// Clause 6.3.6: the EA/AA entries become trusted issuers (TrustConfiguration::add_authority);
/// returns the number added (entries already known are not counted)
std::size_t apply(const RcaTrustList&, vanetza_idf::security::TrustConfiguration&);
/// Clause 6.3.6: the CRL entries become revocations by the RCA (TrustConfiguration::revoke),
/// replacing the RCA's earlier list; returns the number of entries
std::size_t apply(const RevocationList&, const Certificate& rca, vanetza_idf::security::TrustConfiguration&);
} // namespace vanetza_idf::pki
@@ -0,0 +1,346 @@
#pragma once
#include <vanetza_idf/access.hpp>
#include <vanetza_idf/id_change.hpp>
#include <vanetza/common/clock.hpp>
#include <vanetza/common/its_aid.hpp>
#include <vanetza/common/position_provider.hpp>
#include <vanetza/common/runtime.hpp>
#include <vanetza/security/backend.hpp>
#include <vanetza/security/security_entity.hpp>
#include <vanetza/security/v3/certificate.hpp>
#include <vanetza/security/v3/certificate_provider.hpp>
#include <vanetza/security/v3/certificate_validator.hpp>
#include <vanetza/security/v3/issuer_memory_lookup.hpp>
#include <vanetza/security/v3/location_checker.hpp>
#include <vanetza/security/v3/revocation_lookup.hpp>
#include <vanetza/security/v3/sign_header_policy.hpp>
#include <vanetza/security/v3/trust_store.hpp>
#include <chrono>
#include <deque>
#include <functional>
#include <memory>
#include <vector>
/** Signing security entity for the SN-SAP.
*
* Standards: TS 103 097 V2.2.1 (secured message and certificate formats,
* clause 5.2 SignedData constraints, clause 7 profiles), IEEE Std 1609.2
* (hashing and ECDSA, clause 5.3), TS 102 940 V2.1.1 (trust model: root CA ->
* AA -> authorization ticket, clause 6.5 identity management), TS 102 941
* V2.2.1 (credential life cycle), TS 102 723-8 V2.0.0/V1.1.1 (SN-SAP
* primitives), TS 103 300-3 V2.3.1 clause 6.5 (VAM signing and certificate
* attachment). Verification of received messages (SN-DECAP) follows IEEE Std
* 1609.2 clause 5.2 as TS 103 097 clause 5.2 requires, with the profile checks of
* clause 7.1 and the chain checks of TS 102 940 clause 6; it is built when
* VIDF_SECURITY_VERIFY is set (default with the security feature), otherwise
* SN-DECAP reports Configuration_Problem (docs/idf/conformance.md GAP-SEC-001).
*/
namespace vanetza_idf::security {
using vanetza::ByteBuffer;
using vanetza::security::HashedId8;
using vanetza::security::PrivateKey;
using vanetza::security::v3::Certificate;
/** SN-ENCAP.request context_information values understood by this entity
* (TS 102 723-8 V1.1.1 Table 24: opaque octets "used in selecting properties
* of the underlying security protocol"; the standard leaves the content to the
* implementation, these values are this library's definition). */
namespace context {
/// VAM generated for a VRU cluster: TS 103 300-3 V2.3.1 clause 6.5.3 500 ms certificate cadence
inline const ByteBuffer vam_cluster {0x01};
}
/** Authorization tickets owned by this station together with their private keys.
*
* TS 103 097 V2.2.1 clause 7.2.1 (AT profile), TS 102 940 V2.1.1 clause 6.5
* ("multiple authorization tickets ... to be used ... over its life time").
* Tickets are provisioned by the application, e.g. from files written by the
* station's own PKI (COER EtsiTs103097Certificate plus raw private key, the
* formats of vanetza::security::v3::load_certificate_from_file and
* load_private_key_from_*_file) or from a TS 102 941 authorization response.
*/
class CertificatePool : public vanetza::security::v3::BaseCertificateProvider {
public:
/// An authorization ticket (TS 103 097 V2.2.1 clause 7.2.1 profile) with the private
/// key of its verifyKeyIndicator; the pool owns both.
struct Ticket {
Certificate certificate;
PrivateKey key;
HashedId8 digest; // HashedId8 per TS 103 097 clause 6 / IEEE 1609.2 clause 6.4.3
};
explicit CertificatePool(vanetza::security::Backend&);
/** Add a ticket. Fails closed (Result::invalid_argument) when the certificate
* does not decode, is not an authorization ticket (clause 7.2.1: issuer
* sha256AndDigest/sha384AndDigest, appPermissions present, no
* certIssuePermissions), the key type or length does not match the
* verification key, or the private key does not belong to the certificate
* (checked by a sign/verify round trip). Duplicate digests are rejected. */
Result add(const ByteBuffer& coer_certificate, const PrivateKey&);
Result add(Certificate, PrivateKey);
/// pool bookkeeping for the application's provisioning and persistence
std::size_t size() const { return tickets_.size(); }
bool empty() const { return tickets_.empty(); }
const std::vector<Ticket>& tickets() const { return tickets_; }
/// currently selected ticket, nullptr when the pool is empty
const Ticket* current() const;
/// next different ticket valid at now (round robin), nullptr when none
const Ticket* next_valid(vanetza::Clock::time_point now) const;
/// make the ticket with this digest current (identifier change COMMIT)
Result select(const HashedId8&);
/// drop tickets whose validity ended before now, never the current one; returns count
std::size_t prune(vanetza::Clock::time_point now);
// v3::CertificateProvider: the current ticket; std::logic_error when empty
const Certificate& own_certificate() override;
const PrivateKey& own_private_key() override;
private:
vanetza::security::Backend& backend_;
std::vector<Ticket> tickets_;
std::size_t current_ = 0;
};
/** Trust anchors and issuing authorities supplied by the application.
* TS 102 940 V2.1.1 clause 6.1 (root CA, AA), TS 103 097 clauses 7.2.3/7.2.4;
* TS 102 941 V2.2.1 clause 6.3 CTL/CRL retrieval is not part of this library. */
class TrustConfiguration {
public:
/// self-signed root CA certificate (clause 7.2.3) -> trust store and issuer lookup
Result add_root(const ByteBuffer& coer_certificate);
Result add_root(const Certificate&);
/// AA or other subordinate CA certificate (clause 7.2.4) -> issuer lookup; a certificate
/// already known is Result::rejected (nothing changes), a non-CA one invalid_argument
Result add_authority(const ByteBuffer& coer_certificate);
Result add_authority(const Certificate&);
const vanetza::security::v3::TrustStore& roots() const { return roots_; }
const vanetza::security::v3::IssuerMemoryLookup& issuers() const { return issuers_; }
/// every CA certificate added (for P2P certificate distribution lookups by HashedId3)
const std::vector<Certificate>& authorities() const { return authorities_; }
/** TS 102 941 V2.2.1 clause 6.3.3/6.3.6: a certificate the named issuer has revoked
* (the RCA's CRL lists the CA certificates it no longer trusts). The validator walks
* every chain link against this list (IEEE Std 1609.2 clause 5.2: a revoked
* certificate anywhere in the chain invalidates it), so a revoked AA takes all its
* tickets with it; the station's own tickets included (it stops signing). */
void revoke(const HashedId8& issuer, const HashedId8& certificate);
/// forget every revocation recorded for this issuer (a fresh CRL replaces the old one)
void clear_revocations(const HashedId8& issuer);
const vanetza::security::v3::RevocationLookup& revocations() const { return revocations_; }
private:
vanetza::security::v3::TrustStore roots_;
vanetza::security::v3::IssuerMemoryLookup issuers_;
std::vector<Certificate> authorities_;
vanetza::security::v3::RevocationMemoryLookup revocations_;
};
/** IEEE Std 1609.2 clause 5.3.1 certificate signature: Hash(Hash(toBeSigned) || Hash(issuer
* certificate)) with the hash algorithm of the IssuerIdentifier choice (sha256AndDigest /
* sha384AndDigest, or the algorithm named in the self choice with the empty string as
* issuer hash), verified with the issuer's verification key; issuer == nullptr means
* self-signed. TS 103 097 V2.2.1 clause 6 admits NIST P-256, brainpoolP256r1 (SHA-256)
* and brainpoolP384r1 (SHA-384). */
bool verify_certificate_signature(vanetza::security::Backend&, const Certificate& subject, const Certificate* issuer);
/** IEEE Std 1609.2-2025 clause 5.1.2 permission consistency of a certificate chain, chain[0]
* the end entity and the last element the anchor: every appPermissions entry of the end
* entity is covered by a PsidGroupPermissions group of each ancestor whose chain-length
* window (6.4.28) reaches down to the end entity and whose eeType admits it, the SSP
* inside the group's range (6.4.29/6.4.30 SspRange, BitmapSspRange: a 1 bit of the mask
* fixes the subordinate's bit; an omitted SSP needs "all"); every subordinate CA's ranges
* nest inside its issuer's. Unknown CHOICE alternatives are critical (5.2.6): false.
* Used by the receive-side chain validator and by the issuing tools before they write. */
bool chain_permissions_consistent(const std::vector<const Certificate*>& chain);
/** IEEE Std 1609.2-2025 clause 6.4.17: no part of the subject's region may lie outside the
* issuer's. No issuer region: true; issuer region but none on the subject: false;
* geometric issuer regions by the upstream geometry; identifiedRegion issuer (6.4.21 to
* 6.4.24): identifier containment for an identifiedRegion subject, the
* permissive_identified_region policy for a geometric subject (no border database). */
bool region_within(const Certificate& subject, const Certificate& issuer, bool permissive_identified_region);
/** Receive-side parameters that TS 103 097 leaves to the station. generationTime
* plausibility: IEEE Std 1609.2 clause 5.2.3.2 lists it among the relevance checks
* without fixing values; the defaults are a 3 s clock tolerance into the future and the
* 5 min window the TS 103 096-2 test purposes apply to generation times (the CAM/DENM
* services apply their own, tighter freshness rules on top). Replay: a message whose
* (signer digest, generationTime) pair was already accepted is DUPLICATE_MESSAGE
* (TS 102 723-8 Table 27 report). */
struct VerificationPolicy {
std::chrono::seconds generation_time_future_tolerance {3};
std::chrono::seconds generation_time_max_age {300};
std::size_t replay_window = 256; // remembered accepted (signer, time) pairs, 0 disables
std::size_t verified_chain_cache = 64; // authorization tickets whose chain signature was verified
std::size_t certificate_cache_limit = 32; // learned certificates kept (the cache is emptied beyond it)
std::size_t learned_authority_limit = 8; // AA certificates learned through P2P distribution
bool permissive_identified_region = true; // identifiedRegion without a country database: accept (see docs)
};
/** TS 103 097 V2.2.1 clause 5.2 and 7.1.1 to 7.1.3 structural checks of a received
* signed message, before any cryptography: protocolVersion 3 on both levels, hashId
* consistent with the signature choice, generationTime present, p2pcdLearningRequest and
* missingCrlIdentifier absent, per ITS-AID: CAM without expiryTime, generationLocation
* and encryptionKey; DENM with generationLocation and with a certificate as signer,
* without expiryTime and encryptionKey; signer digest or certificate (not self).
* Returns Success when the structure is admissible, otherwise the report to give. */
vanetza::security::VerificationReport check_profile(const vanetza::security::v3::SecuredMessage&);
/** Header fields and signer identifier per message profile.
*
* TS 103 097 V2.2.1 clause 5.2: psid and generationTime always present,
* p2pcdLearningRequest and missingCrlIdentifier always absent.
* - CAM (ITS-AID 36, TS 102 965 Table A.1): clause 7.1.1 - digest by default,
* certificate once one second after its last inclusion, immediately after a
* CAM from an unknown AT (request_certificate) or an inline P2PCD request for
* our AT, inlineP2pcdRequest for unknown certificates, requestedCertificate
* for known CA certificates; no other header fields.
* - DENM (37): clause 7.1.2 - certificate always, generationLocation present.
* - VAM (638): TS 103 300-3 V2.3.1 clause 6.5.3 - individual VAM: certificate
* if >= 1 s since the last certificate attached to a VAM or a new CAM signer
* was observed (report_new_cam_signer), else digest; cluster VAM (SN-ENCAP
* context_information == context::vam_cluster): certificate if >= 500 ms,
* else digest.
* - other ITS-AIDs, including GN-MGMT beacons (141): clause 7.1.3 generic
* profile, which constrains only clause 5.2; this library includes the
* certificate once per second per ITS-AID and the digest otherwise.
*/
class Ts103097SignHeaderPolicy : public vanetza::security::v3::SignHeaderPolicy {
public:
Ts103097SignHeaderPolicy(const vanetza::Runtime&, vanetza::PositionProvider&, CertificatePool&,
const TrustConfiguration&);
~Ts103097SignHeaderPolicy() override;
void prepare_header(const vanetza::security::SignRequest&, vanetza::security::v3::SecuredMessage&) override;
void request_unrecognized_certificate(HashedId8) override;
void request_certificate() override;
void enqueue_p2p_request(vanetza::security::HashedId3) override;
void discard_p2p_request(vanetza::security::HashedId3) override;
/// TS 103 300-3 clause 6.5.3 item 2: a CAM was received from a not previously seen signer
void report_new_cam_signer();
/// after an identifier change the new ticket has never been announced: attach it next
void reset_after_identifier_change();
private:
class Impl;
std::unique_ptr<Impl> impl_;
};
/** SN-LOG-SECURITY-EVENT.request (TS 102 723-8 V1.1.1 Table 22). Events are
* kept in a bounded log the application can drain for misbehaviour reporting. */
struct SecurityEvent {
enum class Type : std::uint8_t {
TIME_CONSISTENCY_FAILED, LOCATION_CONSISTENCY_FAILED, ID_CONSISTENCY_FAILED,
DISALLOWED_MESSAGE_CONTENT, DISALLOWED_MESSAGE_FREQUENCY, REPLAY_DETECTION_TIME,
REPLAY_DETECTION_LOCATION, MOVEMENT_PLAUSIBILITY, APPEARANCE_PLAUSIBILITY,
LOCATION_PLAUSIBILITY_SENSOR, LOCATION_PLAUSIBILITY_MAP, LOCATION_PLAUSIBILITY_CONTRADICTION,
APPLICATION_SPECIFIC
};
struct Location { std::int32_t latitude; std::int32_t longitude; };
struct Evidence { std::uint8_t type; ByteBuffer content; }; // event_evidence_type/content
Type event_type;
std::vector<HashedId8> neighbour_id_list;
std::uint32_t event_time; // INTEGER 0..2^32-1, shall be in the past
std::optional<Location> event_location;
std::vector<Evidence> event_evidence_list;
};
class IdentityManager;
/** The security entity: SN-ENCAP/SN-DECAP plus the identifier-change service.
* Runtime, position provider, backend, pool and trust configuration are
* owned by the application and must outlive the entity. All calls come from
* the one task that also drives the stack. */
class SecurityEntity : public vanetza::security::SecurityEntity {
public:
SecurityEntity(vanetza::Runtime&, vanetza::PositionProvider&, vanetza::security::Backend&, CertificatePool&,
const TrustConfiguration&);
~SecurityEntity() override;
/** SN-ENCAP.request/.confirm (TS 102 723-8 V1.1.1 Tables 24/25; TS 103 836-4-1 V2.2.1
* Table 34). Refused (SignConfirmError::No_Certificate) when the pool is empty, the current
* ticket is not valid for the ITS-AID, or an identifier change is between PREPARE and COMMIT
* (clause 6.3.1.3: messages with old identifiers shall be avoided). */
vanetza::security::EncapConfirm encapsulate_packet(vanetza::security::EncapRequest&&) override;
/** SN-DECAP.request/.confirm (Tables 26/27). With VIDF_SECURITY_VERIFY: check_profile(),
* then IEEE Std 1609.2 clause 5.2 verification (signer lookup in the learned-certificate
* cache or the inline certificate, authorization ticket validity, permissions for the
* ITS-AID, chain to a trust anchor with verified certificate signatures, region, message
* signature), generationTime plausibility and replay detection per VerificationPolicy;
* a CAM/DENM from an unknown station or with an unknown AA drives the P2P certificate
* distribution of TS 103 097 clause 7.1.1 through the header policy, and an AA carried
* in requestedCertificate is learned once its signature chains to a trust anchor.
* Without VIDF_SECURITY_VERIFY: unsigned messages report UNSIGNED_MESSAGE, signed ones
* Configuration_Problem, never success (GAP-SEC-001); a receiver with
* itsGnSnDecapResultHandling = STRICT drops them either way. */
vanetza::security::DecapConfirm decapsulate_packet(vanetza::security::DecapRequest&&) override;
void set_verification_policy(const VerificationPolicy&);
const VerificationPolicy& verification_policy() const;
/// authorities learned through P2P certificate distribution (requestedCertificate), verified
const std::deque<Certificate>& learned_authorities() const;
/// SN-LOG-SECURITY-EVENT.request (Table 22); .confirm has no parameters (Table 23)
void log_security_event(SecurityEvent);
/// take all logged events, oldest first
std::deque<SecurityEvent> drain_security_events();
void set_security_event_capacity(std::size_t);
/** Outcome counters. The GN router reports GN-DATA.confirm before SN-ENCAP runs
* (TS 103 836-4-1 clause 10.3.10.2 step 2 happens at transmission), so a refused
* encapsulation is visible here and in the absence of a packet, not in Stack::request. */
struct Statistics { unsigned signed_messages = 0, refused_no_ticket = 0, refused_change_pending = 0,
refused_permission = 0, failed = 0;
// SN-DECAP outcomes (VIDF_SECURITY_VERIFY)
unsigned verified = 0, rejected_profile = 0, rejected_signer = 0, rejected_certificate = 0,
rejected_signature = 0, rejected_time = 0, replayed = 0, learned_authorities = 0; };
const Statistics& statistics() const;
IdChangeService& id_change();
const IdChangeService& id_change() const;
IdentityManager& identity_manager();
Ts103097SignHeaderPolicy& header_policy();
vanetza::security::v3::DefaultCertificateValidator& validator();
CertificatePool& certificates();
private:
class Impl;
std::unique_ptr<Impl> impl_;
};
/** TS 102 723-8 V1.1.1 clause 6.3 implementation: subscriptions, the two-phase commit
* (clause 6.3.1.3, Figures 11 to 13), locks with automatic release (clause 6.3.2,
* Table 18) and deferred triggers (clause 6.3.3). New identifier = HashedId8 of the
* next authorization ticket in the pool (TS 102 940 V2.1.1 clause 6.5). */
class IdentityManager : public IdChangeService {
public:
IdentityManager(vanetza::Runtime&, CertificatePool&);
~IdentityManager() override; // DEREG to every subscriber (Figure 15)
SubscriptionHandle subscribe(IdChangeHook, ByteBuffer subscriber_data = {}) override;
Result unsubscribe(SubscriptionHandle) override;
Result trigger() override;
LockHandle lock(std::uint8_t duration_seconds) override;
Result unlock(LockHandle) override;
Identifier current_identifier() const override;
bool change_pending() const override;
/** Time allowed for all PREPARE (or COMMIT) responses before the change is aborted
* (or the COMMIT is closed). Not specified by TS 102 723-8; library default 500 ms. */
void set_response_timeout(vanetza::Clock::duration);
/// invoked after a successful COMMIT with the new identifier
void on_committed(std::function<void(const Identifier&)>);
struct Statistics { unsigned committed = 0, aborted = 0, timed_out = 0, commit_failures = 0; };
const Statistics& statistics() const;
bool locked() const;
private:
class Impl;
std::shared_ptr<Impl> impl_; // responders keep a weak reference across the manager's lifetime
};
} // namespace vanetza_idf::security
@@ -0,0 +1,164 @@
#pragma once
#include <vanetza_idf/security.hpp>
#include <vanetza/common/its_aid.hpp>
#include <optional>
#include <utility>
#include <vector>
/** SF-SAP language binding: TS 102 723-9 V1.1.1 (interface between the
* security entity and the facilities layer). Tables 10 to 21 carry the same
* parameters as the SN-SAP identifier-change primitives of TS 102 723-8, and
* clause 4.1.5 allows one security entity to serve several layers: the SF
* primitives below are served by the same IdChangeService instance the GN
* core subscribes to. TS 103 300-3 V2.3.1 clause 5.3.5 requires the VRU
* basic service to subscribe, stop generating VAMs on PREPARE, resume after
* COMMIT with a new StationId, and to use the lock for elevated-hazard
* situations. The facilities-side subscriber may run in the same task, in
* another task or process, or on another device; the library defines no
* transport, only these bindings.
*
* SF-SIGN/-VERIFY/-ENCRYPT/-DECRYPT/-ENCAP/-DECAP (Tables 2 to 9, 24 to 27)
* are declared as parameter types only: for messages carried over BTP and
* GeoNetworking the security envelope is applied at the GeoNetworking layer
* (TS 103 300-3 clause 6.5.1; TS 103 836-4-1 clause 10.3.10.2 SN-ENCAP), so
* this library provides no facilities-level signing service.
*/
namespace vanetza_idf::SF_SAP {
// The same identifier-change service types as sn_sap.hpp (TS 102 723-9 V1.1.1 clause 4.1.5:
// one security entity serves several layers), under the SF-SAP names.
using security::Identifier;
using security::IdChangeCommand;
using security::LockHandle;
using security::SubscriptionHandle;
/// Table 10: SF-IDCHANGE-SUBSCRIBE.request
struct SF_IDCHANGE_SUBSCRIBE_request {
security::IdChangeHook idchange_event_hook; // Mandatory, signature of clause 5.2.6
vanetza::ByteBuffer subscriber_data; // Optional
};
/// Table 11: SF-IDCHANGE-SUBSCRIBE.confirm
struct SF_IDCHANGE_SUBSCRIBE_confirm { SubscriptionHandle subscription; };
/// Table 12: SF-IDCHANGE-EVENT.indication (the hook arguments)
struct SF_IDCHANGE_EVENT_indication {
IdChangeCommand command;
Identifier id;
vanetza::ByteBuffer subscriber_data;
};
/// Table 13: SF-IDCHANGE-EVENT.response
struct SF_IDCHANGE_EVENT_response { bool return_code; };
/// Table 14: SF-IDCHANGE-UNSUBSCRIBE.request; Table 15 confirm carries no parameter
struct SF_IDCHANGE_UNSUBSCRIBE_request { SubscriptionHandle subscription; };
/// Tables 16/17: SF-IDCHANGE-TRIGGER.request/.confirm carry no parameter
struct SF_IDCHANGE_TRIGGER_request {};
/// Table 18: SF-ID-LOCK.request
struct SF_ID_LOCK_request { std::uint8_t Duration; };
/// Table 19: SF-ID-LOCK.confirm
struct SF_ID_LOCK_confirm { LockHandle lock_handle; };
/// Table 20: SF-ID-UNLOCK.request; Table 21 confirm carries no parameter
struct SF_ID_UNLOCK_request { LockHandle lock_handle; };
/// Clause 5.2.11 / Table 22 (message example): SF-LOG-SECURITY-EVENT.request
using SF_LOG_SECURITY_EVENT_request = security::SecurityEvent;
/// TS 102 723-9 V1.1.1 clause 5.2.5: SF-IDCHANGE-SUBSCRIBE (Tables 10/11)
inline SF_IDCHANGE_SUBSCRIBE_confirm SF_IDCHANGE_SUBSCRIBE_request_submit(security::IdChangeService& service,
SF_IDCHANGE_SUBSCRIBE_request primitive) {
return {service.subscribe(std::move(primitive.idchange_event_hook), std::move(primitive.subscriber_data))};
}
/// Clause 5.2.7: SF-IDCHANGE-UNSUBSCRIBE (Tables 14/15)
inline Result SF_IDCHANGE_UNSUBSCRIBE_request_submit(security::IdChangeService& service,
SF_IDCHANGE_UNSUBSCRIBE_request primitive) {
return service.unsubscribe(primitive.subscription);
}
/// Clause 5.2.8: SF-IDCHANGE-TRIGGER (Tables 16/17)
inline Result SF_IDCHANGE_TRIGGER_request_submit(security::IdChangeService& service, SF_IDCHANGE_TRIGGER_request) {
return service.trigger();
}
/// Clause 5.2.9: SF-ID-LOCK (Tables 18/19), Duration in seconds 0..255
inline SF_ID_LOCK_confirm SF_ID_LOCK_request_submit(security::IdChangeService& service, SF_ID_LOCK_request primitive) {
return {service.lock(primitive.Duration)};
}
/// Clause 5.2.10: SF-ID-UNLOCK (Tables 20/21)
inline Result SF_ID_UNLOCK_request_submit(security::IdChangeService& service, SF_ID_UNLOCK_request primitive) {
return service.unlock(primitive.lock_handle);
}
/// Clause 5.2.11: SF-LOG-SECURITY-EVENT (Tables 22/23)
inline void SF_LOG_SECURITY_EVENT_request_submit(security::SecurityEntity& entity,
SF_LOG_SECURITY_EVENT_request primitive) {
entity.log_security_event(std::move(primitive));
}
// ---- Parameter types only (see the header comment) -------------------------
/// Table 2: SF-SIGN.request
struct SF_SIGN_request {
std::size_t tbs_message_length = 0;
vanetza::ByteBuffer tbs_message;
vanetza::ItsAid its_aid = 0;
std::size_t permissions_length = 0;
vanetza::ByteBuffer Permissions; // <= 31 octets, SSP of the ITS-AID
vanetza::ByteBuffer context_information; // Optional
std::optional<std::uint64_t> key_handle; // Optional
};
/// Table 3: SF-SIGN.confirm
struct SF_SIGN_confirm { std::size_t sec_message_length = 0; vanetza::ByteBuffer sec_message; };
/// Table 4: SF-VERIFY.request
struct SF_VERIFY_request {
std::size_t sec_header_length = 0;
vanetza::ByteBuffer sec_header;
std::size_t message_length = 0;
vanetza::ByteBuffer message;
};
/// Table 5: SF-VERIFY.confirm (report values of TS 102 723-8 Table 27 / vanetza::security::VerificationReport)
struct SF_VERIFY_confirm {
vanetza::security::VerificationReport report;
std::optional<security::HashedId8> certificate_id;
std::size_t its_aid_length = 0;
vanetza::ItsAid its_aid = 0;
vanetza::ByteBuffer permissions;
};
/// Table 6: SF-ENCRYPT.request
struct SF_ENCRYPT_request {
std::size_t tbe_payload_length = 0;
vanetza::ByteBuffer tbe_payload;
std::size_t target_id_list_length = 0;
std::vector<security::HashedId8> target_id_list;
vanetza::ByteBuffer context_information; // Optional
};
/// Table 7: SF-ENCRYPT.confirm
struct SF_ENCRYPT_confirm { std::size_t encrypted_message_length = 0; vanetza::ByteBuffer encrypted_message; };
/// Table 8: SF-DECRYPT.request
struct SF_DECRYPT_request { std::size_t encrypted_message_length = 0; vanetza::ByteBuffer encrypted_message; };
/// Table 9: SF-DECRYPT.confirm
struct SF_DECRYPT_confirm {
enum class Report : std::uint8_t { SUCCESS, UNENCRYPTED_MESSAGE, DECRYPTION_ERROR, INCOMPATIBLE_PROTOCOL };
std::size_t plaintext_message_length = 0;
vanetza::ByteBuffer plaintext_message;
Report report;
};
/// Table 24: SF-ENCAP.request (identical parameters to SN-ENCAP.request)
struct SF_ENCAP_request {
std::size_t tbe_packet_length = 0;
vanetza::ByteBuffer tbe_packet;
std::optional<std::uint16_t> sec_services;
vanetza::ItsAid its_aid = 0;
vanetza::ByteBuffer permissions;
vanetza::ByteBuffer context_information;
std::vector<security::HashedId8> target_id_list;
};
/// Table 25: SF-ENCAP.confirm
struct SF_ENCAP_confirm { std::size_t sec_packet_length = 0; vanetza::ByteBuffer sec_packet; };
/// Table 26: SF-DECAP.request
struct SF_DECAP_request { std::size_t sec_packet_length = 0; vanetza::ByteBuffer sec_packet; };
/// Table 27: SF-DECAP.confirm
struct SF_DECAP_confirm {
std::size_t plaintext_packet_length = 0;
vanetza::ByteBuffer plaintext_packet;
vanetza::security::VerificationReport report;
std::optional<security::HashedId8> certificate_id;
std::size_t its_aid_length = 0;
vanetza::ItsAid its_aid = 0;
vanetza::ByteBuffer permissions;
};
} // namespace vanetza_idf::SF_SAP
@@ -0,0 +1,189 @@
#pragma once
#include <vanetza_idf/security.hpp>
#include <vanetza/common/byte_buffer_sink.hpp>
#include <vanetza/net/osi_layer.hpp>
#include <vanetza/net/packet.hpp>
#include <vanetza/security/secured_message.hpp>
#include <boost/iostreams/stream.hpp>
#include <optional>
#include <utility>
#include <vector>
/** SN-SAP language binding: TS 102 723-8 V2.0.0 clause 5 incorporates V1.1.1,
* whose Tables 10 to 27 define the primitives below. '-' and '.' in primitive
* names become '_'; parameters keep the tables' names in snake_case. The
* *_submit functions are thin calls into the security entity, like
* NF_SAP::BTP_DATA_request_submit. Inside the stack the GeoNetworking router
* calls the same entity directly (vanetza::security::SecurityEntity), so
* these bindings serve a caller in another task, process or device and the
* test adapters; the library defines no transport for that.
*/
namespace vanetza_idf::SN_SAP {
// Types of the identifier-change service (id_change.hpp: TS 102 723-8 V1.1.1 clause 6.3,
// identifier = HashedId8 per TS 102 940 V2.1.1 clause 6.5) under the SN-SAP names.
using security::Identifier;
using security::IdChangeCommand;
using security::LockHandle;
using security::SubscriptionHandle;
/// TS 102 723-8 V1.1.1 Table 10: SN-IDCHANGE-SUBSCRIBE.request
struct SN_IDCHANGE_SUBSCRIBE_request {
security::IdChangeHook idchange_event_hook; // Mandatory: hook function, signature of clause 5.2.6
vanetza::ByteBuffer subscriber_data; // Optional: passed back on every hook call
};
/// Table 11: SN-IDCHANGE-SUBSCRIBE.confirm
struct SN_IDCHANGE_SUBSCRIBE_confirm {
SubscriptionHandle subscription; // INTEGER 0 to 2^64-1
};
/// Table 12: SN-IDCHANGE-EVENT.indication (the arguments of the hook function)
struct SN_IDCHANGE_EVENT_indication {
IdChangeCommand command; // PREPARE, COMMIT, ABORT, DEREG (clause 6.3)
Identifier id; // OCTET STRING, 8 octets: id to be set
vanetza::ByteBuffer subscriber_data; // Optional
};
/// Table 13: SN-IDCHANGE-EVENT.response
struct SN_IDCHANGE_EVENT_response {
bool return_code; // acknowledgement to the given command
};
/// Table 14: SN-IDCHANGE-UNSUBSCRIBE.request; Table 15 confirm carries no parameter
struct SN_IDCHANGE_UNSUBSCRIBE_request {
SubscriptionHandle subscription;
};
/// Table 16/17: SN-IDCHANGE-TRIGGER.request/.confirm carry no parameter
struct SN_IDCHANGE_TRIGGER_request {};
/// Table 18: SN-ID-LOCK.request
struct SN_ID_LOCK_request {
std::uint8_t Duration; // INTEGER 0 to 2^8-1: number of seconds to lock
};
/// Table 19: SN-ID-LOCK.confirm
struct SN_ID_LOCK_confirm {
LockHandle lock_handle; // INTEGER 0 to 2^64-1: handle to unlock manually
};
/// Table 20: SN-ID-UNLOCK.request; Table 21 confirm carries no parameter
struct SN_ID_UNLOCK_request {
LockHandle lock_handle;
};
/// Table 22: SN-LOG-SECURITY-EVENT.request; Table 23 confirm carries no parameter
using SN_LOG_SECURITY_EVENT_request = security::SecurityEvent;
/// Table 24: SN-ENCAP.request (the GeoNetworking source fills it per TS 103 836-4-1 V2.2.1 Table 34)
struct SN_ENCAP_request {
std::size_t tbe_packet_length = 0; // Mandatory: length of tbe_packet
vanetza::ByteBuffer tbe_packet; // Mandatory: packet to encapsulate (Common Header onwards)
std::optional<std::uint16_t> sec_services; // Optional: security service(s) to invoke
vanetza::ItsAid its_aid = 0; // Mandatory: ITS-AID selecting the security profile
vanetza::ByteBuffer permissions; // Mandatory: SSP associated with the ITS-AID (<= 31 octets)
vanetza::ByteBuffer context_information; // Optional: opaque, see security::context
std::vector<security::HashedId8> target_id_list; // Optional: recipients (encryption is not implemented)
};
/// Table 25: SN-ENCAP.confirm
struct SN_ENCAP_confirm {
std::size_t sec_packet_length = 0;
vanetza::ByteBuffer sec_packet; // the Secured Packet, EtsiTs103097Data (COER)
};
/// Table 26: SN-DECAP.request
struct SN_DECAP_request {
std::size_t sec_packet_length = 0;
vanetza::ByteBuffer sec_packet; // EtsiTs103097Data (COER), without the GN basic header
};
/// Table 27: SN-DECAP.confirm. report never reads SUCCESS from this library: verification
/// is not implemented (docs/idf/conformance.md GAP-SEC-001), the entity answers
/// CONFIGURATION_PROBLEM (signed) or UNSIGNED_MESSAGE; the ITS-AID and permissions are
/// those the packet claims, forwarded for the caller's own policy, not verified.
struct SN_DECAP_confirm {
std::size_t plaintext_packet_length = 0;
vanetza::ByteBuffer plaintext_packet;
vanetza::security::VerificationReport report = vanetza::security::VerificationReport::Configuration_Problem;
std::optional<security::HashedId8> certificate_id;
vanetza::ItsAid its_aid = 0;
vanetza::ByteBuffer permissions;
};
/// TS 102 723-8 V1.1.1 clause 5.2.5: SN-IDCHANGE-SUBSCRIBE (Tables 10/11)
inline SN_IDCHANGE_SUBSCRIBE_confirm SN_IDCHANGE_SUBSCRIBE_request_submit(security::IdChangeService& service,
SN_IDCHANGE_SUBSCRIBE_request primitive) {
return {service.subscribe(std::move(primitive.idchange_event_hook), std::move(primitive.subscriber_data))};
}
/// Clause 5.2.7: SN-IDCHANGE-UNSUBSCRIBE (Tables 14/15)
inline Result SN_IDCHANGE_UNSUBSCRIBE_request_submit(security::IdChangeService& service,
SN_IDCHANGE_UNSUBSCRIBE_request primitive) {
return service.unsubscribe(primitive.subscription);
}
/// Clause 5.2.8: SN-IDCHANGE-TRIGGER (Tables 16/17)
inline Result SN_IDCHANGE_TRIGGER_request_submit(security::IdChangeService& service, SN_IDCHANGE_TRIGGER_request) {
return service.trigger();
}
/// Clause 5.2.9: SN-ID-LOCK (Tables 18/19), Duration in seconds 0..255
inline SN_ID_LOCK_confirm SN_ID_LOCK_request_submit(security::IdChangeService& service, SN_ID_LOCK_request primitive) {
return {service.lock(primitive.Duration)};
}
/// Clause 5.2.10: SN-ID-UNLOCK (Tables 20/21)
inline Result SN_ID_UNLOCK_request_submit(security::IdChangeService& service, SN_ID_UNLOCK_request primitive) {
return service.unlock(primitive.lock_handle);
}
/// Clause 5.2.11: SN-LOG-SECURITY-EVENT (Tables 22/23)
inline void SN_LOG_SECURITY_EVENT_request_submit(security::SecurityEntity& entity,
SN_LOG_SECURITY_EVENT_request primitive) {
entity.log_security_event(std::move(primitive));
}
/** SN-ENCAP over raw octets. Result::invalid_argument on a length mismatch or a
* permissions field beyond 31 octets, Result::unsupported when sec_services or
* target_id_list request anything but signing, Result::security_unavailable
* when the entity refuses (no usable ticket, identifier change pending). */
/// Clause 5.2.12: SN-ENCAP (Tables 24/25); clause 5.2.13 SN-DECAP (Tables 26/27) below
inline Result SN_ENCAP_request_submit(vanetza::security::SecurityEntity& entity, SN_ENCAP_request primitive,
SN_ENCAP_confirm& confirm) {
if (primitive.tbe_packet_length != primitive.tbe_packet.size() || primitive.tbe_packet.empty() ||
primitive.permissions.size() > 31) return Result::invalid_argument;
if (primitive.sec_services || !primitive.target_id_list.empty()) return Result::unsupported;
vanetza::security::SignRequest request;
request.plain_message[vanetza::OsiLayer::Network] = std::move(primitive.tbe_packet);
request.its_aid = primitive.its_aid;
request.permissions = std::move(primitive.permissions);
request.context_information = std::move(primitive.context_information);
auto result = entity.encapsulate_packet(vanetza::security::EncapRequest {std::move(request)});
const auto* secured = result.secured_message();
if (!secured) return Result::security_unavailable;
confirm.sec_packet.clear();
vanetza::byte_buffer_sink sink(confirm.sec_packet);
boost::iostreams::stream_buffer<vanetza::byte_buffer_sink> stream(sink);
vanetza::OutputArchive archive(stream);
vanetza::security::serialize(archive, *secured);
stream.pubsync();
confirm.sec_packet_length = confirm.sec_packet.size();
return Result::accepted;
}
/** SN-DECAP over raw octets (clause 5.2.13). Result::invalid_argument on a length
* mismatch or when sec_packet is not a decodable EtsiTs103097Data; otherwise the
* confirm carries the entity's report (never Success, see SN_DECAP_confirm). */
inline Result SN_DECAP_request_submit(vanetza::security::SecurityEntity& entity, const SN_DECAP_request& primitive,
SN_DECAP_confirm& confirm) {
if (primitive.sec_packet_length != primitive.sec_packet.size() || primitive.sec_packet.empty())
return Result::invalid_argument;
vanetza::security::v3::SecuredMessage message;
if (!message.decode(primitive.sec_packet)) return Result::invalid_argument;
vanetza::security::SecuredMessage variant {std::move(message)};
auto result = entity.decapsulate_packet(vanetza::security::DecapRequest {vanetza::security::SecuredMessageView {variant}});
confirm = SN_DECAP_confirm {};
if (const auto* report = boost::get<vanetza::security::VerificationReport>(&result.report)) confirm.report = *report;
if (result.certificate_id) confirm.certificate_id = *result.certificate_id;
confirm.its_aid = result.its_aid;
confirm.permissions = std::move(result.permissions);
if (const auto* packet = boost::get<vanetza::CohesivePacket>(&result.plaintext_payload)) {
confirm.plaintext_packet.assign(packet->buffer().begin(), packet->buffer().end());
} else if (const auto* chunks = boost::get<vanetza::ChunkPacket>(&result.plaintext_payload)) {
for (auto layer : vanetza::osi_layer_range<vanetza::OsiLayer::Network, vanetza::OsiLayer::Application>()) {
const auto& part = (*chunks)[layer];
vanetza::ByteBuffer bytes;
part.convert(bytes);
confirm.plaintext_packet.insert(confirm.plaintext_packet.end(), bytes.begin(), bytes.end());
}
}
confirm.plaintext_packet_length = confirm.plaintext_packet.size();
return Result::accepted;
}
} // namespace vanetza_idf::SN_SAP
@@ -0,0 +1,173 @@
#pragma once
#include <vanetza_idf/access.hpp>
#include <vanetza_idf/id_change.hpp>
#include <vanetza/common/manual_runtime.hpp>
#include <vanetza/common/position_fix.hpp>
#include <vanetza/dcc/channel_load.hpp>
#include <vanetza/geonet/data_request.hpp>
#include <vanetza/geonet/data_indication.hpp>
#include <vanetza/geonet/data_confirm.hpp>
#include <vanetza/security/security_entity.hpp>
#include <functional>
#include <memory>
#include <optional>
namespace vanetza_idf {
enum class BtpType { a, b };
/** NF-SAP / BTP-DATA.request: TS 103 836-5-1 V2.1.1 Annex A.2
* and clauses 7, 8.2 (IF-NF-001); TS 102 723-11 V2.0.0 clause 5
* incorporates V1.1.1, including its clause 5.1 GeoAware SAP.
* All port numbers are HOST order. data.size() is the Length parameter.
* Omitted GN parameters inherit the configured MIB. No magic sentinel values.
*/
struct BtpRequest {
BtpType type = BtpType::b;
std::optional<std::uint16_t> source_port;
std::uint16_t destination_port = 0;
std::optional<std::uint16_t> destination_port_info;
vanetza::geonet::TransportType transport = vanetza::geonet::TransportType::SHB;
vanetza::geonet::DestinationVariant destination = nullptr;
vanetza::geonet::CommunicationProfile communication_profile = vanetza::geonet::CommunicationProfile::ITS_G5;
// The selected security entity implements this profile; ITS-AID, SSP and
// context information are SN-ENCAP inputs (TS 102 723-8 V2.0.0 clause 5 /
// V1.1.1 Table 24) that GN forwards unchanged (TS 103 836-4-1 V2.2.1
// Table 34, Annex J.2 "Security context information").
std::optional<std::uint32_t> security_profile;
vanetza::ItsAid its_aid = 0;
vanetza::ByteBuffer permissions;
vanetza::ByteBuffer security_context; // see security::context for the values this library defines
std::optional<vanetza::geonet::Lifetime> maximum_lifetime;
std::optional<vanetza::geonet::DataRequest::Repetition> repetition;
std::optional<unsigned> maximum_hop_limit;
vanetza::geonet::TrafficClass traffic_class;
vanetza::ByteBuffer data;
};
/** BTP-DATA.indication: TS 103 836-5-1 V2.1.1 Annex A.3/8.3 (IF-NF-002).
* gn retains the transport, destination, source vector, report, permissions,
* traffic class, remaining lifetime and hop limit. Owns the received payload.
*/
struct BtpIndication {
BtpType type;
std::optional<std::uint16_t> source_port;
std::uint16_t destination_port;
std::optional<std::uint16_t> destination_port_info;
vanetza::geonet::DataIndication gn;
std::optional<vanetza::ByteBuffer> certificate_id;
vanetza::ByteBuffer data;
};
/** GN-DATA.request: TS 103 836-4-1 V2.2.1 clause 9.3 N-SAP (EN 302 636-4-1
* clause 9.3 heritage), IF-GN-001. Unlike BtpRequest, data is the raw SDU
* with no assumed upper-layer header; the Common Header next_header is
* "Any". Only SHB and GBC are implemented; GUC, GAC and TSB are rejected
* as Result::unsupported (GAP-GN-001), matching Stack::request(BtpRequest).
*/
struct GnRequest {
vanetza::geonet::TransportType transport = vanetza::geonet::TransportType::SHB;
vanetza::geonet::DestinationVariant destination = nullptr; // Area for GBC, null for SHB
vanetza::geonet::CommunicationProfile communication_profile = vanetza::geonet::CommunicationProfile::ITS_G5;
vanetza::geonet::TrafficClass traffic_class;
std::optional<vanetza::geonet::Lifetime> maximum_lifetime;
std::optional<unsigned> maximum_hop_limit;
std::optional<vanetza::geonet::DataRequest::Repetition> repetition;
vanetza::ItsAid its_aid = 0;
vanetza::ByteBuffer permissions;
vanetza::ByteBuffer security_context; // TS 103 836-4-1 V2.2.1 Annex J.2 Security context information
vanetza::ByteBuffer data;
};
/** GN-DATA.indication for a packet whose Common Header next_header is "Any"
* (no upper protocol registered by BtpRequest's handler), TS 103 836-4-1
* V2.2.1 clause 9.3. gn retains the complete router indication.
*/
struct GnIndication {
vanetza::geonet::DataIndication gn;
vanetza::ByteBuffer data;
};
struct StackConfig {
vanetza::geonet::MIB mib;
std::size_t maximum_gnpdu = 4096;
std::uint32_t security_profile = 0;
AlDataRequest radio_parameters;
StackConfig();
};
/** Portable network/transport module for the NF-SAP entry point.
* The caller serializes all calls in ONE task/event loop, including reception,
* clock advancement and destruction. Access/SecurityEntity must outlive Stack.
* Timer source is injectable for host tests, HIL and ESP-IDF esp_timer.
* No FreeRTOS task, radio, BLE link, socket or filesystem is opened internally.
* This is a port of the upstream router; see docs/idf/conformance.md for R2 gaps.
*
* Identifier change (TS 103 836-4-1 V2.2.1 clause 10.2.1.4, anonymous address
* configuration): with itsGnLocalAddrConfMethod == Anonymous and an
* IdChangeService, the GN core subscribes at construction and unsubscribes at
* destruction. On COMMIT the GN address MID becomes the 48 least significant
* bits of the new HashedId8 (TS 102 940 V2.1.1 clause 6.5), with the I/G bit
* cleared (a source address is individual) and the U/L bit set; the link-layer
* source follows because every request takes it from the GN address. Between
* PREPARE and COMMIT the forwarding buffers are flushed and request() returns
* Result::identity_change_pending (TS 102 723-8 clause 6.3.1.3). The access
* adapter is not notified by the stack: it may subscribe to the same service
* (TS 102 723-7 analogy for the IN-SAP side is not defined here).
*/
class Stack {
public:
using Receive = std::function<void(BtpIndication)>;
using ReceiveGn = std::function<void(GnIndication)>;
using Report = std::function<void(Result)>;
/** id_change may be omitted: when security is a vanetza_idf::security::SecurityEntity
* its own identifier-change service is used. */
Stack(StackConfig, vanetza::ManualRuntime&, Access&,
vanetza::security::SecurityEntity* security = nullptr,
security::IdChangeService* id_change = nullptr);
~Stack();
Stack(const Stack&) = delete;
Stack& operator=(const Stack&) = delete;
Result request(BtpRequest);
Result request(GnRequest);
Result indicate(AlDataIndication);
Result update_position(const vanetza::PositionFix&);
Result advance(vanetza::Clock::time_point);
void on_receive(Receive);
void on_receive_gn(ReceiveGn);
void on_access_result(Report);
const StackConfig& config() const;
/// the identifier-change service this stack subscribed to, nullptr without one
security::IdChangeService* id_change();
vanetza::security::SecurityEntity* security_entity();
/// true between PREPARE and COMMIT (or ABORT) of an identifier change
bool identity_change_pending() const;
/// GeoNetworking address currently in use (MID follows the identifier change)
const vanetza::geonet::Address& address() const;
/** Managed address configuration (TS 103 836-4-1 V2.2.1 clause 10.2.1.3.3): the N&T
* management entity updates the GN address with an unsolicited CORE_MMT.response
* (MN_SAP::CORE_MMT_response_apply). Result::unsupported with Auto (10.2.1.2) or
* Anonymous (10.2.1.4) configuration, identity_change_pending during a change. */
Result set_address(const vanetza::geonet::Address&);
/** Release-2 DCC_NET (TS 103 836-4-2 V2.1.1 clauses 5, 6.2, 6.3.3, 7.2; SYS-DCC-003).
* Always active for this ITS-G5-only Stack: outgoing SHB packets carry a real DCC-MCO
* field (local/one-hop CBR, TX power) instead of the upstream NullDccFieldGenerator's
* reserved zero field, and every received SHB packet's DCC-MCO updates LocTEX-G5
* (already unconditional in the upstream router once itsGnIfType is ITS_G5, which this
* Stack requires). report_local_channel_load feeds DCC_NET's own CBR_G calculation
* (clause 5.3); it is independent of AccessStack::report_channel_load (DCC_ACC), which
* the caller should feed with global_channel_busy_ratio() when available, else the same
* local measurement (SYS-DCC-001: "consume Release-2 CBR_G when available, otherwise
* LCBR").
*/
void report_local_channel_load(vanetza::dcc::ChannelLoad);
/// EIRP of the station's own transmissions, for the outgoing DCC-MCO field's TX-power octet.
void report_tx_power(unsigned dbm);
/** CBR_G (TS 103 836-4-2 clause 5.3), once at least one 100 ms aggregation cycle has run;
* std::nullopt beforehand (no neighbour data collected yet). */
std::optional<vanetza::dcc::ChannelLoad> global_channel_busy_ratio() const;
private:
class Impl;
std::unique_ptr<Impl> impl_;
};
}
@@ -0,0 +1,27 @@
# Kconfig hides this backend on other SoCs; this independent guard also rejects
# forced/stale configurations. The private ABI is qualified per SDK version.
if(NOT IDF_TARGET STREQUAL "esp32c5")
message(FATAL_ERROR "Integrated ITS-G5 radio requires IDF_TARGET=esp32c5")
endif()
if(NOT IDF_VERSION_MAJOR EQUAL 6 OR NOT IDF_VERSION_MINOR EQUAL 0 OR NOT IDF_VERSION_PATCH EQUAL 2)
message(FATAL_ERROR "C5 private radio ABI is pinned to ESP-IDF 6.0.2")
endif()
set(_otm_source "${VIDF_PORT}/third_party/otm/main/tx_custom.c")
if(NOT EXISTS "${_otm_source}")
message(FATAL_ERROR "Initialize the OpenTrafficMap submodule before selecting the C5 radio")
endif()
file(READ "${_otm_source}" _otm)
string(REPLACE "\r\n" "\n" _otm "${_otm}")
string(SHA256 _otm_hash "${_otm}")
if(NOT _otm_hash STREQUAL "cb1dccfef96912ca59275e8a9102f41f56925b94a19d4a4629082aaeb779be1b")
message(FATAL_ERROR "OpenTrafficMap transmitter differs from the reviewed source revision")
endif()
# Keep the upstream checkout immutable. The sole logic patch preserves the
# actual driver submission result, which upstream discarded. ESP_OK still
# means submission, not independent observation of transmission on air.
string(REPLACE " ieee80211_post_hmac_tx(eb);"
" result = ieee80211_post_hmac_tx(eb);" _otm "${_otm}")
set(_otm_generated "${CMAKE_CURRENT_BINARY_DIR}/otm_tx_custom.c")
file(WRITE "${_otm_generated}" "#include <assert.h>\n#include <stddef.h>\n${_otm}\n_Static_assert(offsetof(x_ebuf_t, txdesc) == 0x38, \"ebuf txdesc offset\");\n_Static_assert(offsetof(x_eb_txdesc_t, rate) == 0x0c, \"txdesc rate offset\");\n")
target_sources(${VIDF_TARGET} PRIVATE "${VIDF_PORT}/src/c5_radio.cpp" "${_otm_generated}")
target_include_directories(${VIDF_TARGET} PRIVATE "${VIDF_PORT}/third_party/otm/main")
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,97 @@
#include <vanetza_idf/access.hpp>
#include <cmath>
#include <utility>
#if VIDF_NETWORK
#include <vanetza/access/data_rates.hpp>
#include <vanetza/dcc/limeric.hpp>
#include <vanetza/dcc/limeric_budget.hpp>
#include <vanetza/dcc/profile.hpp>
#include <vanetza/dcc/transmission.hpp>
#include <array>
#include <chrono>
#endif
namespace vanetza_idf {
Result validate(const AlDataRequest& request, std::size_t maximum) {
if (request.data.empty() || request.data.size() > maximum || request.priority > 7 ||
!std::isfinite(request.transmit_power_dbm) || request.bandwidth_mhz == 0)
return Result::invalid_argument;
return Result::accepted;
}
#if VIDF_NETWORK
namespace {
// EN 303 797 V2.1.1 clause 4.6.2's Ton limit (SYS-DCC-001 acceptance criterion 3); a single
// frame at or below this duration also cannot alone exceed the 3 % duty cycle at Toff>=25 ms.
constexpr vanetza::Clock::duration max_ton = std::chrono::milliseconds(4);
// Index by OfdmMcs; mirrors the rate table in C5Radio::request/transmit_burst.
constexpr std::array<const vanetza::access::DataRateG5*, 8> g5_rates {
&vanetza::access::G5_3Mbps, &vanetza::access::G5_4dot5Mbps, &vanetza::access::G5_6Mbps,
&vanetza::access::G5_9Mbps, &vanetza::access::G5_12Mbps, &vanetza::access::G5_18bps,
&vanetza::access::G5_24Mbps, &vanetza::access::G5_27Mbps,
};
// Airtime-only use of Transmission::channel_occupancy(); profile is immaterial here since no
// FlowControl queueing is involved, only the data-rate/length formula.
vanetza::Clock::duration request_airtime(const AlDataRequest& request) {
vanetza::dcc::TransmissionLite transmission(vanetza::dcc::Profile::DP2, request.data.size());
const auto index = static_cast<unsigned>(request.mcs);
transmission.m_data_rate = index < g5_rates.size() ? g5_rates[index] : nullptr;
return transmission.channel_occupancy();
}
// TS 102 687 V1.2.1 Table 3 defaults, with cbr_target overridden to the project's Release-2
// value (SYS-DCC-001/002/003; see AccessStack::enable_dcc's docstring for why).
vanetza::dcc::Limeric::Parameters limeric_parameters(vanetza::dcc::ChannelLoad cbr_target) {
vanetza::dcc::Limeric::Parameters parameters;
parameters.cbr_target = cbr_target;
return parameters;
}
} // namespace
class AccessStack::Dcc {
public:
Dcc(vanetza::Runtime& rt, vanetza::dcc::ChannelLoad target) :
limeric(rt, limeric_parameters(target)),
budget(limeric, rt)
{
// TS 102 687 V1.2.1 Annex B: the gate-opening time depends on the current permitted
// duty cycle, so it must be recalculated whenever the Adaptive approach updates delta.
limeric.on_duty_cycle_change = [this](const vanetza::dcc::Limeric*, vanetza::Clock::time_point) {
budget.update();
};
}
vanetza::dcc::Limeric limeric;
vanetza::dcc::LimericBudget budget;
};
void AccessStack::enable_dcc(vanetza::Runtime& runtime, vanetza::dcc::ChannelLoad cbr_target) {
dcc_ = std::make_unique<Dcc>(runtime, cbr_target);
}
void AccessStack::report_channel_load(vanetza::dcc::ChannelLoad load) {
if (dcc_) dcc_->limeric.update_cbr(load);
}
vanetza::UnitInterval AccessStack::permitted_duty_cycle() const {
return dcc_ ? dcc_->limeric.permitted_duty_cycle() : vanetza::UnitInterval(1.0);
}
#endif
AccessStack::AccessStack(Access& access, std::size_t maximum) :
access_(access), maximum_gnpdu_(maximum) {}
AccessStack::~AccessStack() = default;
Result AccessStack::request(AlDataRequest request) {
auto result = validate(request, maximum_gnpdu_);
if (result != Result::accepted) return result;
#if VIDF_NETWORK
if (dcc_) {
const auto ton = request_airtime(request);
if (ton <= vanetza::Clock::duration::zero() || ton > max_ton) return Result::invalid_argument;
if (dcc_->budget.delay() > vanetza::Clock::duration::zero()) return Result::resource_limit;
result = access_.request(std::move(request));
if (result == Result::accepted) dcc_->budget.notify(ton);
return result;
}
#endif
return access_.request(std::move(request));
}
}
@@ -0,0 +1,233 @@
#include <vanetza_idf/backend_mbedtls.hpp>
#include <vanetza_idf/ecc.hpp>
#include <vanetza/security/sha.hpp>
#include <psa/crypto.h>
#include <algorithm>
#include <deque>
#include <stdexcept>
namespace vanetza_idf {
using namespace vanetza::security;
namespace {
psa_ecc_family_t family(KeyType type) {
switch (type) {
case KeyType::NistP256: return PSA_ECC_FAMILY_SECP_R1;
case KeyType::BrainpoolP256r1:
case KeyType::BrainpoolP384r1: return PSA_ECC_FAMILY_BRAINPOOL_P_R1;
default: throw std::runtime_error("unsupported key type");
}
}
std::size_t curve_bits(KeyType type) { return key_length(type) * 8; }
struct ScopedKey {
mbedtls_svc_key_id_t id = 0;
~ScopedKey() { if (id) psa_destroy_key(id); }
};
// SEC 1 uncompressed encoding of a generic public key, recovering y when compressed.
vanetza::ByteBuffer public_key_octets(const PublicKey& key) {
Uncompressed point;
switch (key.compression) {
case KeyCompression::NoCompression:
point.x = key.x; point.y = key.y;
break;
case KeyCompression::Y0:
case KeyCompression::Y1: {
auto recovered = ecc::decompress(key.type, key.x, key.compression == KeyCompression::Y1);
if (!recovered) throw std::runtime_error("public key is not on the curve");
point = std::move(*recovered);
break;
}
default: throw std::runtime_error("unsupported point compression");
}
if (point.x.size() != key_length(key.type) || point.y.size() != key_length(key.type))
throw std::runtime_error("public key coordinate length");
return ecc::encode_uncompressed(point);
}
// The digest handed in is the SHA-256/SHA-384 output IEEE Std 1609.2 clause 5.3.1 prescribes
// for the curve, so the operation names that hash: implementations that accelerate ECDSA
// (ESP-IDF's PSA driver for the ESP32-C5 ECDSA peripheral, CONFIG_MBEDTLS_HARDWARE_ECDSA_VERIFY)
// only take PSA_ALG_ECDSA(PSA_ALG_SHA_256/384), never PSA_ALG_ECDSA_ANY, which falls back to
// software. Keys are imported with the wildcard policy so either form is permitted.
psa_algorithm_t ecdsa_algorithm(std::size_t digest_octets) {
switch (digest_octets) {
case 32: return PSA_ALG_ECDSA(PSA_ALG_SHA_256);
case 48: return PSA_ALG_ECDSA(PSA_ALG_SHA_384);
default: return PSA_ALG_ECDSA_ANY;
}
}
mbedtls_svc_key_id_t import_public(KeyType type, const vanetza::ByteBuffer& sec1) {
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_type(&attributes, PSA_KEY_TYPE_ECC_PUBLIC_KEY(family(type)));
psa_set_key_bits(&attributes, curve_bits(type));
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_VERIFY_HASH);
psa_set_key_algorithm(&attributes, PSA_ALG_ECDSA(PSA_ALG_ANY_HASH));
mbedtls_svc_key_id_t id = 0;
if (psa_import_key(&attributes, sec1.data(), sec1.size(), &id) != PSA_SUCCESS)
throw std::runtime_error("PSA public key import failed");
return id;
}
vanetza::ByteBuffer raw_signature(mbedtls_svc_key_id_t key, KeyType type, const vanetza::ByteBuffer& digest) {
vanetza::ByteBuffer signature(2 * key_length(type));
std::size_t length = 0;
const auto status = psa_sign_hash(key, ecdsa_algorithm(digest.size()), digest.data(), digest.size(),
signature.data(), signature.size(), &length);
if (status != PSA_SUCCESS || length != signature.size()) throw std::runtime_error("PSA ECDSA signing failed");
return signature;
}
bool verify_raw(KeyType type, const vanetza::ByteBuffer& sec1, const vanetza::ByteBuffer& digest,
const vanetza::ByteBuffer& r, const vanetza::ByteBuffer& s) {
const auto n = key_length(type);
if (r.size() != n || s.size() != n) return false;
vanetza::ByteBuffer signature;
signature.reserve(2 * n);
signature.insert(signature.end(), r.begin(), r.end());
signature.insert(signature.end(), s.begin(), s.end());
ScopedKey key;
try { key.id = import_public(type, sec1); } catch (const std::runtime_error&) { return false; }
return psa_verify_hash(key.id, ecdsa_algorithm(digest.size()), digest.data(), digest.size(),
signature.data(), signature.size()) == PSA_SUCCESS;
}
} // namespace
class BackendMbedTls::Impl {
public:
struct Entry { KeyType type; vanetza::ByteBuffer secret; mbedtls_svc_key_id_t id; };
std::deque<Entry> cache;
std::size_t capacity = 4;
~Impl() { for (auto& entry : cache) psa_destroy_key(entry.id); }
mbedtls_svc_key_id_t signing_key(KeyType type, const vanetza::ByteBuffer& secret) {
if (secret.size() != key_length(type)) throw std::runtime_error("private key length");
for (auto it = cache.begin(); it != cache.end(); ++it) {
if (it->type == type && it->secret == secret) {
Entry hit = std::move(*it);
cache.erase(it);
cache.push_front(std::move(hit)); // most recently used first
return cache.front().id;
}
}
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_type(&attributes, PSA_KEY_TYPE_ECC_KEY_PAIR(family(type)));
psa_set_key_bits(&attributes, curve_bits(type));
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_SIGN_HASH);
psa_set_key_algorithm(&attributes, PSA_ALG_ECDSA(PSA_ALG_ANY_HASH));
mbedtls_svc_key_id_t id = 0;
if (psa_import_key(&attributes, secret.data(), secret.size(), &id) != PSA_SUCCESS)
throw std::runtime_error("PSA private key import failed");
while (cache.size() >= capacity) {
psa_destroy_key(cache.back().id);
cache.pop_back();
}
cache.push_front(Entry {type, secret, id});
return id;
}
};
BackendMbedTls::BackendMbedTls() : impl_(std::make_unique<Impl>()) {
if (psa_crypto_init() != PSA_SUCCESS) throw std::runtime_error("PSA crypto initialisation failed");
}
BackendMbedTls::~BackendMbedTls() = default;
void BackendMbedTls::set_key_cache_size(std::size_t size) { impl_->capacity = std::max<std::size_t>(1, size); }
EcdsaSignature BackendMbedTls::sign_data(const ecdsa256::PrivateKey& key, const vanetza::ByteBuffer& data) {
const auto digest = calculate_sha256_digest(data.data(), data.size());
const vanetza::ByteBuffer secret(key.key.begin(), key.key.end());
const auto id = impl_->signing_key(KeyType::NistP256, secret);
auto raw = raw_signature(id, KeyType::NistP256, vanetza::ByteBuffer(digest.begin(), digest.end()));
EcdsaSignature signature;
X_Coordinate_Only r;
r.x.assign(raw.begin(), raw.begin() + 32);
signature.R = std::move(r);
signature.s.assign(raw.begin() + 32, raw.end());
return signature;
}
Signature BackendMbedTls::sign_digest(const PrivateKey& key, const vanetza::ByteBuffer& digest) {
const auto id = impl_->signing_key(key.type, key.key);
auto raw = raw_signature(id, key.type, digest);
const auto n = key_length(key.type);
Signature signature;
signature.type = key.type;
signature.r.assign(raw.begin(), raw.begin() + n);
signature.s.assign(raw.begin() + n, raw.end());
return signature;
}
bool BackendMbedTls::verify_data(const ecdsa256::PublicKey& key, const vanetza::ByteBuffer& data,
const EcdsaSignature& signature) {
const auto digest = calculate_sha256_digest(data.data(), data.size());
Uncompressed point;
point.x.assign(key.x.begin(), key.x.end());
point.y.assign(key.y.begin(), key.y.end());
return verify_raw(KeyType::NistP256, ecc::encode_uncompressed(point),
vanetza::ByteBuffer(digest.begin(), digest.end()),
convert_for_signing(signature.R), signature.s);
}
bool BackendMbedTls::verify_digest(const PublicKey& key, const vanetza::ByteBuffer& digest, const Signature& signature) {
if (key.type != signature.type) return false;
try {
return verify_raw(key.type, public_key_octets(key), digest, signature.r, signature.s);
} catch (const std::runtime_error&) {
return false;
}
}
boost::optional<Uncompressed> BackendMbedTls::decompress_point(const EccPoint& ecc_point) {
struct Visitor : boost::static_visitor<boost::optional<Uncompressed>> {
boost::optional<Uncompressed> operator()(const X_Coordinate_Only&) const { return boost::none; }
boost::optional<Uncompressed> operator()(const Compressed_Lsb_Y_0& p) const {
return ecc::decompress(KeyType::NistP256, p.x, false);
}
boost::optional<Uncompressed> operator()(const Compressed_Lsb_Y_1& p) const {
return ecc::decompress(KeyType::NistP256, p.x, true);
}
boost::optional<Uncompressed> operator()(const Uncompressed& p) const { return p; }
};
Visitor visitor;
return boost::apply_visitor(visitor, ecc_point);
}
vanetza::ByteBuffer BackendMbedTls::calculate_hash(HashAlgorithm algorithm, const vanetza::ByteBuffer& data) {
psa_algorithm_t alg;
std::size_t size;
switch (algorithm) {
case HashAlgorithm::SHA256: alg = PSA_ALG_SHA_256; size = 32; break;
case HashAlgorithm::SHA384: alg = PSA_ALG_SHA_384; size = 48; break;
default: return {};
}
vanetza::ByteBuffer digest(size);
std::size_t written = 0;
if (psa_hash_compute(alg, data.data(), data.size(), digest.data(), digest.size(), &written) != PSA_SUCCESS ||
written != size) throw std::runtime_error("PSA hash failed");
return digest;
}
ecdsa256::KeyPair BackendMbedTls::generate_key_pair() {
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_type(&attributes, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1));
psa_set_key_bits(&attributes, 256);
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_EXPORT);
psa_set_key_algorithm(&attributes, PSA_ALG_ECDSA_ANY);
ScopedKey key;
if (psa_generate_key(&attributes, &key.id) != PSA_SUCCESS) throw std::runtime_error("PSA key generation failed");
ecdsa256::KeyPair pair;
std::size_t length = 0;
if (psa_export_key(key.id, pair.private_key.key.data(), pair.private_key.key.size(), &length) != PSA_SUCCESS ||
length != pair.private_key.key.size()) throw std::runtime_error("PSA private key export failed");
std::array<std::uint8_t, 65> sec1 {};
if (psa_export_public_key(key.id, sec1.data(), sec1.size(), &length) != PSA_SUCCESS || length != sec1.size() ||
sec1[0] != 0x04) throw std::runtime_error("PSA public key export failed");
std::copy(sec1.begin() + 1, sec1.begin() + 33, pair.public_key.x.begin());
std::copy(sec1.begin() + 33, sec1.end(), pair.public_key.y.begin());
return pair;
}
} // namespace vanetza_idf
@@ -0,0 +1,165 @@
#include <vanetza_idf/c5_radio.hpp>
#include <vanetza_idf/its_g5_frame.hpp>
#include <esp_event.h>
#include <esp_wifi.h>
#include <hal/modem_syscon_ll.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <algorithm>
#include <atomic>
#include <cmath>
#include <cstring>
#include <mutex>
extern "C" {
void phy_11p_set(int, int);
void phy_change_channel(int, int, int, int);
esp_err_t esp_wifi_80211_tx_custom(wifi_interface_t, const void*, int32_t, bool,
wifi_tx_rate_config_t*, wifi_band_t, wifi_bandwidth_t);
}
namespace vanetza_idf {
class C5Radio::Impl {
public:
struct Raw {
std::uint16_t length;
std::int8_t rssi;
std::uint32_t timestamp;
std::uint8_t bytes[2346];
};
C5RadioConfig config;
QueueHandle_t queue = nullptr;
bool initialized = false, started = false, own_event_loop = false;
std::uint16_t sequence = 0;
std::atomic<std::uint32_t> dropped {0};
static Impl* active;
static std::mutex callback_mutex;
explicit Impl(C5RadioConfig c) : config(c) {}
static void receive(void* buffer, wifi_promiscuous_pkt_type_t type) {
if (!buffer || type != WIFI_PKT_DATA) return;
const auto* packet = static_cast<const wifi_promiscuous_pkt_t*>(buffer);
if (packet->rx_ctrl.rx_state != 0) return;
// esp_wifi_he_types.h documents sig_len on this chip as "the length of the
// reception MPDU", not "MPDU + FCS" as older-chip ESP-IDF documentation
// states; measured on real hardware, it is consistently 4 bytes longer than
// the actual frame, and those 4 trailing bytes are a fixed, content-independent
// value (00 00 99 00), not a real, software-recoverable FCS. This promiscuous
// filter never sets WIFI_PROMIS_FILTER_MASK_FCSFAIL, so the frame itself has
// already passed a real hardware FCS check by the time it reaches this callback
// -- see ports/esp_idf/tools/radio_pair.py's docstring for the full evidence.
const auto length = packet->rx_ctrl.sig_len;
std::lock_guard<std::mutex> lock(callback_mutex);
if (!active || !active->queue) return;
if (length < 38 || length > sizeof(Raw::bytes)) { ++active->dropped; return; }
Raw raw {};
raw.length = length; raw.rssi = packet->rx_ctrl.rssi;
raw.timestamp = packet->rx_ctrl.timestamp;
std::memcpy(raw.bytes, packet->payload, length);
if (xQueueSend(active->queue, &raw, 0) != pdTRUE) ++active->dropped;
}
};
C5Radio::Impl* C5Radio::Impl::active = nullptr;
std::mutex C5Radio::Impl::callback_mutex;
C5Radio::C5Radio(C5RadioConfig c) : impl_(std::make_unique<Impl>(c)) {}
C5Radio::~C5Radio() { stop(); }
esp_err_t C5Radio::start() {
auto& p = *impl_;
const auto& c = p.config;
if (p.initialized) return ESP_ERR_INVALID_STATE;
if (c.channel_number < 172 || c.channel_number > 184 || c.channel_number % 2 ||
!std::isfinite(c.transmit_power_dbm) || c.transmit_power_dbm < 2 || c.transmit_power_dbm > 20 ||
std::floor(c.transmit_power_dbm * 4) != c.transmit_power_dbm * 4 ||
c.receive_queue_length == 0 || c.receive_queue_length > 32) return ESP_ERR_INVALID_ARG;
{
std::lock_guard<std::mutex> lock(Impl::callback_mutex);
if (Impl::active) return ESP_ERR_INVALID_STATE;
p.queue = xQueueCreate(c.receive_queue_length, sizeof(Impl::Raw));
if (!p.queue) return ESP_ERR_NO_MEM;
Impl::active = &p;
}
auto result = esp_event_loop_create_default();
p.own_event_loop = result == ESP_OK;
if (result != ESP_OK && result != ESP_ERR_INVALID_STATE) { stop(); return result; }
// OpenTrafficMap main/main.c and cmd_sniffer.c establish the FE clock and
// NON_NGV_10 PHY mode. These private calls are SDK-specific, not ETSI SAPs.
modem_syscon_ll_enable_fe_40m_clock(&MODEM_SYSCON, true);
wifi_init_config_t wifi = WIFI_INIT_CONFIG_DEFAULT();
wifi.nvs_enable = 0;
result = esp_wifi_init(&wifi);
if (result != ESP_OK) { stop(); return result; }
p.initialized = true;
auto attempt = [&](esp_err_t r) { if (result == ESP_OK) result = r; };
attempt(esp_wifi_set_storage(WIFI_STORAGE_RAM));
attempt(esp_wifi_set_mode(WIFI_MODE_STA));
if (result == ESP_OK) { result = esp_wifi_start(); p.started = result == ESP_OK; }
if (result != ESP_OK) { stop(); return result; }
attempt(esp_wifi_set_band_mode(WIFI_BAND_MODE_5G_ONLY));
attempt(esp_wifi_set_ps(WIFI_PS_NONE));
attempt(esp_wifi_set_max_tx_power(static_cast<std::int8_t>(c.transmit_power_dbm * 4)));
wifi_promiscuous_filter_t filter {};
filter.filter_mask = WIFI_PROMIS_FILTER_MASK_DATA;
attempt(esp_wifi_set_promiscuous_filter(&filter));
attempt(esp_wifi_set_promiscuous_rx_cb(Impl::receive));
attempt(esp_wifi_set_promiscuous(true));
if (result != ESP_OK) { stop(); return result; }
phy_11p_set(1, 0); // 10 MHz; never infer PHY width from WIFI_BW20 below.
phy_change_channel(5000 + 5 * c.channel_number, 1, 0, 0);
return ESP_OK;
}
void C5Radio::stop() {
if (!impl_) return;
auto& p = *impl_;
if (p.started) esp_wifi_set_promiscuous(false);
{
std::lock_guard<std::mutex> lock(Impl::callback_mutex);
if (Impl::active == &p) Impl::active = nullptr;
if (p.queue) { vQueueDelete(p.queue); p.queue = nullptr; }
}
if (p.started) esp_wifi_stop();
if (p.initialized) esp_wifi_deinit();
if (p.own_event_loop) esp_event_loop_delete_default();
p.started = p.initialized = p.own_event_loop = false;
}
Result C5Radio::request(AlDataRequest request) {
auto& p = *impl_;
if (!p.started) return Result::rejected;
if (!p.config.laboratory_transmission) return Result::unsupported;
// Reject controls this fixed-channel backend cannot honour. In particular,
// do not silently reinterpret an AL_DATA bandwidth or transceiver mode.
if (request.bandwidth_mhz != 10 || request.channel_number != p.config.channel_number ||
request.transceiver_id != 0 || request.transceiver_mode || request.datastream_id ||
request.transmit_power_dbm != p.config.transmit_power_dbm) return Result::unsupported;
constexpr wifi_phy_rate_t rates[] = {WIFI_PHY_RATE_6M, WIFI_PHY_RATE_9M, WIFI_PHY_RATE_12M,
WIFI_PHY_RATE_18M, WIFI_PHY_RATE_24M, WIFI_PHY_RATE_36M, WIFI_PHY_RATE_48M, WIFI_PHY_RATE_54M};
const auto index = static_cast<unsigned>(request.mcs);
if (index >= std::size(rates)) return Result::invalid_argument;
vanetza::ByteBuffer bytes;
const auto encoded = its_g5::encode_frame(request, p.sequence, bytes);
if (encoded != Result::accepted) return encoded;
p.sequence = (p.sequence + 1) & 4095;
wifi_tx_rate_config_t rate {};
rate.phymode = WIFI_PHY_MODE_11A; rate.rate = rates[index];
// Legacy OFDM rate identifiers are halved by the 10 MHz PHY mode.
// The driver consumes the frame synchronously into its own ebuf.
const auto result = esp_wifi_80211_tx_custom(WIFI_IF_STA, bytes.data(), bytes.size(), false,
&rate, WIFI_BAND_5G, WIFI_BW20);
return result == ESP_OK ? Result::accepted : result == ESP_ERR_NO_MEM ? Result::resource_limit : Result::rejected;
}
void C5Radio::poll(const Receive& receive, const Capture& capture) {
auto& p = *impl_;
if (!p.queue) return;
Impl::Raw raw {};
// Bound work per poll even if the radio continuously fills the queue.
for (unsigned i = 0; i < p.config.receive_queue_length && xQueueReceive(p.queue, &raw, 0) == pdTRUE; ++i) {
if (capture) capture(vanetza::ByteBuffer(raw.bytes, raw.bytes + raw.length), raw.rssi, raw.timestamp);
AlDataIndication ind;
if (its_g5::decode_frame(raw.bytes, raw.length, true, ind) != Result::accepted) continue;
ind.channel_number = p.config.channel_number;
ind.received_power_dbm = raw.rssi;
// The SDK does not provide channel-busy time here. CBR remains absent;
// counting received packets would not be a valid CBR measurement.
if (receive) receive(std::move(ind));
}
}
std::uint32_t C5Radio::dropped_frames() const { return impl_->dropped.load(); }
}
@@ -0,0 +1,125 @@
#include <vanetza_idf/credentials.hpp>
#include <cstdio>
#include <fstream>
#include <iterator>
namespace vanetza_idf::security {
using vanetza::security::KeyType;
namespace {
constexpr std::uint8_t magic[4] = {'V', 'C', 'R', '1'};
enum Record : std::uint8_t { root = 1, authority = 2, ticket = 3, ticket_key = 4 };
std::uint8_t curve_code(KeyType type) {
switch (type) {
case KeyType::NistP256: return 1;
case KeyType::BrainpoolP256r1: return 2;
case KeyType::BrainpoolP384r1: return 3;
default: return 0;
}
}
KeyType curve_type(std::uint8_t code) {
switch (code) {
case 1: return KeyType::NistP256;
case 2: return KeyType::BrainpoolP256r1;
case 3: return KeyType::BrainpoolP384r1;
default: return KeyType::Unspecified;
}
}
void put(ByteBuffer& out, std::uint8_t type, const ByteBuffer& payload) {
out.push_back(type);
out.push_back(static_cast<std::uint8_t>(payload.size() >> 8));
out.push_back(static_cast<std::uint8_t>(payload.size()));
out.insert(out.end(), payload.begin(), payload.end());
}
} // namespace
ByteBuffer encode(const Credentials& credentials) {
ByteBuffer out(std::begin(magic), std::end(magic));
for (const auto& coer : credentials.roots) put(out, Record::root, coer);
for (const auto& coer : credentials.authorities) put(out, Record::authority, coer);
for (const auto& t : credentials.tickets) {
put(out, Record::ticket, t.certificate);
ByteBuffer key {curve_code(t.key.type)};
key.insert(key.end(), t.key.key.begin(), t.key.key.end());
put(out, Record::ticket_key, key);
}
return out;
}
bool decode(const ByteBuffer& bundle, Credentials& out) {
if (bundle.size() < sizeof(magic) || !std::equal(std::begin(magic), std::end(magic), bundle.begin())) return false;
Credentials parsed;
bool key_pending = false; // a ticket certificate awaits its key record
std::size_t at = sizeof(magic);
while (at < bundle.size()) {
if (bundle.size() - at < 3) return false;
const std::uint8_t type = bundle[at];
const std::size_t length = (static_cast<std::size_t>(bundle[at + 1]) << 8) | bundle[at + 2];
at += 3;
if (bundle.size() - at < length || length == 0) return false;
ByteBuffer payload(bundle.begin() + at, bundle.begin() + at + length);
at += length;
if (key_pending && type != Record::ticket_key) return false;
switch (type) {
case Record::root: parsed.roots.push_back(std::move(payload)); break;
case Record::authority: parsed.authorities.push_back(std::move(payload)); break;
case Record::ticket:
parsed.tickets.push_back(Credentials::Ticket {std::move(payload), PrivateKey {}});
key_pending = true;
break;
case Record::ticket_key: {
if (!key_pending) return false;
const KeyType curve = curve_type(payload[0]);
if (curve == KeyType::Unspecified || payload.size() - 1 != vanetza::security::key_length(curve)) return false;
auto& key = parsed.tickets.back().key;
key.type = curve;
key.key.assign(payload.begin() + 1, payload.end());
key_pending = false;
break;
}
default:
return false;
}
}
if (key_pending) return false;
out = std::move(parsed);
return true;
}
ApplyReport apply(const Credentials& credentials, TrustConfiguration& trust, CertificatePool& pool) {
ApplyReport report;
for (const auto& coer : credentials.roots) {
if ((report.result = trust.add_root(coer)) != Result::accepted) return report;
++report.roots;
}
for (const auto& coer : credentials.authorities) {
if ((report.result = trust.add_authority(coer)) != Result::accepted) return report;
++report.authorities;
}
for (const auto& t : credentials.tickets) {
if ((report.result = pool.add(t.certificate, t.key)) != Result::accepted) return report;
++report.tickets;
}
return report;
}
Result FileCredentialStore::save(const Credentials& credentials) {
const ByteBuffer bundle = encode(credentials);
std::ofstream out(path_, std::ios::binary | std::ios::trunc);
out.write(reinterpret_cast<const char*>(bundle.data()), bundle.size());
return out ? Result::accepted : Result::rejected;
}
Result FileCredentialStore::load(Credentials& credentials) {
std::ifstream in(path_, std::ios::binary);
if (!in) return Result::rejected;
const ByteBuffer bundle(std::istreambuf_iterator<char>(in), {});
return decode(bundle, credentials) ? Result::accepted : Result::invalid_argument;
}
Result FileCredentialStore::erase() {
return std::remove(path_.c_str()) == 0 ? Result::accepted : Result::rejected;
}
} // namespace vanetza_idf::security
@@ -0,0 +1,82 @@
#include <vanetza_idf/ecc.hpp>
#include <boost/multiprecision/cpp_int.hpp>
#include <algorithm>
#include <iterator>
namespace vanetza_idf::ecc {
using namespace vanetza::security;
namespace {
// Fixed width, no heap: a 384-bit product needs 768 bits before reduction.
using Int = boost::multiprecision::number<boost::multiprecision::cpp_int_backend<
1024, 1024, boost::multiprecision::unsigned_magnitude, boost::multiprecision::unchecked, void>>;
// FIPS 186-4 D.1.2.3 (P-256), RFC 5639 3.4 (brainpoolP256r1) and 3.6 (brainpoolP384r1).
// The host regression compares every constant with OpenSSL's built-in groups.
constexpr CurveParameters curves[] = {
{KeyType::NistP256, 32,
"FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF",
"FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC",
"5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B"},
{KeyType::BrainpoolP256r1, 32,
"A9FB57DBA1EEA9BC3E660A909D838D726E3BF623D52620282013481D1F6E5377",
"7D5A0975FC2C3057EEF67530417AFFE7FB8055C126DC5C6CE94A4B44F330B5D9",
"26DC5C6CE94A4B44F330B5D9BBD77CBF958416295CF7E1CE6BCCDC18FF8C07B6"},
{KeyType::BrainpoolP384r1, 48,
"8CB91E82A3386D280F5D6F7E50E641DF152F7109ED5456B412B1DA197FB71123ACD3A729901D1A71874700133107EC53",
"7BC382C63D8C150C3C72080ACE05AFA0C2BEA28E4FB22787139165EFBA91F90F8AA5814A503AD4EB04A8C7DD22CE2826",
"04A8C7DD22CE28268B39B55416F0447C2FB77DE107DCD2A62E880EA53EEB62D57CB4390295DBC9943AB78696FA504C11"},
};
Int from_hex(const char* hex) { return Int(std::string("0x") + hex); }
Int from_bytes(const vanetza::ByteBuffer& bytes) {
Int value;
boost::multiprecision::import_bits(value, bytes.begin(), bytes.end(), 8, true);
return value;
}
vanetza::ByteBuffer to_bytes(const Int& value, std::size_t octets) {
vanetza::ByteBuffer out;
boost::multiprecision::export_bits(value, std::back_inserter(out), 8, true);
if (out.size() > octets) return {};
out.insert(out.begin(), octets - out.size(), 0);
return out;
}
} // namespace
const CurveParameters* curve(KeyType type) {
for (const auto& c : curves) if (c.type == type) return &c;
return nullptr;
}
boost::optional<Uncompressed> decompress(KeyType type, const vanetza::ByteBuffer& x_bytes, bool y_odd) {
const auto* c = curve(type);
if (!c || x_bytes.size() != c->octets) return boost::none;
const Int p = from_hex(c->p), a = from_hex(c->a), b = from_hex(c->b);
const Int x = from_bytes(x_bytes);
if (x >= p) return boost::none;
// rhs = x^3 + a*x + b (mod p)
Int rhs = (x * x) % p;
rhs = (rhs * x) % p;
rhs = (rhs + (a * x) % p) % p;
rhs = (rhs + b) % p;
// p = 3 (mod 4): sqrt(n) = n^((p+1)/4) (mod p) when n is a quadratic residue.
Int y = boost::multiprecision::powm(rhs, (p + 1) / 4, p);
if ((y * y) % p != rhs) return boost::none; // x is not on the curve
if (static_cast<bool>(y & 1) != y_odd) y = p - y;
Uncompressed point;
point.x = x_bytes;
point.y = to_bytes(y, c->octets);
if (point.y.empty()) return boost::none;
return point;
}
vanetza::ByteBuffer encode_uncompressed(const Uncompressed& point) {
vanetza::ByteBuffer out;
out.reserve(1 + point.x.size() + point.y.size());
out.push_back(0x04);
out.insert(out.end(), point.x.begin(), point.x.end());
out.insert(out.end(), point.y.begin(), point.y.end());
return out;
}
} // namespace vanetza_idf::ecc
@@ -0,0 +1,135 @@
#include <vanetza_idf/ecies_mbedtls.hpp>
#include <vanetza_idf/ecc.hpp>
#include <psa/crypto.h>
#include <stdexcept>
namespace vanetza_idf::pki {
using namespace vanetza::security;
namespace {
psa_ecc_family_t family_of(KeyType type) {
switch (type) {
case KeyType::NistP256: return PSA_ECC_FAMILY_SECP_R1;
case KeyType::BrainpoolP256r1:
case KeyType::BrainpoolP384r1: return PSA_ECC_FAMILY_BRAINPOOL_P_R1;
default: throw std::runtime_error("unsupported key type");
}
}
struct ScopedKey {
mbedtls_svc_key_id_t id = 0;
~ScopedKey() { if (id) psa_destroy_key(id); }
};
mbedtls_svc_key_id_t import_raw(psa_key_type_t type, std::size_t bits, psa_key_usage_t usage, psa_algorithm_t alg,
const std::uint8_t* data, std::size_t length) {
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_type(&attributes, type);
if (bits) psa_set_key_bits(&attributes, bits);
psa_set_key_usage_flags(&attributes, usage);
psa_set_key_algorithm(&attributes, alg);
mbedtls_svc_key_id_t id = 0;
if (psa_import_key(&attributes, data, length, &id) != PSA_SUCCESS) throw std::runtime_error("PSA key import failed");
return id;
}
} // namespace
EciesMbedTls::EciesMbedTls() {
if (psa_crypto_init() != PSA_SUCCESS) throw std::runtime_error("PSA crypto initialisation failed");
}
KeyPair EciesMbedTls::generate_key(KeyType type) {
const std::size_t n = key_length(type);
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_type(&attributes, PSA_KEY_TYPE_ECC_KEY_PAIR(family_of(type)));
psa_set_key_bits(&attributes, n * 8);
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_EXPORT);
psa_set_key_algorithm(&attributes, PSA_ALG_ECDH);
ScopedKey key;
if (psa_generate_key(&attributes, &key.id) != PSA_SUCCESS) throw std::runtime_error("PSA key generation failed");
KeyPair pair;
pair.priv.type = type;
pair.priv.key.resize(n);
std::size_t length = 0;
if (psa_export_key(key.id, pair.priv.key.data(), n, &length) != PSA_SUCCESS || length != n)
throw std::runtime_error("PSA private key export failed");
ByteBuffer sec1(1 + 2 * n);
if (psa_export_public_key(key.id, sec1.data(), sec1.size(), &length) != PSA_SUCCESS || length != sec1.size() || sec1[0] != 0x04)
throw std::runtime_error("PSA public key export failed");
pair.pub.type = type;
pair.pub.compression = KeyCompression::NoCompression;
pair.pub.x.assign(sec1.begin() + 1, sec1.begin() + 1 + n);
pair.pub.y.assign(sec1.begin() + 1 + n, sec1.end());
return pair;
}
std::optional<ByteBuffer> EciesMbedTls::ecdh_x(const PrivateKey& own, const PublicKey& peer) {
if (own.type != peer.type || peer.compression != KeyCompression::NoCompression) return std::nullopt;
const std::size_t n = key_length(own.type);
if (own.key.size() != n || peer.x.size() != n || peer.y.size() != n) return std::nullopt;
try {
ScopedKey key;
key.id = import_raw(PSA_KEY_TYPE_ECC_KEY_PAIR(family_of(own.type)), n * 8, PSA_KEY_USAGE_DERIVE, PSA_ALG_ECDH,
own.key.data(), own.key.size());
Uncompressed point;
point.x = peer.x; point.y = peer.y;
const ByteBuffer sec1 = ecc::encode_uncompressed(point);
ByteBuffer secret(n);
std::size_t length = 0;
// PSA raw ECDH output is the x coordinate of the shared point (PSA Crypto API 1.1, PSA_ALG_ECDH)
if (psa_raw_key_agreement(PSA_ALG_ECDH, key.id, sec1.data(), sec1.size(), secret.data(), secret.size(), &length) != PSA_SUCCESS ||
length != n) return std::nullopt;
return secret;
} catch (const std::exception&) {
return std::nullopt;
}
}
ByteBuffer EciesMbedTls::hmac_sha256(const ByteBuffer& key, const ByteBuffer& data) {
ScopedKey mac_key;
mac_key.id = import_raw(PSA_KEY_TYPE_HMAC, 0, PSA_KEY_USAGE_SIGN_MESSAGE, PSA_ALG_HMAC(PSA_ALG_SHA_256),
key.data(), key.size());
ByteBuffer out(32);
std::size_t length = 0;
if (psa_mac_compute(mac_key.id, PSA_ALG_HMAC(PSA_ALG_SHA_256), data.data(), data.size(), out.data(), out.size(), &length) != PSA_SUCCESS ||
length != 32) throw std::runtime_error("PSA HMAC failed");
return out;
}
ByteBuffer EciesMbedTls::random(std::size_t octets) {
ByteBuffer out(octets);
if (octets && psa_generate_random(out.data(), out.size()) != PSA_SUCCESS) throw std::runtime_error("PSA random failed");
return out;
}
bool EciesMbedTls::aes_ccm_encrypt(const std::array<std::uint8_t, 16>& key, const std::array<std::uint8_t, 12>& nonce,
const ByteBuffer& plaintext, ByteBuffer& out) {
try {
ScopedKey aes;
aes.id = import_raw(PSA_KEY_TYPE_AES, 128, PSA_KEY_USAGE_ENCRYPT, PSA_ALG_CCM, key.data(), key.size());
out.resize(plaintext.size() + 16);
std::size_t length = 0;
if (psa_aead_encrypt(aes.id, PSA_ALG_CCM, nonce.data(), nonce.size(), nullptr, 0, plaintext.data(), plaintext.size(),
out.data(), out.size(), &length) != PSA_SUCCESS || length != out.size()) { out.clear(); return false; }
return true;
} catch (const std::exception&) {
return false;
}
}
bool EciesMbedTls::aes_ccm_decrypt(const std::array<std::uint8_t, 16>& key, const std::array<std::uint8_t, 12>& nonce,
const ByteBuffer& in, ByteBuffer& plaintext) {
if (in.size() < 16) return false;
try {
ScopedKey aes;
aes.id = import_raw(PSA_KEY_TYPE_AES, 128, PSA_KEY_USAGE_DECRYPT, PSA_ALG_CCM, key.data(), key.size());
plaintext.resize(in.size() - 16);
std::size_t length = 0;
if (psa_aead_decrypt(aes.id, PSA_ALG_CCM, nonce.data(), nonce.size(), nullptr, 0, in.data(), in.size(),
plaintext.data(), plaintext.size(), &length) != PSA_SUCCESS || length != plaintext.size()) {
plaintext.clear();
return false;
}
return true;
} catch (const std::exception&) {
return false;
}
}
} // namespace vanetza_idf::pki
@@ -0,0 +1,121 @@
#include <vanetza_idf/ecies_openssl.hpp>
#include <vanetza_idf/ecc.hpp>
#include <vanetza/security/openssl_wrapper.hpp>
#include <openssl/ec.h>
#include <openssl/ecdh.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/obj_mac.h>
#include <openssl/rand.h>
#include <memory>
#include <stdexcept>
namespace vanetza_idf::pki {
using namespace vanetza::security;
namespace {
int nid_of(KeyType type) {
switch (type) {
case KeyType::NistP256: return NID_X9_62_prime256v1;
case KeyType::BrainpoolP256r1: return NID_brainpoolP256r1;
case KeyType::BrainpoolP384r1: return NID_brainpoolP384r1;
default: return NID_undef;
}
}
struct CipherContext {
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
~CipherContext() { EVP_CIPHER_CTX_free(ctx); }
};
} // namespace
KeyPair EciesOpenSsl::generate_key(KeyType type) {
openssl::Key key(nid_of(type));
openssl::check(EC_KEY_generate_key(key));
const std::size_t n = key_length(type);
KeyPair pair;
pair.priv.type = type;
pair.priv.key.resize(n);
BN_bn2binpad(EC_KEY_get0_private_key(key), pair.priv.key.data(), n);
openssl::BigNumber x, y;
openssl::BigNumberContext ctx;
openssl::check(EC_POINT_get_affine_coordinates(EC_KEY_get0_group(key), EC_KEY_get0_public_key(key), x, y, ctx));
pair.pub.type = type;
pair.pub.compression = KeyCompression::NoCompression;
pair.pub.x.resize(n); pair.pub.y.resize(n);
BN_bn2binpad(x, pair.pub.x.data(), n);
BN_bn2binpad(y, pair.pub.y.data(), n);
return pair;
}
std::optional<ByteBuffer> EciesOpenSsl::ecdh_x(const PrivateKey& own, const PublicKey& peer) {
if (own.type != peer.type || peer.compression != KeyCompression::NoCompression) return std::nullopt;
try {
openssl::Key key(nid_of(own.type));
openssl::BigNumber d(own.key);
openssl::check(EC_KEY_set_private_key(key, d));
openssl::Group group(nid_of(peer.type));
openssl::Point point(group);
openssl::BigNumberContext ctx;
openssl::check(EC_POINT_set_affine_coordinates(group, point, openssl::BigNumber(peer.x), openssl::BigNumber(peer.y), ctx));
openssl::check(EC_POINT_is_on_curve(group, point, ctx) == 1);
ByteBuffer secret(key_length(own.type));
// no KDF here: the raw x coordinate is the ECIES shared secret input to KDF2
const int written = ECDH_compute_key(secret.data(), secret.size(), point, key, nullptr);
if (written != static_cast<int>(secret.size())) return std::nullopt;
return secret;
} catch (const std::exception&) {
return std::nullopt;
}
}
ByteBuffer EciesOpenSsl::hmac_sha256(const ByteBuffer& key, const ByteBuffer& data) {
ByteBuffer out(32);
unsigned length = 0;
if (!HMAC(EVP_sha256(), key.data(), static_cast<int>(key.size()), data.data(), data.size(), out.data(), &length) ||
length != 32) throw std::runtime_error("HMAC-SHA256 failed");
return out;
}
ByteBuffer EciesOpenSsl::random(std::size_t octets) {
ByteBuffer out(octets);
if (octets && RAND_bytes(out.data(), static_cast<int>(octets)) != 1) throw std::runtime_error("RAND_bytes failed");
return out;
}
bool EciesOpenSsl::aes_ccm_encrypt(const std::array<std::uint8_t, 16>& key, const std::array<std::uint8_t, 12>& nonce,
const ByteBuffer& plaintext, ByteBuffer& out) {
CipherContext c;
if (!c.ctx) return false;
int length = 0;
if (EVP_EncryptInit_ex(c.ctx, EVP_aes_128_ccm(), nullptr, nullptr, nullptr) != 1 ||
EVP_CIPHER_CTX_ctrl(c.ctx, EVP_CTRL_CCM_SET_IVLEN, static_cast<int>(nonce.size()), nullptr) != 1 ||
EVP_CIPHER_CTX_ctrl(c.ctx, EVP_CTRL_CCM_SET_TAG, 16, nullptr) != 1 ||
EVP_EncryptInit_ex(c.ctx, nullptr, nullptr, key.data(), nonce.data()) != 1 ||
EVP_EncryptUpdate(c.ctx, nullptr, &length, nullptr, static_cast<int>(plaintext.size())) != 1) return false;
out.resize(plaintext.size() + 16);
if (EVP_EncryptUpdate(c.ctx, out.data(), &length, plaintext.data(), static_cast<int>(plaintext.size())) != 1 ||
length != static_cast<int>(plaintext.size())) return false;
int final_length = 0;
if (EVP_EncryptFinal_ex(c.ctx, out.data() + length, &final_length) != 1) return false;
if (EVP_CIPHER_CTX_ctrl(c.ctx, EVP_CTRL_CCM_GET_TAG, 16, out.data() + plaintext.size()) != 1) return false;
return true;
}
bool EciesOpenSsl::aes_ccm_decrypt(const std::array<std::uint8_t, 16>& key, const std::array<std::uint8_t, 12>& nonce,
const ByteBuffer& in, ByteBuffer& plaintext) {
if (in.size() < 16) return false;
const std::size_t payload = in.size() - 16;
CipherContext c;
if (!c.ctx) return false;
int length = 0;
if (EVP_DecryptInit_ex(c.ctx, EVP_aes_128_ccm(), nullptr, nullptr, nullptr) != 1 ||
EVP_CIPHER_CTX_ctrl(c.ctx, EVP_CTRL_CCM_SET_IVLEN, static_cast<int>(nonce.size()), nullptr) != 1 ||
EVP_CIPHER_CTX_ctrl(c.ctx, EVP_CTRL_CCM_SET_TAG, 16, const_cast<std::uint8_t*>(in.data() + payload)) != 1 ||
EVP_DecryptInit_ex(c.ctx, nullptr, nullptr, key.data(), nonce.data()) != 1 ||
EVP_DecryptUpdate(c.ctx, nullptr, &length, nullptr, static_cast<int>(payload)) != 1) return false;
plaintext.resize(payload);
// CCM verifies the tag inside this update; a mismatch returns 0
if (EVP_DecryptUpdate(c.ctx, plaintext.data(), &length, in.data(), static_cast<int>(payload)) != 1 ||
length != static_cast<int>(payload)) { plaintext.clear(); return false; }
return true;
}
} // namespace vanetza_idf::pki
@@ -0,0 +1,27 @@
// ESP32-C5 boot repair, linked into every application that uses this component on
// that target (the component's CMake forces the object in with "-u").
//
// ESP-IDF's esp_system/port/soc/esp32c5/system_internal.c documents that the C5
// ROM's UART initialisation misses the UART0 function clock enable
// (PCR_UART0_SCLK_EN, "does not reset with the UART module") and repairs it inside
// esp_restart(). A reset triggered over USB-Serial/JTAG or JTAG (esptool's
// DTR/RTS toggle, OpenOCD, a debugger) bypasses esp_restart(), so once an
// application has left the bit cleared the next boot's ROM stage waits forever
// for PCR_UART0_READY and the board stays unreachable until a power cycle.
// Setting the bit early in every boot keeps warm resets working regardless of what
// the application does with UART0; the cost is one register write.
#include "sdkconfig.h"
#if CONFIG_IDF_TARGET_ESP32C5
#include <esp_err.h>
#include <esp_private/startup_internal.h>
#include <soc/pcr_reg.h>
#include <soc/soc.h>
ESP_SYSTEM_INIT_FN(vidf_esp32c5_rom_uart_clock, CORE, BIT(0), 120) {
REG_SET_BIT(PCR_UART0_SCLK_CONF_REG, PCR_UART0_SCLK_EN);
return ESP_OK;
}
// Referenced through "-u" so the linker keeps this object and its init entry.
void vidf_esp32c5_rom_uart_clock_link(void) {}
#endif
@@ -0,0 +1,70 @@
#include <vanetza_idf/facilities.hpp>
#include <vanetza/common/its_aid.hpp>
#include <utility>
namespace vanetza_idf::facilities {
Descriptor descriptor(Kind kind) {
// TS 103 248 BTP well-known ports; CDD MessageId values. The service
// protocolVersion comes from each service ASN.1 definition: CAM/DENM 2,
// VAM 3 (TS 103 300-3 V2.3.1 ItsPduHeaderVam). Release != protocolVersion.
switch (kind) {
case Kind::cam: return {2001, 2, 2};
case Kind::denm: return {2002, 1, 2};
// TS 103 248 V2.4.1, Table 1: VA (VAM) is 2018; 2009 is CP (CPM).
case Kind::vam: return {2018, 16, 3};
}
return {0, 0, 0};
}
template<class Message>
Result check(Kind kind, const vanetza::ByteBuffer& data) {
Message message;
if (!message.decode_exact(data) || !message.validate()) return Result::invalid_argument;
const auto expected = descriptor(kind);
if (message->header.messageId != expected.message_id ||
message->header.protocolVersion != expected.protocol_version) return Result::invalid_argument;
return Result::accepted;
}
Result validate_pdu(Kind kind, const vanetza::ByteBuffer& data, std::size_t maximum) {
if (data.empty() || data.size() > maximum) return Result::invalid_argument;
try {
switch (kind) {
#if VIDF_CAM
case Kind::cam: return check<Cam>(kind, data);
#endif
#if VIDF_DENM
case Kind::denm: return check<Denm>(kind, data);
#endif
#if VIDF_VAM
case Kind::vam: return check<Vam>(kind, data);
#endif
default: return Result::unsupported;
}
} catch (const std::bad_alloc&) { return Result::resource_limit; }
catch (const std::exception&) { return Result::invalid_argument; }
}
#if VIDF_NETWORK
Result send(Stack& stack, Kind kind, vanetza::ByteBuffer bytes, BtpRequest req) {
auto result = validate_pdu(kind, bytes, stack.config().mib.itsGnMaxSduSize - 4u);
if (result != Result::accepted) return result;
req.type = BtpType::b;
req.source_port.reset();
req.destination_port = descriptor(kind).port;
req.destination_port_info = 0;
if (req.its_aid == 0) {
// SN-ENCAP its_aid selects the security profile: TS 102 965 V2.4.1 Table A.1
// (CA 36, DEN 37, VRU 638); TS 103 300-3 V2.3.1 clause 6.5.1 for VAMs.
switch (kind) {
case Kind::cam: req.its_aid = vanetza::aid::CA; break;
case Kind::denm: req.its_aid = vanetza::aid::DEN; break;
case Kind::vam: req.its_aid = vanetza::aid::VRU; break;
}
}
if (kind == Kind::vam && req.permissions.empty()) {
// TS 103 300-3 V2.3.1 clause 6.5.2: BitmapSsp whose first octet, value 1, is the SSP version.
req.permissions = {0x01};
}
req.data = std::move(bytes);
return stack.request(std::move(req));
}
#endif
}
@@ -0,0 +1,66 @@
#include <vanetza_idf/hil.hpp>
#include <algorithm>
#include <stdexcept>
namespace vanetza_idf::hil {
namespace {
constexpr std::uint8_t magic[] = {'V','I','D','1'};
std::uint32_t crc32(const std::uint8_t* data, std::size_t size) {
std::uint32_t crc = 0xffffffff;
for (std::size_t i = 0; i < size; ++i) {
crc ^= data[i];
for (int j = 0; j < 8; ++j) crc = (crc >> 1) ^ (0xedb88320u & (0u - (crc & 1u)));
}
return ~crc;
}
void append32(vanetza::ByteBuffer& b, std::uint32_t n) {
for (int shift = 24; shift >= 0; shift -= 8) b.push_back(n >> shift);
}
std::uint32_t read32(const std::uint8_t* p) {
return (std::uint32_t(p[0]) << 24) | (std::uint32_t(p[1]) << 16) | (std::uint32_t(p[2]) << 8) | p[3];
}
}
vanetza::ByteBuffer encode(const Frame& frame, std::size_t maximum) {
const auto size = frame.payload.size();
const auto channel = static_cast<unsigned>(frame.channel);
if (maximum > 65535 || size == 0 || size > maximum || channel < 1 || channel > 3)
throw std::invalid_argument("Invalid HIL frame");
vanetza::ByteBuffer b(std::begin(magic), std::end(magic));
b.push_back(channel);
append32(b, frame.sequence);
b.push_back(size >> 8); b.push_back(size);
b.insert(b.end(), frame.payload.begin(), frame.payload.end());
append32(b, crc32(b.data(), b.size()));
return b;
}
Decoder::Decoder(std::size_t maximum) : maximum_(maximum) {
if (maximum == 0 || maximum > 65535) throw std::invalid_argument("Invalid HIL MTU");
buffer_.reserve(maximum + 15);
}
void Decoder::feed(const std::uint8_t* p, std::size_t size, const Handler& handler) {
if (!p && size) throw std::invalid_argument("Null HIL bytes");
// At most MTU+15 bytes retained regardless of input chunk size.
for (std::size_t i = 0; i < size; ++i) { buffer_.push_back(p[i]); process(handler); }
}
void Decoder::process(const Handler& handler) {
while (buffer_.size() >= 4) {
if (!std::equal(std::begin(magic), std::end(magic), buffer_.begin())) {
buffer_.erase(buffer_.begin()); continue;
}
if (buffer_.size() < 11) return;
const auto size = (std::size_t(buffer_[9]) << 8) | buffer_[10];
if (buffer_[4] < 1 || buffer_[4] > 3 || size == 0 || size > maximum_) {
buffer_.erase(buffer_.begin()); continue;
}
const auto total = size + 15;
if (buffer_.size() < total) return;
if (crc32(buffer_.data(), total - 4) != read32(buffer_.data() + total - 4)) {
buffer_.erase(buffer_.begin()); continue;
}
Frame frame {static_cast<Channel>(buffer_[4]), read32(buffer_.data() + 5),
{buffer_.begin() + 11, buffer_.begin() + 11 + size}};
buffer_.erase(buffer_.begin(), buffer_.begin() + total);
handler(std::move(frame));
}
}
}
@@ -0,0 +1,225 @@
// TS 102 723-8 V1.1.1 clause 6.3 identifier-change procedures over one pool of
// authorization tickets. See id_change.hpp and security.hpp for the contracts.
#include <vanetza_idf/security.hpp>
#include <chrono>
#include <map>
#include <memory>
#include <set>
#include <vector>
namespace vanetza_idf::security {
using namespace vanetza;
class IdentityManager::Impl : public std::enable_shared_from_this<IdentityManager::Impl> {
public:
struct Subscription { IdChangeHook hook; ByteBuffer data; };
struct Round {
std::uint64_t id;
IdChangeCommand phase; // PREPARE or COMMIT
Identifier new_identifier;
std::set<SubscriptionHandle> outstanding;
};
Runtime& runtime;
CertificatePool& pool;
std::map<SubscriptionHandle, Subscription> subscriptions;
SubscriptionHandle next_subscription = 1;
std::map<LockHandle, Clock::time_point> locks;
LockHandle next_lock = 1;
std::unique_ptr<Round> round;
std::uint64_t round_counter = 0;
bool pending_trigger = false;
Clock::duration response_timeout = std::chrono::milliseconds(500);
std::function<void(const Identifier&)> committed;
Statistics stats;
unsigned notify_depth = 0; // hooks run re-entrantly; phase changes wait until delivery completed
char round_timer_scope = 0;
char lock_timer_scope = 0;
Impl(Runtime& rt, CertificatePool& p) : runtime(rt), pool(p) {}
Identifier current() const {
const auto* ticket = pool.current();
return ticket ? ticket->digest : Identifier {};
}
bool locked() const { return !locks.empty(); }
// Table 13 return_code carrier handed to hooks for PREPARE and COMMIT.
class Responder : public IdChangeResponder {
public:
Responder(std::weak_ptr<Impl> owner, std::uint64_t round, SubscriptionHandle handle) :
owner_(std::move(owner)), round_(round), handle_(handle) {}
void respond(bool return_code) override {
if (auto impl = owner_.lock()) impl->handle_response(round_, handle_, return_code);
}
private:
std::weak_ptr<Impl> owner_;
std::uint64_t round_;
SubscriptionHandle handle_;
};
// Deliver a command to every current subscriber; hooks may respond, unsubscribe or
// subscribe re-entrantly, so iterate over a snapshot and re-check membership.
void notify(IdChangeCommand command, const Identifier& id, bool expect_response) {
std::vector<SubscriptionHandle> handles;
for (const auto& entry : subscriptions) handles.push_back(entry.first);
if (expect_response && round) round->outstanding.insert(handles.begin(), handles.end());
const auto round_id = round ? round->id : 0;
++notify_depth;
for (auto handle : handles) {
// An abort raised by an earlier hook ends this round: nobody else gets PREPARE/COMMIT.
if (expect_response && (!round || round->id != round_id)) break;
auto it = subscriptions.find(handle);
if (it == subscriptions.end()) {
if (round && round->id == round_id) round->outstanding.erase(handle);
continue;
}
std::shared_ptr<IdChangeResponder> responder;
if (expect_response) responder = std::make_shared<Responder>(weak_from_this(), round_id, handle);
IdChangeHook hook = it->second.hook;
ByteBuffer data = it->second.data;
hook(command, id, data, responder);
}
--notify_depth;
if (expect_response && notify_depth == 0 && round && round->id == round_id) {
if (round->outstanding.empty()) advance();
else arm_round_timer();
}
}
void arm_round_timer() {
runtime.cancel(&round_timer_scope);
runtime.schedule(response_timeout, [this](Clock::time_point) { on_round_timeout(); }, &round_timer_scope);
}
void try_start() {
if (!pending_trigger || locked() || round) return;
const auto* next = pool.next_valid(runtime.now());
pending_trigger = false;
if (!next) return; // nothing to change to; trigger() already reported this
round = std::make_unique<Round>(Round {++round_counter, IdChangeCommand::PREPARE, next->digest, {}});
notify(IdChangeCommand::PREPARE, round->new_identifier, true); // advances or arms the timer itself
}
void handle_response(std::uint64_t round_id, SubscriptionHandle handle, bool return_code) {
if (!round || round->id != round_id) return;
if (round->outstanding.erase(handle) == 0) return;
if (!return_code) {
if (round->phase == IdChangeCommand::PREPARE) { abort(); return; }
++stats.commit_failures; // COMMIT cannot be rolled back (Figure 11); recorded
}
if (round->outstanding.empty() && notify_depth == 0) advance();
}
void advance() {
runtime.cancel(&round_timer_scope);
if (round->phase == IdChangeCommand::PREPARE) {
round->phase = IdChangeCommand::COMMIT;
pool.select(round->new_identifier);
notify(IdChangeCommand::COMMIT, round->new_identifier, true); // finishes or arms the timer
} else {
finish();
}
}
void finish() {
runtime.cancel(&round_timer_scope);
const Identifier id = round->new_identifier;
round.reset();
++stats.committed;
if (committed) committed(id);
try_start(); // a trigger queued during this round
}
void abort() {
runtime.cancel(&round_timer_scope);
const Identifier id = round->new_identifier;
round.reset();
++stats.aborted;
notify(IdChangeCommand::ABORT, id, false); // Figures 12/13
}
void on_round_timeout() {
if (!round) return;
++stats.timed_out;
if (round->phase == IdChangeCommand::PREPARE) {
abort();
} else {
stats.commit_failures += static_cast<unsigned>(round->outstanding.size());
finish();
}
}
void arm_lock_timer() {
runtime.cancel(&lock_timer_scope);
if (locks.empty()) return;
Clock::time_point earliest = locks.begin()->second;
for (const auto& lock : locks) earliest = std::min(earliest, lock.second);
runtime.schedule(earliest, [this](Clock::time_point) { on_lock_timer(); }, &lock_timer_scope);
}
void on_lock_timer() {
const auto now = runtime.now();
for (auto it = locks.begin(); it != locks.end();) {
if (it->second <= now) it = locks.erase(it); else ++it;
}
arm_lock_timer();
try_start();
}
};
IdentityManager::IdentityManager(Runtime& rt, CertificatePool& pool) : impl_(std::make_shared<Impl>(rt, pool)) {}
IdentityManager::~IdentityManager() {
impl_->runtime.cancel(&impl_->round_timer_scope);
impl_->runtime.cancel(&impl_->lock_timer_scope);
impl_->round.reset();
impl_->notify(IdChangeCommand::DEREG, impl_->current(), false); // Figure 15
impl_->subscriptions.clear();
}
SubscriptionHandle IdentityManager::subscribe(IdChangeHook hook, ByteBuffer subscriber_data) {
const auto handle = impl_->next_subscription++;
impl_->subscriptions.emplace(handle, Impl::Subscription {std::move(hook), std::move(subscriber_data)});
return handle;
}
Result IdentityManager::unsubscribe(SubscriptionHandle handle) {
if (impl_->subscriptions.erase(handle) == 0) return Result::invalid_argument;
if (impl_->round && impl_->round->outstanding.erase(handle) && impl_->round->outstanding.empty() &&
impl_->notify_depth == 0)
impl_->advance();
return Result::accepted;
}
Result IdentityManager::trigger() {
if (!impl_->pool.next_valid(impl_->runtime.now())) return Result::rejected;
impl_->pending_trigger = true;
impl_->try_start();
return Result::accepted;
}
LockHandle IdentityManager::lock(std::uint8_t duration_seconds) {
const auto handle = impl_->next_lock++;
impl_->locks[handle] = impl_->runtime.now() + std::chrono::seconds(duration_seconds);
impl_->arm_lock_timer();
if (duration_seconds == 0) impl_->on_lock_timer(); // released at once
return handle;
}
Result IdentityManager::unlock(LockHandle handle) {
if (impl_->locks.erase(handle) == 0) return Result::invalid_argument;
impl_->arm_lock_timer();
impl_->try_start();
return Result::accepted;
}
Identifier IdentityManager::current_identifier() const { return impl_->current(); }
bool IdentityManager::change_pending() const { return static_cast<bool>(impl_->round); }
void IdentityManager::set_response_timeout(Clock::duration timeout) { impl_->response_timeout = timeout; }
void IdentityManager::on_committed(std::function<void(const Identifier&)> callback) { impl_->committed = std::move(callback); }
const IdentityManager::Statistics& IdentityManager::statistics() const { return impl_->stats; }
bool IdentityManager::locked() const { return impl_->locked(); }
} // namespace vanetza_idf::security
@@ -0,0 +1,37 @@
#include <vanetza_idf/its_g5_frame.hpp>
#include <algorithm>
namespace vanetza_idf::its_g5 {
namespace {
constexpr std::uint8_t snap[] = {0xaa, 0xaa, 0x03, 0, 0, 0, 0x89, 0x47};
}
Result encode_frame(const AlDataRequest& request, std::uint16_t sequence, vanetza::ByteBuffer& out) {
if (validate(request, maximum_gnpdu) != Result::accepted || sequence > 4095)
return Result::invalid_argument;
out.assign(34 + request.data.size(), 0);
out[0] = 0x88; // QoS Data; To DS=From DS=0 (OCB), no encryption at MAC.
std::copy(request.destination.octets.begin(), request.destination.octets.end(), out.begin() + 4);
std::copy(request.source.octets.begin(), request.source.octets.end(), out.begin() + 10);
std::fill(out.begin() + 16, out.begin() + 22, 0xff); // wildcard BSSID
out[22] = (sequence << 4) & 0xff; out[23] = sequence >> 4;
out[24] = request.priority; // TID carries IEEE 802.1D user priority.
std::copy(std::begin(snap), std::end(snap), out.begin() + 26);
std::copy(request.data.begin(), request.data.end(), out.begin() + 34);
return Result::accepted;
}
Result decode_frame(const std::uint8_t* data, std::size_t length, bool fcs, AlDataIndication& out) {
if (!data || length < 34 + (fcs ? 4u : 0u)) return Result::invalid_argument;
if (fcs) length -= 4;
// Only non-fragmented, unprotected QoS Data with the OCB three-address
// layout. Reject A-MSDU/HT-control rather than misinterpreting their offsets.
if (data[0] != 0x88 || (data[1] & 0xc7) || (data[22] & 0x0f) ||
(data[24] & 0x80) || !std::all_of(data + 16, data + 22, [](auto b) { return b == 0xff; }))
return Result::unsupported;
if (!std::equal(std::begin(snap), std::end(snap), data + 26)) return Result::unsupported;
if (length == 34 || length - 34 > maximum_gnpdu) return Result::invalid_argument;
std::copy(data + 4, data + 10, out.destination.octets.begin());
std::copy(data + 10, data + 16, out.source.octets.begin());
out.data.assign(data + 34, data + length);
return Result::accepted;
}
}
@@ -0,0 +1,137 @@
// Management-plane bindings: MN-SAP (TS 102 723-4, TS 103 836-4-1 Annex K,
// TS 103 175 clause 8.3), MF-SAP (TS 102 723-5, TS 103 175 clause 8.4) and
// MI-SAP (TS 102 723-3, TS 103 175 clause 8.2). Values only ever come from the
// providers the application registers.
#include <vanetza_idf/mf_sap.hpp>
#include <vanetza_idf/mi_sap.hpp>
#include <vanetza_idf/mn_sap.hpp>
namespace vanetza_idf {
namespace {
// Shared GET/SET machinery: format check, access check, then the provider.
template<class No, class Param, class Error, class Provider>
void get_all(Provider* provider, const std::vector<No>& numbers, bool (*in_format)(No, std::uint32_t),
std::vector<Param>& out, std::vector<Error>& errors) {
for (auto no : numbers) {
std::uint32_t value = 0;
auto status = provider ? provider->get(no, value) : MN_SAP::ErrStatus::UNSUPPORTED;
if (status == MN_SAP::ErrStatus::SUCCESS && !in_format(no, value)) status = MN_SAP::ErrStatus::INVALID_VALUE;
if (status == MN_SAP::ErrStatus::SUCCESS) out.push_back(Param {no, value});
else errors.push_back(Error {no, status});
}
}
template<class No, class Param, class Error, class Provider>
void set_all(Provider* provider, const std::vector<Param>& params, bool (*in_format)(No, std::uint32_t),
bool (*writable)(No), std::vector<Error>& errors) {
for (const auto& param : params) {
MN_SAP::ErrStatus status;
if (!writable(param.no)) status = MN_SAP::ErrStatus::READ_ONLY;
else if (!in_format(param.no, param.value)) status = MN_SAP::ErrStatus::INVALID_VALUE;
else status = provider ? provider->set(param.no, param.value) : MN_SAP::ErrStatus::UNSUPPORTED;
if (status != MN_SAP::ErrStatus::SUCCESS) errors.push_back(Error {param.no, status});
}
}
} // namespace
namespace MN_SAP {
Result CORE_MMT_response_apply(Stack& stack, const CORE_MMT_response& response) {
if (!response.time && !response.local_position_vector && !response.geonetworking_address && !response.tc_mapping)
return Result::invalid_argument; // clause K.3: at least one parameter is present
if (response.time) {
const auto result = stack.advance(*response.time);
if (result != Result::accepted) return result;
}
if (response.local_position_vector) {
const auto result = stack.update_position(*response.local_position_vector);
if (result != Result::accepted) return result;
}
if (response.geonetworking_address) {
const auto result = stack.set_address(*response.geonetworking_address);
if (result != Result::accepted) return result;
}
if (response.tc_mapping) return Result::unsupported; // fixed TS 102 687 mapping in this port
return Result::accepted;
}
bool n_param_value_in_format(N_Param_No no, std::uint32_t value) {
switch (no) { // TS 103 175 Table 10 formats
case N_Param_No::GLOBAL_CBR:
case N_Param_No::LOCAL_CBR: return value <= 100;
case N_Param_No::CHANNEL_NUMBER: return value >= 1 && value <= 7;
case N_Param_No::AVAILABLE_RESOURCE:
case N_Param_No::IDLE_TIME: return value <= 0xffff;
case N_Param_No::LAST_TRANSMIT_TIME: return true;
case N_Param_No::TX_POWER_LEVEL_LIMIT: return value <= 31; // bits 5..7 reserved
}
return false;
}
bool n_param_writable(N_Param_No no) {
return no == N_Param_No::CHANNEL_NUMBER || no == N_Param_No::IDLE_TIME || no == N_Param_No::TX_POWER_LEVEL_LIMIT;
}
MN_GET_confirm MN_GET_request_submit(NetworkParameterProvider* provider, const MN_GET_request& request) {
MN_GET_confirm confirm {request.nt_id, request.command_ref, {}, {}};
get_all<N_Param_No, N_Param, N_Error>(provider, request.n_param_no, n_param_value_in_format, confirm.n_param, confirm.errors);
return confirm;
}
MN_SET_confirm MN_SET_request_submit(NetworkParameterProvider* provider, const MN_SET_request& request) {
MN_SET_confirm confirm {request.nt_id, request.command_ref, {}};
set_all<N_Param_No, N_Param, N_Error>(provider, request.n_param, n_param_value_in_format, n_param_writable, confirm.errors);
return confirm;
}
} // namespace MN_SAP
namespace MF_SAP {
bool f_param_value_in_format(F_Param_No no, std::uint32_t value) {
switch (no) { // TS 103 175 Table 13 formats
case F_Param_No::CHANNEL_NUMBER: return value >= 1 && value <= 7;
case F_Param_No::AVAILABLE_RESOURCE: return value <= 0xffff;
}
return false;
}
MF_SET_confirm MF_SET_request_submit(FacilitiesParameterSink* sink, const MF_SET_request& request) {
MF_SET_confirm confirm {request.fac_id, request.command_ref, {}};
for (const auto& param : request.f_param) {
ErrStatus status;
if (!f_param_value_in_format(param.no, param.value)) status = ErrStatus::INVALID_VALUE;
else status = sink ? sink->set(param.no, param.value) : ErrStatus::UNSUPPORTED;
if (status != ErrStatus::SUCCESS) confirm.errors.push_back(F_Error {param.no, status});
}
return confirm;
}
MF_COMMAND_confirm MF_COMMAND_request_submit(FacilitiesParameterSink* sink, const MF_COMMAND_request& request) {
if (!sink) return {request.fac_id, request.command_ref, ErrStatus::UNSUPPORTED};
return sink->command(request);
}
MF_REQUEST_confirm MF_REQUEST_request_submit(FacilitiesParameterSink* sink, const MF_REQUEST_request& request) {
if (!sink) return {request.fac_id, request.command_ref, ErrStatus::UNSUPPORTED, {}};
return sink->request(request);
}
} // namespace MF_SAP
namespace MI_SAP {
bool i_param_value_in_format(I_Param_No no, std::uint32_t value) {
switch (no) { // TS 103 175 Table 5 formats
case I_Param_No::CHANNEL_NUMBER: return value >= 1 && value <= 7;
case I_Param_No::LOCAL_CBR: return value <= 100;
case I_Param_No::MESSAGE_LENGTH: return value <= 0xff;
case I_Param_No::LAST_TRANSMIT_TIME: return true;
case I_Param_No::IDLE_TIME: return value <= 0xffff;
case I_Param_No::TX_POWER_LEVEL_LIMIT: return value <= 31;
}
return false;
}
bool i_param_writable(I_Param_No no) {
return no == I_Param_No::CHANNEL_NUMBER || no == I_Param_No::IDLE_TIME || no == I_Param_No::TX_POWER_LEVEL_LIMIT;
}
MI_GET_confirm MI_GET_request_submit(AccessParameterProvider* provider, const MI_GET_request& request) {
MI_GET_confirm confirm {request.mac_id, request.command_ref, {}, {}};
get_all<I_Param_No, I_Param, I_Error>(provider, request.i_param_no, i_param_value_in_format, confirm.i_param, confirm.errors);
return confirm;
}
MI_SET_confirm MI_SET_request_submit(AccessParameterProvider* provider, const MI_SET_request& request) {
MI_SET_confirm confirm {request.mac_id, request.command_ref, {}};
set_all<I_Param_No, I_Param, I_Error>(provider, request.i_param, i_param_value_in_format, i_param_writable, confirm.errors);
return confirm;
}
} // namespace MI_SAP
} // namespace vanetza_idf
@@ -0,0 +1,50 @@
#include <vanetza_idf/nvs_credential_store.hpp>
#include <nvs.h>
namespace vanetza_idf::security {
namespace {
// nvs_open/nvs_close around one operation; NVS handles are cheap and the store is stateless
class Handle {
public:
Handle(const std::string& ns, nvs_open_mode_t mode) { error_ = nvs_open(ns.c_str(), mode, &handle_); }
~Handle() { if (error_ == ESP_OK) nvs_close(handle_); }
esp_err_t error() const { return error_; }
nvs_handle_t get() const { return handle_; }
private:
nvs_handle_t handle_ = 0;
esp_err_t error_ = ESP_FAIL;
};
} // namespace
Result NvsCredentialStore::save(const Credentials& credentials) {
const ByteBuffer bundle = encode(credentials);
Handle nvs(namespace_, NVS_READWRITE);
if (nvs.error() != ESP_OK) return Result::rejected;
if (nvs_set_blob(nvs.get(), key_.c_str(), bundle.data(), bundle.size()) != ESP_OK) return Result::resource_limit;
return nvs_commit(nvs.get()) == ESP_OK ? Result::accepted : Result::rejected;
}
Result NvsCredentialStore::load(Credentials& credentials) {
Handle nvs(namespace_, NVS_READONLY);
if (nvs.error() != ESP_OK) return Result::rejected; // no such namespace yet: nothing stored
std::size_t size = 0;
const esp_err_t probe = nvs_get_blob(nvs.get(), key_.c_str(), nullptr, &size);
if (probe == ESP_ERR_NVS_NOT_FOUND) return Result::rejected;
if (probe != ESP_OK || size == 0) return Result::rejected;
ByteBuffer bundle(size);
if (nvs_get_blob(nvs.get(), key_.c_str(), bundle.data(), &size) != ESP_OK) return Result::rejected;
bundle.resize(size);
return decode(bundle, credentials) ? Result::accepted : Result::invalid_argument;
}
Result NvsCredentialStore::erase() {
Handle nvs(namespace_, NVS_READWRITE);
if (nvs.error() != ESP_OK) return Result::rejected;
const esp_err_t error = nvs_erase_key(nvs.get(), key_.c_str());
if (error == ESP_ERR_NVS_NOT_FOUND) return Result::rejected;
if (error != ESP_OK) return Result::rejected;
return nvs_commit(nvs.get()) == ESP_OK ? Result::accepted : Result::rejected;
}
} // namespace vanetza_idf::security
@@ -0,0 +1,776 @@
// TS 102 941 V2.2.1 enrolment/authorization messages on the upstream
// EtsiTs103097Data wrapper. See pki.hpp for the clause map.
#include <vanetza_idf/pki.hpp>
#include <vanetza_idf/ecc.hpp>
#include <vanetza_idf/security.hpp>
#include <vanetza/asn1/asn1c_wrapper.hpp>
#include <vanetza/asn1/security/EtsiTs102941Data.h>
#include <vanetza/asn1/security/CtlCommand.h>
#include <vanetza/asn1/security/CtlEntry.h>
#include <vanetza/asn1/security/EtsiTs103097Certificate.h>
#include <vanetza/asn1/security/InnerAtRequest.h>
#include <vanetza/asn1/security/InnerEcRequest.h>
#include <vanetza/asn1/security/PublicEncryptionKey.h>
#include <vanetza/asn1/security/PublicVerificationKey.h>
#include <vanetza/asn1/security/SharedAtRequest.h>
#include <vanetza/asn1/security/ValidityPeriod.h>
#include <vanetza/security/sha.hpp>
#include <vanetza/security/v2/basic_elements.hpp>
#include <vanetza/security/v3/asn1_conversions.hpp>
#include <vanetza/security/v3/hash.hpp>
#include <vanetza/security/v3/secured_message.hpp>
#include <algorithm>
#include <cstring>
namespace vanetza_idf::pki {
using namespace vanetza;
using namespace vanetza::security;
using SecuredData = v3::SecuredMessage; // EtsiTs103097Data wrapper
namespace {
constexpr std::size_t aes_key_length = 16;
constexpr std::size_t nonce_length = 12;
constexpr std::size_t tag_length = 16;
ByteBuffer octets(const OCTET_STRING_t& s) { return ByteBuffer(s.buf, s.buf + s.size); }
// EccP256CurvePoint -> PublicKey (x-only points carry no usable y and are refused)
std::optional<PublicKey> point_to_key(const Vanetza_Security_EccP256CurvePoint& point, KeyType type) {
PublicKey key;
key.type = type;
switch (point.present) {
case Vanetza_Security_EccP256CurvePoint_PR_compressed_y_0:
key.compression = KeyCompression::Y0; key.x = octets(point.choice.compressed_y_0); break;
case Vanetza_Security_EccP256CurvePoint_PR_compressed_y_1:
key.compression = KeyCompression::Y1; key.x = octets(point.choice.compressed_y_1); break;
case Vanetza_Security_EccP256CurvePoint_PR_uncompressedP256:
key.compression = KeyCompression::NoCompression;
key.x = octets(point.choice.uncompressedP256.x); key.y = octets(point.choice.uncompressedP256.y); break;
default: return std::nullopt;
}
if (key.x.size() != 32) return std::nullopt;
return key;
}
// uncompressed form for backends that need y (ECDH peer key)
std::optional<PublicKey> uncompressed(const PublicKey& key) {
if (key.compression == KeyCompression::NoCompression) return key;
auto point = ecc::decompress(key.type, key.x, key.compression == KeyCompression::Y1);
if (!point) return std::nullopt;
PublicKey out = key;
out.compression = KeyCompression::NoCompression;
out.y = point->y;
return out;
}
PublicKey compressed(const PublicKey& key) {
if (key.compression != KeyCompression::NoCompression || key.y.empty()) return key;
PublicKey out = key;
out.compression = (key.y.back() & 1) ? KeyCompression::Y1 : KeyCompression::Y0;
out.y.clear();
return out;
}
void fill_point(Vanetza_Security_EccP256CurvePoint& point, const PublicKey& key) {
point = v3::to_asn1(make_ecc_point(compressed(key)));
}
void fill_verification_key(Vanetza_Security_PublicVerificationKey& out, const PublicKey& key) {
if (key.type == KeyType::BrainpoolP256r1) {
out.present = Vanetza_Security_PublicVerificationKey_PR_ecdsaBrainpoolP256r1;
fill_point(out.choice.ecdsaBrainpoolP256r1, key);
} else {
out.present = Vanetza_Security_PublicVerificationKey_PR_ecdsaNistP256;
fill_point(out.choice.ecdsaNistP256, key);
}
}
void fill_encryption_key(Vanetza_Security_PublicEncryptionKey& out, const PublicKey& key) {
out.supportedSymmAlg = Vanetza_Security_SymmAlgorithm_aes128Ccm;
if (key.type == KeyType::BrainpoolP256r1) {
out.publicKey.present = Vanetza_Security_BasePublicEncryptionKey_PR_eciesBrainpoolP256r1;
fill_point(out.publicKey.choice.eciesBrainpoolP256r1, key);
} else {
out.publicKey.present = Vanetza_Security_BasePublicEncryptionKey_PR_eciesNistP256;
fill_point(out.publicKey.choice.eciesNistP256, key);
}
}
void fill_permissions(Vanetza_Security_CertificateSubjectAttributes& attributes, const Permissions& permissions) {
if (permissions.empty()) return;
attributes.appPermissions = v3::asn1::allocate<Vanetza_Security_SequenceOfPsidSsp>();
for (const auto& permission : permissions) {
auto* entry = v3::asn1::allocate<Vanetza_Security_PsidSsp>();
entry->psid = permission.first;
if (!permission.second.empty()) {
entry->ssp = v3::asn1::allocate<Vanetza_Security_ServiceSpecificPermissions>();
entry->ssp->present = Vanetza_Security_ServiceSpecificPermissions_PR_bitmapSsp;
v3::assign(&entry->ssp->choice.bitmapSsp, permission.second);
}
ASN_SEQUENCE_ADD(attributes.appPermissions, entry);
}
}
// IEEE 1609.2 clause 5.3.1: Hash(Hash(tbsData) || Hash(signer certificate or empty string))
ByteBuffer message_digest(Backend& backend, HashAlgorithm hash, const ByteBuffer& tbs, const Certificate* signer) {
if (signer) return v3::calculate_message_hash(backend, hash, tbs, *signer);
ByteBuffer concat = backend.calculate_hash(hash, tbs);
const ByteBuffer empty = backend.calculate_hash(hash, ByteBuffer {});
concat.insert(concat.end(), empty.begin(), empty.end());
return backend.calculate_hash(hash, concat);
}
// Fill the common header of a signed structure (TS 102 941: psid 623, generationTime only).
void prepare_signed(SecuredData& data, HashAlgorithm hash, Clock::time_point now, const Certificate* signer) {
data.set_hash_id(hash);
data.set_its_aid(aid::SCR);
data.set_generation_time(v2::convert_time64(now));
if (signer) {
const auto digest = signer->calculate_digest();
if (digest) data.set_signer_identifier(*digest); else data.set_signer_identifier_self();
} else {
data.set_signer_identifier_self();
}
}
std::optional<ByteBuffer> finish_signed(Backend& backend, SecuredData& data, HashAlgorithm hash, const PrivateKey& key,
const Certificate* signer) {
try {
const auto digest = message_digest(backend, hash, data.signing_payload(), signer);
data.set_signature(backend.sign_digest(key, digest));
return data.encode();
} catch (const std::exception&) {
return std::nullopt;
}
}
ByteBuffer psk_id_input(const std::array<std::uint8_t, aes_key_length>& key) {
vanetza::asn1::asn1c_oer_wrapper<Vanetza_Security_SymmetricEncryptionKey> wrapper(asn_DEF_Vanetza_Security_SymmetricEncryptionKey);
wrapper->present = Vanetza_Security_SymmetricEncryptionKey_PR_aes128Ccm;
OCTET_STRING_fromBuf(&wrapper->choice.aes128Ccm, reinterpret_cast<const char*>(key.data()), key.size());
return wrapper.encode();
}
std::array<std::uint8_t, 16> left16(const ByteBuffer& in) {
std::array<std::uint8_t, 16> out {};
std::copy_n(in.begin(), std::min<std::size_t>(16, in.size()), out.begin());
return out;
}
// InnerEcResponse / InnerAtResponse share the layout: requestHash, responseCode, certificate OPTIONAL
template<class Inner>
Result read_inner_response(const Inner& inner, const RequestContext& context, std::uint8_t& code,
std::optional<Certificate>& certificate) {
certificate.reset();
if (inner.requestHash.size != 16 || std::memcmp(inner.requestHash.buf, context.request_hash.data(), 16) != 0)
return Result::rejected; // the response answers another request
code = static_cast<std::uint8_t>(inner.responseCode);
if (inner.certificate) {
// asn1c forward-declares a distinct struct tag for this member; the object it points to is
// decoded with asn_DEF_Vanetza_Security_EtsiTs103097Certificate, i.e. an EtsiTs103097Certificate_t.
Certificate cert(*reinterpret_cast<const Vanetza_Security_EtsiTs103097Certificate_t*>(inner.certificate));
if (!cert.validate()) return Result::rejected;
certificate = std::move(cert);
} else if (code == 0) {
return Result::rejected; // clause 6.2.3.2.2: a positive response returns a certificate
}
return Result::accepted;
}
// Decrypt, verify the authority signature and hand back the EtsiTs102941Data
std::optional<ByteBuffer> open_response(Backend& backend, EciesBackend& ecies, const RequestContext& context,
const Certificate& authority, const ByteBuffer& encoded) {
auto signed_bytes = decrypt_with_psk(ecies, context.aes_key, encoded);
if (!signed_bytes) return std::nullopt;
return verify_signed(backend, *signed_bytes, nullptr, &authority, aid::SCR);
}
} // namespace
ByteBuffer kdf2_sha256(Backend& backend, const ByteBuffer& shared_secret, const ByteBuffer& kdp, std::size_t length) {
ByteBuffer derived;
for (std::uint32_t counter = 1; derived.size() < length; ++counter) {
ByteBuffer input = shared_secret;
input.push_back(static_cast<std::uint8_t>(counter >> 24));
input.push_back(static_cast<std::uint8_t>(counter >> 16));
input.push_back(static_cast<std::uint8_t>(counter >> 8));
input.push_back(static_cast<std::uint8_t>(counter));
input.insert(input.end(), kdp.begin(), kdp.end());
const auto block = backend.calculate_hash(HashAlgorithm::SHA256, input);
derived.insert(derived.end(), block.begin(), block.end());
}
derived.resize(length);
return derived;
}
std::optional<EncryptedKey> ecies_encrypt_key(Backend& backend, EciesBackend& ecies, const PublicKey& recipient,
const ByteBuffer& p1, const std::array<std::uint8_t, 16>& aes_key) {
const auto peer = uncompressed(recipient);
if (!peer || (peer->type != KeyType::NistP256 && peer->type != KeyType::BrainpoolP256r1)) return std::nullopt;
try {
const KeyPair ephemeral = ecies.generate_key(peer->type);
const auto secret = ecies.ecdh_x(ephemeral.priv, *peer);
if (!secret) return std::nullopt;
// K1 (16 octets, key encryption) || K2 (32 octets, MAC key)
const ByteBuffer k = kdf2_sha256(backend, *secret, p1, aes_key_length + 32);
EncryptedKey out;
out.v = ephemeral.pub;
ByteBuffer c(aes_key_length);
for (std::size_t i = 0; i < aes_key_length; ++i) c[i] = k[i] ^ aes_key[i];
const ByteBuffer k2(k.begin() + aes_key_length, k.end());
const ByteBuffer tag = ecies.hmac_sha256(k2, c);
if (tag.size() < tag_length) return std::nullopt;
out.c = left16(c);
out.t = left16(tag);
return out;
} catch (const std::exception&) {
return std::nullopt;
}
}
std::optional<std::array<std::uint8_t, 16>> ecies_decrypt_key(Backend& backend, EciesBackend& ecies,
const PrivateKey& recipient, const ByteBuffer& p1,
const EncryptedKey& encrypted) {
const auto peer = uncompressed(encrypted.v);
if (!peer) return std::nullopt;
try {
const auto secret = ecies.ecdh_x(recipient, *peer);
if (!secret) return std::nullopt;
const ByteBuffer k = kdf2_sha256(backend, *secret, p1, aes_key_length + 32);
const ByteBuffer c(encrypted.c.begin(), encrypted.c.end());
const ByteBuffer k2(k.begin() + aes_key_length, k.end());
const ByteBuffer tag = ecies.hmac_sha256(k2, c);
if (tag.size() < tag_length) return std::nullopt;
// constant-time comparison of the truncated tag
unsigned diff = 0;
for (std::size_t i = 0; i < tag_length; ++i) diff |= tag[i] ^ encrypted.t[i];
if (diff != 0) return std::nullopt;
std::array<std::uint8_t, 16> key {};
for (std::size_t i = 0; i < aes_key_length; ++i) key[i] = k[i] ^ encrypted.c[i];
return key;
} catch (const std::exception&) {
return std::nullopt;
}
}
// ---- building blocks -------------------------------------------------------
std::optional<ByteBuffer> sign_data(Backend& backend, Clock::time_point now, HashAlgorithm hash, const ByteBuffer& payload,
const PrivateKey& key, const Certificate* signer) {
SecuredData data = SecuredData::with_signed_data();
prepare_signed(data, hash, now, signer);
data.set_payload(payload);
return finish_signed(backend, data, hash, key, signer);
}
std::optional<ByteBuffer> sign_external(Backend& backend, Clock::time_point now, const ByteBuffer& external_payload,
const PrivateKey& key, const Certificate& signer) {
SecuredData data = SecuredData::with_signed_data_hash();
prepare_signed(data, HashAlgorithm::SHA256, now, &signer);
data.set_external_payload_hash(calculate_sha256_digest(external_payload.data(), external_payload.size()));
return finish_signed(backend, data, HashAlgorithm::SHA256, key, &signer);
}
std::optional<ByteBuffer> encrypt_for(Backend& backend, EciesBackend& ecies, const Certificate& recipient,
const ByteBuffer& plaintext, std::array<std::uint8_t, 16>& aes_key) {
const auto encryption_key = v3::get_public_encryption_key(*recipient.content());
const auto recipient_id = recipient.calculate_digest();
if (!encryption_key || !recipient_id) return std::nullopt;
try {
const ByteBuffer random = ecies.random(aes_key_length + nonce_length);
std::array<std::uint8_t, nonce_length> nonce {};
std::copy_n(random.begin(), aes_key_length, aes_key.begin());
std::copy_n(random.begin() + aes_key_length, nonce_length, nonce.begin());
ByteBuffer ciphertext;
if (!ecies.aes_ccm_encrypt(aes_key, nonce, plaintext, ciphertext)) return std::nullopt;
// P1 = SHA-256 of the recipient certificate (IEEE 1609.2 clause 5.3.5, certRecipInfo)
const ByteBuffer p1 = backend.calculate_hash(HashAlgorithm::SHA256, recipient.encode());
const auto wrapped = ecies_encrypt_key(backend, ecies, *encryption_key, p1, aes_key);
if (!wrapped) return std::nullopt;
SecuredData data = SecuredData::with_encrypted_data();
data.set_aes_ccm_ciphertext(ciphertext, nonce);
data.set_cert_recip_info(*recipient_id, wrapped->c, wrapped->t, compressed(wrapped->v));
return data.encode();
} catch (const std::exception&) {
return std::nullopt;
}
}
std::optional<ByteBuffer> encrypt_with_psk(EciesBackend& ecies, const std::array<std::uint8_t, 16>& aes_key,
const ByteBuffer& plaintext) {
try {
const ByteBuffer random = ecies.random(nonce_length);
std::array<std::uint8_t, nonce_length> nonce {};
std::copy_n(random.begin(), nonce_length, nonce.begin());
ByteBuffer ciphertext;
if (!ecies.aes_ccm_encrypt(aes_key, nonce, plaintext, ciphertext)) return std::nullopt;
SecuredData data = SecuredData::with_encrypted_data();
data.set_aes_ccm_ciphertext(ciphertext, nonce);
// pskRecipInfo = HashedId8 of the COER SymmetricEncryptionKey (IEEE 1609.2 clause 6.3.43)
const ByteBuffer key_encoding = psk_id_input(aes_key);
const HashedId8 psk_id = create_hashed_id8(calculate_sha256_digest(key_encoding.data(), key_encoding.size()));
auto* recipient = v3::asn1::allocate<Vanetza_Security_RecipientInfo>();
recipient->present = Vanetza_Security_RecipientInfo_PR_pskRecipInfo;
OCTET_STRING_fromBuf(&recipient->choice.pskRecipInfo, reinterpret_cast<const char*>(psk_id.data()), psk_id.size());
ASN_SEQUENCE_ADD(&data->content->choice.encryptedData.recipients.list, recipient);
return data.encode();
} catch (const std::exception&) {
return std::nullopt;
}
}
std::optional<ByteBuffer> decrypt_as_recipient(Backend& backend, EciesBackend& ecies, const Certificate& recipient,
const PrivateKey& encryption_key, const ByteBuffer& encoded,
std::array<std::uint8_t, 16>* aes_key_out) {
SecuredData data;
if (!data.decode(encoded) || !data.is_encrypted()) return std::nullopt;
const auto own_id = recipient.calculate_digest();
if (!own_id) return std::nullopt;
const auto& recipients = data->content->choice.encryptedData.recipients.list;
for (int i = 0; i < recipients.count; ++i) {
const auto* info = recipients.array[i];
if (!info || info->present != Vanetza_Security_RecipientInfo_PR_certRecipInfo) continue;
const auto& cert_info = info->choice.certRecipInfo;
if (cert_info.recipientId.size != 8 || std::memcmp(cert_info.recipientId.buf, own_id->data(), 8) != 0) continue;
const Vanetza_Security_EciesP256EncryptedKey* ecies_key = nullptr;
KeyType type = KeyType::NistP256;
if (cert_info.encKey.present == Vanetza_Security_EncryptedDataEncryptionKey_PR_eciesNistP256) {
ecies_key = &cert_info.encKey.choice.eciesNistP256;
} else if (cert_info.encKey.present == Vanetza_Security_EncryptedDataEncryptionKey_PR_eciesBrainpoolP256r1) {
ecies_key = &cert_info.encKey.choice.eciesBrainpoolP256r1;
type = KeyType::BrainpoolP256r1;
} else {
continue;
}
if (ecies_key->c.size != 16 || ecies_key->t.size != 16) continue;
EncryptedKey wrapped;
auto v = point_to_key(ecies_key->v, type);
if (!v) continue;
wrapped.v = *v;
std::copy_n(ecies_key->c.buf, 16, wrapped.c.begin());
std::copy_n(ecies_key->t.buf, 16, wrapped.t.begin());
const ByteBuffer p1 = backend.calculate_hash(HashAlgorithm::SHA256, recipient.encode());
const auto aes_key = ecies_decrypt_key(backend, ecies, encryption_key, p1, wrapped);
if (!aes_key) return std::nullopt;
ByteBuffer ciphertext;
std::array<std::uint8_t, nonce_length> nonce {};
data.get_aes_ccm_ciphertext(ciphertext, nonce);
ByteBuffer plaintext;
if (!ecies.aes_ccm_decrypt(*aes_key, nonce, ciphertext, plaintext)) return std::nullopt;
if (aes_key_out) *aes_key_out = *aes_key;
return plaintext;
}
return std::nullopt;
}
std::optional<ByteBuffer> decrypt_with_psk(EciesBackend& ecies, const std::array<std::uint8_t, 16>& aes_key,
const ByteBuffer& encoded) {
SecuredData data;
if (!data.decode(encoded) || !data.is_encrypted()) return std::nullopt;
if (!data.check_psk_match(aes_key)) return std::nullopt; // response is not for this request
ByteBuffer ciphertext;
std::array<std::uint8_t, nonce_length> nonce {};
data.get_aes_ccm_ciphertext(ciphertext, nonce);
ByteBuffer plaintext;
if (!ecies.aes_ccm_decrypt(aes_key, nonce, ciphertext, plaintext)) return std::nullopt;
return plaintext;
}
std::optional<ByteBuffer> verify_signed(Backend& backend, const ByteBuffer& encoded, const PublicKey* self_key,
const Certificate* signer, ItsAid expected_psid) {
SecuredData data;
if (!data.decode(encoded) || !data.is_signed() || data.protocol_version() != 3) return std::nullopt;
if (data.its_aid() != expected_psid || !data.generation_time()) return std::nullopt;
const auto hash = data.hash_id();
const auto signature = data.signature();
if (hash == HashAlgorithm::Unspecified || !signature) return std::nullopt;
const auto* signed_data = data->content->choice.signedData;
PublicKey key;
if (signer) {
const auto digest = signer->calculate_digest();
if (signed_data->signer.present != Vanetza_Security_SignerIdentifier_PR_digest || !digest ||
signed_data->signer.choice.digest.size != 8 ||
std::memcmp(signed_data->signer.choice.digest.buf, digest->data(), 8) != 0) return std::nullopt;
const auto signer_key = v3::get_public_key(*signer->content());
if (!signer_key) return std::nullopt;
key = *signer_key;
} else {
if (signed_data->signer.present != Vanetza_Security_SignerIdentifier_PR_self || !self_key) return std::nullopt;
key = *self_key;
}
const auto digest = message_digest(backend, hash, data.signing_payload(), signer);
if (!backend.verify_digest(key, digest, *signature)) return std::nullopt;
auto payload = data.payload();
const auto& packet = boost::get<CohesivePacket>(payload);
const auto view = create_byte_view(packet, OsiLayer::Network, max_osi_layer());
return ByteBuffer(view.begin(), view.end());
}
// ---- TS 102 941 clause 6.2.3.2 --------------------------------------------------
Result build_enrolment_request(Backend& backend, EciesBackend& ecies, Clock::time_point now,
const EnrolmentRequestParameters& params, const Certificate& ea, ByteBuffer& encoded,
RequestContext& context) {
if (params.its_id.empty() || params.verification_key.priv.key.empty() || params.outer_signer_key.key.empty())
return Result::invalid_argument;
if (!v3::get_public_encryption_key(*ea.content())) return Result::invalid_argument;
// 1. InnerEcRequest (certificateFormat ts103097v131 = 1)
vanetza::asn1::asn1c_oer_wrapper<Vanetza_Security_InnerEcRequest> inner(asn_DEF_Vanetza_Security_InnerEcRequest);
v3::assign(&inner->itsId, params.its_id);
inner->certificateFormat = Vanetza_Security_CertificateFormat_ts103097v131;
fill_verification_key(inner->publicKeys.verificationKey, params.verification_key.pub);
fill_permissions(inner->requestedSubjectAttributes, params.app_permissions);
if (!inner.validate()) return Result::invalid_argument;
// 2. InnerEcRequestSignedForPop: self-signed with the new verification key
auto pop = sign_data(backend, now, params.hash, inner.encode(), params.verification_key.priv, nullptr);
if (!pop) return Result::security_unavailable;
vanetza::asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
mgmt->version = Vanetza_Security_Version_v1;
mgmt->content.present = Vanetza_Security_EtsiTs102941DataContent_PR_enrolmentRequest;
void* target = &mgmt->content.choice.enrolmentRequest;
if (!vanetza::asn1::decode_oer(asn_DEF_Vanetza_Security_EtsiTs103097Data, &target, *pop)) return Result::security_unavailable;
// 3. Outer EtsiTs103097Data-Signed: self (canonical key) or digest of the current EC
auto outer = sign_data(backend, now, params.hash, mgmt.encode(), params.outer_signer_key, params.current_ec);
if (!outer) return Result::security_unavailable;
// 4. EtsiTs103097Data-Encrypted for the EA
auto encrypted = encrypt_for(backend, ecies, ea, *outer, context.aes_key);
if (!encrypted) return Result::security_unavailable;
encoded = std::move(*encrypted);
context.request_hash = left16(backend.calculate_hash(HashAlgorithm::SHA256, encoded));
return Result::accepted;
}
Result parse_enrolment_response(Backend& backend, EciesBackend& ecies, const RequestContext& context, const Certificate& ea,
const ByteBuffer& encoded, EnrolmentResponse& out) {
auto mgmt_bytes = open_response(backend, ecies, context, ea, encoded);
if (!mgmt_bytes) return Result::rejected;
vanetza::asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
if (!mgmt.decode(*mgmt_bytes) || mgmt->version != Vanetza_Security_Version_v1 ||
mgmt->content.present != Vanetza_Security_EtsiTs102941DataContent_PR_enrolmentResponse) return Result::rejected;
return read_inner_response(mgmt->content.choice.enrolmentResponse, context, out.response_code, out.certificate);
}
// ---- TS 102 941 clause 6.2.3.3 --------------------------------------------------
Result build_authorization_request(Backend& backend, EciesBackend& ecies, Clock::time_point now,
const AuthorizationRequestParameters& params, const Certificate& ea,
const Certificate& aa, ByteBuffer& encoded, RequestContext& context) {
if (!params.ec || params.ec_key.key.empty() || params.verification_key.priv.key.empty() || params.app_permissions.empty())
return Result::invalid_argument;
if (params.hash != HashAlgorithm::SHA256) return Result::unsupported; // extDataHash carries sha256HashedData
if (!v3::get_public_encryption_key(*aa.content()) || (params.privacy && !v3::get_public_encryption_key(*ea.content())))
return Result::invalid_argument;
const auto ea_id = ea.calculate_digest();
if (!ea_id) return Result::invalid_argument;
vanetza::asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
mgmt->version = Vanetza_Security_Version_v1;
mgmt->content.present = Vanetza_Security_EtsiTs102941DataContent_PR_authorizationRequest;
auto& inner = mgmt->content.choice.authorizationRequest;
// publicKeys: new verification key and optional encryption key
fill_verification_key(inner.publicKeys.verificationKey, params.verification_key.pub);
if (params.encryption_key) {
inner.publicKeys.encryptionKey = v3::asn1::allocate<Vanetza_Security_PublicEncryptionKey>();
fill_encryption_key(*inner.publicKeys.encryptionKey, params.encryption_key->pub);
}
// hmacKey: 32 random octets; keyTag = leftmost 16 octets of HMAC-SHA256(hmacKey, OER(publicKeys))
ByteBuffer hmac_key;
try { hmac_key = ecies.random(32); } catch (const std::exception&) { return Result::security_unavailable; }
if (hmac_key.size() != 32) return Result::security_unavailable;
v3::assign(&inner.hmacKey, hmac_key);
ByteBuffer tag_input = vanetza::asn1::encode_oer(asn_DEF_Vanetza_Security_PublicVerificationKey, &inner.publicKeys.verificationKey);
if (inner.publicKeys.encryptionKey) {
const auto enc = vanetza::asn1::encode_oer(asn_DEF_Vanetza_Security_PublicEncryptionKey, inner.publicKeys.encryptionKey);
tag_input.insert(tag_input.end(), enc.begin(), enc.end());
}
const auto key_tag = ecies.hmac_sha256(hmac_key, tag_input);
if (key_tag.size() < 16) return Result::security_unavailable;
// SharedAtRequest
auto& shared = inner.sharedAtRequest;
OCTET_STRING_fromBuf(&shared.eaId, reinterpret_cast<const char*>(ea_id->data()), ea_id->size());
OCTET_STRING_fromBuf(&shared.keyTag, reinterpret_cast<const char*>(key_tag.data()), 16);
shared.certificateFormat = Vanetza_Security_CertificateFormat_ts103097v131;
fill_permissions(shared.requestedSubjectAttributes, params.app_permissions);
if (params.validity_period) {
shared.requestedSubjectAttributes.validityPeriod = v3::asn1::allocate<Vanetza_Security_ValidityPeriod>();
shared.requestedSubjectAttributes.validityPeriod->start = v2::convert_time32(params.validity_period->first);
shared.requestedSubjectAttributes.validityPeriod->duration.present = Vanetza_Security_Duration_PR_hours;
shared.requestedSubjectAttributes.validityPeriod->duration.choice.hours = params.validity_period->second;
}
// EC signature over the SharedAtRequest (external payload), signer = digest of the EC
const ByteBuffer shared_encoded = vanetza::asn1::encode_oer(asn_DEF_Vanetza_Security_SharedAtRequest, &shared);
auto ec_signed = sign_external(backend, now, shared_encoded, params.ec_key, *params.ec);
if (!ec_signed) return Result::security_unavailable;
ByteBuffer ec_signature_bytes;
if (params.privacy) {
std::array<std::uint8_t, 16> throwaway {};
auto encrypted = encrypt_for(backend, ecies, ea, *ec_signed, throwaway);
if (!encrypted) return Result::security_unavailable;
inner.ecSignature.present = Vanetza_Security_EcSignature_PR_encryptedEcSignature;
void* target = &inner.ecSignature.choice.encryptedEcSignature;
if (!vanetza::asn1::decode_oer(asn_DEF_Vanetza_Security_EtsiTs103097Data, &target, *encrypted)) return Result::security_unavailable;
} else {
inner.ecSignature.present = Vanetza_Security_EcSignature_PR_ecSignature;
void* target = &inner.ecSignature.choice.ecSignature;
if (!vanetza::asn1::decode_oer(asn_DEF_Vanetza_Security_EtsiTs103097Data, &target, *ec_signed)) return Result::security_unavailable;
}
if (!mgmt.validate()) return Result::security_unavailable;
ByteBuffer plaintext = mgmt.encode();
if (params.include_pop) {
auto pop = sign_data(backend, now, params.hash, plaintext, params.verification_key.priv, nullptr);
if (!pop) return Result::security_unavailable;
plaintext = std::move(*pop);
} else {
// EtsiTs103097Data unsecured envelope around the EtsiTs102941Data
SecuredData envelope;
envelope->protocolVersion = 3;
envelope->content = v3::asn1::allocate<Vanetza_Security_Ieee1609Dot2Content>();
envelope->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_unsecuredData;
v3::assign(&envelope->content->choice.unsecuredData, plaintext);
plaintext = envelope.encode();
}
auto encrypted = encrypt_for(backend, ecies, aa, plaintext, context.aes_key);
if (!encrypted) return Result::security_unavailable;
encoded = std::move(*encrypted);
context.request_hash = left16(backend.calculate_hash(HashAlgorithm::SHA256, encoded));
return Result::accepted;
}
Result parse_authorization_response(Backend& backend, EciesBackend& ecies, const RequestContext& context, const Certificate& aa,
const ByteBuffer& encoded, AuthorizationResponse& out) {
auto mgmt_bytes = open_response(backend, ecies, context, aa, encoded);
if (!mgmt_bytes) return Result::rejected;
vanetza::asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
if (!mgmt.decode(*mgmt_bytes) || mgmt->version != Vanetza_Security_Version_v1 ||
mgmt->content.present != Vanetza_Security_EtsiTs102941DataContent_PR_authorizationResponse) return Result::rejected;
return read_inner_response(mgmt->content.choice.authorizationResponse, context, out.response_code, out.certificate);
}
// ---- TS 102 941 clause 6.3: RCA trust list and revocation list -----------------------
namespace {
void set_url(Vanetza_Security_Url_t& url, const std::string& text) {
OCTET_STRING_fromBuf(&url, text.data(), text.size());
}
std::string get_url(const Vanetza_Security_Url_t& url) {
return std::string(reinterpret_cast<const char*>(url.buf), url.size);
}
void set_hashed_id8(Vanetza_Security_HashedId8_t& target, const HashedId8& id) {
OCTET_STRING_fromBuf(&target, reinterpret_cast<const char*>(id.data()), id.size());
}
std::optional<HashedId8> get_hashed_id8(const Vanetza_Security_HashedId8_t& source) {
if (source.size != 8) return std::nullopt;
HashedId8 id;
std::copy_n(source.buf, 8, id.begin());
return id;
}
// the certificate structure of an entry, decoded from COER into the inline member
bool put_certificate(Vanetza_Security_EtsiTs103097Certificate_t& target, const ByteBuffer& coer) {
void* into = &target;
return vanetza::asn1::decode_oer(asn_DEF_Vanetza_Security_EtsiTs103097Certificate, &into, coer);
}
// EtsiTs102941Data{content} signed by the RCA with the signer certificate inline (clause 6.3.4)
std::optional<ByteBuffer> sign_list(Backend& backend, Clock::time_point now, const Certificate& rca, const PrivateKey& rca_key,
ItsAid psid, const ByteBuffer& mgmt) {
if (!rca.valid_for_application(psid)) return std::nullopt; // TS 103 097 clause 7.2.3: CRL/CTL appPermissions
SecuredData data = SecuredData::with_signed_data();
data.set_hash_id(HashAlgorithm::SHA256);
data.set_its_aid(psid);
data.set_generation_time(v2::convert_time64(now));
data.set_signer_identifier(rca);
data.set_payload(mgmt);
return finish_signed(backend, data, HashAlgorithm::SHA256, rca_key, &rca);
}
// the EtsiTs102941Data of a list message that verifies as signed by rca with the given psid
std::optional<ByteBuffer> open_list(Backend& backend, const ByteBuffer& message, const Certificate& rca, ItsAid psid) {
SecuredData data;
if (!data.decode(message) || !data.is_signed() || data.protocol_version() != 3) return std::nullopt;
if (data.its_aid() != psid || !data.generation_time() || data.hash_id() != HashAlgorithm::SHA256) return std::nullopt;
if (!rca.valid_for_application(psid)) return std::nullopt;
// clause 6.3.4: the signer contains the issuer's certificate; it must be the RCA we trust
const auto* signed_data = data->content->choice.signedData;
if (signed_data->signer.present != Vanetza_Security_SignerIdentifier_PR_certificate ||
signed_data->signer.choice.certificate.list.count != 1) return std::nullopt;
const ByteBuffer inline_cert = vanetza::asn1::encode_oer(asn_DEF_Vanetza_Security_EtsiTs103097Certificate,
signed_data->signer.choice.certificate.list.array[0]);
if (inline_cert != rca.encode()) return std::nullopt;
const auto signature = data.signature();
const auto key = v3::get_public_key(*rca.content());
if (!signature || !key) return std::nullopt;
const auto digest = message_digest(backend, HashAlgorithm::SHA256, data.signing_payload(), &rca);
try {
if (!backend.verify_digest(*key, digest, *signature)) return std::nullopt;
} catch (const std::exception&) {
return std::nullopt;
}
auto payload = data.payload();
const auto& packet = boost::get<CohesivePacket>(payload);
const auto view = create_byte_view(packet, OsiLayer::Network, max_osi_layer());
return ByteBuffer(view.begin(), view.end());
}
} // namespace
std::optional<ByteBuffer> build_rca_ctl(Backend& backend, Clock::time_point now, const Certificate& rca, const PrivateKey& rca_key,
const TrustListEntries& entries, Time32 next_update, std::uint8_t ctl_sequence) {
vanetza::asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
mgmt->version = Vanetza_Security_Version_v1;
mgmt->content.present = Vanetza_Security_EtsiTs102941DataContent_PR_certificateTrustListRca;
auto& ctl = mgmt->content.choice.certificateTrustListRca;
ctl.version = 1; // clause 6.3.4: CtlFormat version 1
ctl.nextUpdate = next_update;
ctl.isFullCtl = 1;
ctl.ctlSequence = ctl_sequence;
const auto add = [&](Vanetza_Security_CtlEntry_PR kind) -> Vanetza_Security_CtlEntry_t& {
auto* command = vanetza::asn1::allocate<Vanetza_Security_CtlCommand_t>();
command->present = Vanetza_Security_CtlCommand_PR_add;
command->choice.add.present = kind;
ASN_SEQUENCE_ADD(&ctl.ctlCommands, command);
return command->choice.add;
};
for (const auto& ea : entries.ea) {
auto& entry = add(Vanetza_Security_CtlEntry_PR_ea).choice.ea;
if (!put_certificate(entry.eaCertificate, ea.certificate)) return std::nullopt;
set_url(entry.aaAccessPoint, ea.access_point);
}
for (const auto& aa : entries.aa) {
auto& entry = add(Vanetza_Security_CtlEntry_PR_aa).choice.aa;
if (!put_certificate(entry.aaCertificate, aa.certificate)) return std::nullopt;
set_url(entry.accessPoint, aa.access_point);
}
for (const auto& dc : entries.dc) {
auto& entry = add(Vanetza_Security_CtlEntry_PR_dc).choice.dc;
set_url(entry.url, dc.url);
for (const auto& id : dc.certificates) {
auto* item = vanetza::asn1::allocate<Vanetza_Security_HashedId8_t>();
set_hashed_id8(*item, id);
ASN_SEQUENCE_ADD(&entry.cert, item);
}
}
if (!mgmt.validate()) return std::nullopt;
return sign_list(backend, now, rca, rca_key, aid::CTL, mgmt.encode());
}
std::optional<ByteBuffer> build_crl(Backend& backend, Clock::time_point now, const Certificate& rca, const PrivateKey& rca_key,
const std::vector<HashedId8>& revoked, Time32 this_update, Time32 next_update) {
vanetza::asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
mgmt->version = Vanetza_Security_Version_v1;
mgmt->content.present = Vanetza_Security_EtsiTs102941DataContent_PR_certificateRevocationList;
auto& crl = mgmt->content.choice.certificateRevocationList;
crl.version = 1;
crl.thisUpdate = this_update;
crl.nextUpdate = next_update;
for (const auto& id : revoked) {
auto* entry = vanetza::asn1::allocate<Vanetza_Security_CrlEntry_t>();
set_hashed_id8(*entry, id);
ASN_SEQUENCE_ADD(&crl.entries, entry);
}
if (!mgmt.validate()) return std::nullopt;
return sign_list(backend, now, rca, rca_key, aid::CRL, mgmt.encode());
}
std::optional<RcaTrustList> parse_rca_ctl(Backend& backend, const ByteBuffer& message, const Certificate& rca) {
const auto mgmt_bytes = open_list(backend, message, rca, aid::CTL);
if (!mgmt_bytes) return std::nullopt;
vanetza::asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
if (!mgmt.decode(*mgmt_bytes) || mgmt->version != Vanetza_Security_Version_v1 ||
mgmt->content.present != Vanetza_Security_EtsiTs102941DataContent_PR_certificateTrustListRca) return std::nullopt;
const auto& ctl = mgmt->content.choice.certificateTrustListRca;
if (ctl.version != 1 || ctl.ctlSequence < 0 || ctl.ctlSequence > 255) return std::nullopt;
RcaTrustList list;
list.sequence = static_cast<std::uint8_t>(ctl.ctlSequence);
list.next_update = static_cast<Time32>(ctl.nextUpdate);
list.full = ctl.isFullCtl != 0;
const auto rca_digest = rca.calculate_digest();
if (!rca_digest) return std::nullopt;
const auto issued_by_rca = [&](const Vanetza_Security_EtsiTs103097Certificate_t& raw, Certificate& out) {
out = Certificate(raw);
const auto issuer = out.issuer_digest();
return issuer && *issuer == *rca_digest && out.is_ca_certificate() &&
vanetza_idf::security::verify_certificate_signature(backend, out, &rca);
};
for (int i = 0; i < ctl.ctlCommands.list.count; ++i) {
const auto* command = ctl.ctlCommands.list.array[i];
if (!command) return std::nullopt;
if (command->present == Vanetza_Security_CtlCommand_PR_add) {
const auto& entry = command->choice.add;
Certificate certificate;
switch (entry.present) {
case Vanetza_Security_CtlEntry_PR_ea:
if (!issued_by_rca(entry.choice.ea.eaCertificate, certificate)) return std::nullopt;
list.ea.push_back(std::move(certificate));
break;
case Vanetza_Security_CtlEntry_PR_aa:
if (!issued_by_rca(entry.choice.aa.aaCertificate, certificate)) return std::nullopt;
list.aa.push_back(std::move(certificate));
break;
case Vanetza_Security_CtlEntry_PR_dc: {
TrustListEntries::DistributionCentre dc;
dc.url = get_url(entry.choice.dc.url);
for (int k = 0; k < entry.choice.dc.cert.list.count; ++k) {
const auto id = entry.choice.dc.cert.list.array[k] ? get_hashed_id8(*entry.choice.dc.cert.list.array[k]) : std::nullopt;
if (!id) return std::nullopt;
dc.certificates.push_back(*id);
}
list.dc.push_back(std::move(dc));
break;
}
default:
return std::nullopt; // clause 6.3.2: an RCA CTL carries no RCA or TLM entries
}
} else if (command->present == Vanetza_Security_CtlCommand_PR_delete) {
if (list.full) return std::nullopt; // clause 6.3.4: a FullCtl has add commands only
const auto& del = command->choice.Delete;
if (del.present == Vanetza_Security_CtlDelete_PR_cert) {
const auto id = get_hashed_id8(del.choice.cert);
if (!id) return std::nullopt;
list.deleted.push_back(*id);
} else if (del.present == Vanetza_Security_CtlDelete_PR_dc) {
list.deleted_dc.push_back(get_url(del.choice.dc));
} else {
return std::nullopt;
}
} else {
return std::nullopt;
}
}
return list;
}
std::optional<RevocationList> parse_crl(Backend& backend, const ByteBuffer& message, const Certificate& rca) {
const auto mgmt_bytes = open_list(backend, message, rca, aid::CRL);
if (!mgmt_bytes) return std::nullopt;
vanetza::asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
if (!mgmt.decode(*mgmt_bytes) || mgmt->version != Vanetza_Security_Version_v1 ||
mgmt->content.present != Vanetza_Security_EtsiTs102941DataContent_PR_certificateRevocationList) return std::nullopt;
const auto& crl = mgmt->content.choice.certificateRevocationList;
if (crl.version != 1) return std::nullopt;
RevocationList list;
list.this_update = static_cast<Time32>(crl.thisUpdate);
list.next_update = static_cast<Time32>(crl.nextUpdate);
for (int i = 0; i < crl.entries.list.count; ++i) {
const auto id = crl.entries.list.array[i] ? get_hashed_id8(*crl.entries.list.array[i]) : std::nullopt;
if (!id) return std::nullopt;
list.revoked.push_back(*id);
}
return list;
}
std::size_t apply(const RcaTrustList& list, vanetza_idf::security::TrustConfiguration& trust) {
std::size_t added = 0;
for (const auto* entries : {&list.ea, &list.aa}) {
for (const auto& certificate : *entries) {
if (trust.add_authority(certificate) == Result::accepted) ++added;
}
}
return added;
}
std::size_t apply(const RevocationList& list, const Certificate& rca, vanetza_idf::security::TrustConfiguration& trust) {
const auto issuer = rca.calculate_digest();
if (!issuer) return 0;
trust.clear_revocations(*issuer);
for (const auto& id : list.revoked) trust.revoke(*issuer, id);
return list.revoked.size();
}
} // namespace vanetza_idf::pki
@@ -0,0 +1,933 @@
#include <vanetza_idf/security.hpp>
#include <vanetza/asn1/asn1c_wrapper.hpp>
#include <vanetza/security/encap_service.hpp>
#include <vanetza/security/v3/hash.hpp>
#include <vanetza/security/v3/secured_message.hpp>
#include <vanetza/security/v3/sign_service.hpp>
#include <vanetza/security/v2/basic_elements.hpp>
#if VIDF_SECURITY_VERIFY
#include <vanetza/security/verify_service.hpp>
#include <vanetza/security/v3/asn1_conversions.hpp>
#include <vanetza/security/v3/basic_elements.hpp>
#include <vanetza/security/v3/certificate_cache.hpp>
#include <vanetza/security/v3/issuer_lookup.hpp>
#include <unordered_set>
#include <vector>
#endif
#include <algorithm>
#include <stdexcept>
namespace vanetza_idf::security {
using namespace vanetza;
using namespace vanetza::security;
namespace {
// TS 103 097 V2.2.1 clause 7.2.1: issuer is a digest, appPermissions present,
// certIssuePermissions absent, CertificateId none.
bool is_authorization_ticket(const Certificate& cert) {
const auto& issuer = cert->issuer;
const bool digest_issuer = issuer.present == Vanetza_Security_IssuerIdentifier_PR_sha256AndDigest ||
issuer.present == Vanetza_Security_IssuerIdentifier_PR_sha384AndDigest;
return digest_issuer && cert.is_at_certificate() &&
cert->toBeSigned.id.present == Vanetza_Security_CertificateId_PR_none;
}
} // namespace
// ---- CertificatePool --------------------------------------------------------
CertificatePool::CertificatePool(Backend& backend) : backend_(backend) {}
Result CertificatePool::add(const ByteBuffer& coer, const PrivateKey& key) {
Certificate certificate;
if (coer.empty() || !certificate.decode(coer) || !certificate.validate()) return Result::invalid_argument;
return add(std::move(certificate), key);
}
Result CertificatePool::add(Certificate certificate, PrivateKey key) {
if (!is_authorization_ticket(certificate)) return Result::invalid_argument;
const auto type = certificate.get_verification_key_type();
if (type == KeyType::Unspecified || key.type != type || key.key.size() != key_length(type))
return Result::invalid_argument;
const auto digest = certificate.calculate_digest();
const auto public_key = v3::get_public_key(*certificate.content());
if (!digest || !public_key) return Result::invalid_argument;
for (const auto& ticket : tickets_) if (ticket.digest == *digest) return Result::invalid_argument;
// The key must belong to this certificate: sign a digest and verify it with the
// certificate's verification key. A provisioning mix-up fails here, not on air.
try {
const ByteBuffer probe = backend_.calculate_hash(v3::specified_hash_algorithm(type), certificate.encode());
const auto signature = backend_.sign_digest(key, probe);
if (!backend_.verify_digest(*public_key, probe, signature)) return Result::invalid_argument;
} catch (const std::exception&) {
return Result::invalid_argument;
}
tickets_.push_back(Ticket {std::move(certificate), std::move(key), *digest});
return Result::accepted;
}
const CertificatePool::Ticket* CertificatePool::current() const {
return tickets_.empty() ? nullptr : &tickets_[current_];
}
const CertificatePool::Ticket* CertificatePool::next_valid(Clock::time_point now) const {
if (tickets_.size() < 2) return nullptr;
for (std::size_t step = 1; step < tickets_.size(); ++step) {
const auto& candidate = tickets_[(current_ + step) % tickets_.size()];
if (candidate.certificate.valid_at_timepoint(now)) return &candidate;
}
return nullptr;
}
Result CertificatePool::select(const HashedId8& digest) {
for (std::size_t i = 0; i < tickets_.size(); ++i) {
if (tickets_[i].digest == digest) { current_ = i; return Result::accepted; }
}
return Result::invalid_argument;
}
std::size_t CertificatePool::prune(Clock::time_point now) {
const auto time32 = v2::convert_time32(now);
std::size_t removed = 0;
for (std::size_t i = 0; i < tickets_.size();) {
if (i != current_ && tickets_[i].certificate.get_start_and_end_validity().end_validity < time32) {
tickets_.erase(tickets_.begin() + i);
if (i < current_) --current_;
++removed;
} else {
++i;
}
}
return removed;
}
const Certificate& CertificatePool::own_certificate() {
if (tickets_.empty()) throw std::logic_error("certificate pool is empty");
return tickets_[current_].certificate;
}
const PrivateKey& CertificatePool::own_private_key() {
if (tickets_.empty()) throw std::logic_error("certificate pool is empty");
return tickets_[current_].key;
}
// ---- TrustConfiguration -----------------------------------------------------
Result TrustConfiguration::add_root(const ByteBuffer& coer) {
Certificate certificate;
if (coer.empty() || !certificate.decode(coer) || !certificate.validate()) return Result::invalid_argument;
return add_root(certificate);
}
Result TrustConfiguration::add_root(const Certificate& certificate) {
// TS 103 097 clause 7.2.3: self-signed with certIssuePermissions.
if (!certificate.issuer_is_self() || !certificate.is_ca_certificate()) return Result::invalid_argument;
if (!certificate.calculate_digest()) return Result::invalid_argument;
roots_.insert(certificate);
if (!issuers_.insert(certificate)) return Result::invalid_argument;
authorities_.push_back(certificate);
return Result::accepted;
}
Result TrustConfiguration::add_authority(const ByteBuffer& coer) {
Certificate certificate;
if (coer.empty() || !certificate.decode(coer) || !certificate.validate()) return Result::invalid_argument;
return add_authority(certificate);
}
Result TrustConfiguration::add_authority(const Certificate& certificate) {
// TS 103 097 clause 7.2.4: issued by digest, carries certIssuePermissions.
if (certificate.issuer_is_self() || !certificate.is_ca_certificate()) return Result::invalid_argument;
const auto digest = certificate.calculate_digest();
if (!digest) return Result::invalid_argument;
if (issuers_.find_issuer(*digest)) return Result::rejected; // already known (a CTL applied twice)
if (!issuers_.insert(certificate)) return Result::invalid_argument;
authorities_.push_back(certificate);
return Result::accepted;
}
void TrustConfiguration::revoke(const HashedId8& issuer, const HashedId8& certificate) {
revocations_.revoke(issuer, certificate);
}
void TrustConfiguration::clear_revocations(const HashedId8& issuer) {
revocations_.clear(issuer);
}
// ---- Certificate signatures and message profiles ----------------------------
namespace {
bool verify_certificate_signature(Backend& backend, const Vanetza_Security_EtsiTs103097Certificate_t& subject,
const Certificate* issuer) {
const auto signature = v3::get_signature(subject);
if (!signature) return false;
HashAlgorithm hash = HashAlgorithm::SHA256;
switch (subject.issuer.present) {
case Vanetza_Security_IssuerIdentifier_PR_sha256AndDigest:
if (!issuer) return false;
break;
case Vanetza_Security_IssuerIdentifier_PR_sha384AndDigest:
if (!issuer) return false;
hash = HashAlgorithm::SHA384;
break;
case Vanetza_Security_IssuerIdentifier_PR_self:
hash = subject.issuer.choice.self == Vanetza_Security_HashAlgorithm_sha384 ? HashAlgorithm::SHA384 : HashAlgorithm::SHA256;
issuer = nullptr; // IEEE Std 1609.2 clause 5.3.1: the empty string stands in for the issuer
break;
default:
return false;
}
const auto public_key = v3::get_public_key(issuer ? *issuer->content() : subject);
if (!public_key) return false;
const ByteBuffer tbs = vanetza::asn1::encode_oer(asn_DEF_Vanetza_Security_ToBeSignedCertificate, &subject.toBeSigned);
ByteBuffer input = backend.calculate_hash(hash, tbs);
const ByteBuffer issuer_hash = backend.calculate_hash(hash, issuer ? issuer->encode() : ByteBuffer {});
input.insert(input.end(), issuer_hash.begin(), issuer_hash.end());
return backend.verify_digest(*public_key, backend.calculate_hash(hash, input), *signature);
}
} // namespace
bool verify_certificate_signature(Backend& backend, const Certificate& subject, const Certificate* issuer) {
return subject.content() && verify_certificate_signature(backend, *subject.content(), issuer);
}
VerificationReport check_profile(const v3::SecuredMessage& msg) {
if (msg.protocol_version() != 3) return VerificationReport::Incompatible_Protocol;
if (!msg.is_signed()) return VerificationReport::Unsigned_Message;
const auto* signed_data = msg->content->choice.signedData;
if (!signed_data || !signed_data->tbsData) return VerificationReport::Incompatible_Protocol;
const auto& tbs = *signed_data->tbsData;
const auto& header = tbs.headerInfo;
// clause 5.2: payload is an Ieee1609Dot2Data (protocolVersion 3) or an external hash
if (!tbs.payload) return VerificationReport::Incompatible_Protocol;
if (tbs.payload->data && tbs.payload->data->protocolVersion != 3) return VerificationReport::Incompatible_Protocol;
if (!tbs.payload->data && !tbs.payload->extDataHash) return VerificationReport::Incompatible_Protocol;
// clause 5.2 with IEEE Std 1609.2 clause 5.3.3: hashId follows the signing algorithm
const bool sha384 = signed_data->hashId == Vanetza_Security_HashAlgorithm_sha384;
switch (signed_data->signature.present) {
case Vanetza_Security_Signature_PR_ecdsaNistP256Signature:
case Vanetza_Security_Signature_PR_ecdsaBrainpoolP256r1Signature:
if (sha384) return VerificationReport::Incompatible_Protocol;
break;
case Vanetza_Security_Signature_PR_ecdsaBrainpoolP384r1Signature:
if (!sha384) return VerificationReport::Incompatible_Protocol;
break;
default:
return VerificationReport::Incompatible_Protocol;
}
if (!header.generationTime) return VerificationReport::Invalid_Timestamp; // "always present"
if (header.p2pcdLearningRequest || header.missingCrlIdentifier) return VerificationReport::Incompatible_Protocol;
switch (signed_data->signer.present) {
case Vanetza_Security_SignerIdentifier_PR_digest:
case Vanetza_Security_SignerIdentifier_PR_certificate:
break;
default:
return VerificationReport::Unsupported_Signer_Identifier_Type;
}
const ItsAid its_aid = msg.its_aid();
if (its_aid == aid::CA) {
// clause 7.1.1: besides generationTime only inlineP2pcdRequest and requestedCertificate may appear
if (header.expiryTime || header.generationLocation || header.encryptionKey) return VerificationReport::Incompatible_Protocol;
} else if (its_aid == aid::DEN) {
// clause 7.1.2: generationLocation present, signer certificate, nothing else
if (!header.generationLocation || header.expiryTime || header.encryptionKey) return VerificationReport::Incompatible_Protocol;
if (signed_data->signer.present != Vanetza_Security_SignerIdentifier_PR_certificate) return VerificationReport::Incompatible_Protocol;
}
return VerificationReport::Success;
}
namespace {
// IEEE Std 1609.2-2025 permission consistency along the chain, which the upstream
// validator reduces to "the issuer lists the ITS-AID". TS 103 097 V2.2.1 clause 5.2
// verifies an SPDU as IEEE Std 1609.2 clause 5.2 requires, and that includes the
// certificate chain consistency of clause 5.1.2:
// * PsidGroupPermissions (6.4.28): minChainLength/chainLengthRange bound the length of
// the chain from that certificate down to and including the end entity (-1: no upper
// bound; 0 is invalid in certIssuePermissions); eeType must permit an authorization
// certificate (app), absent eeType defaulting to {app}.
// * SubjectPermissions (6.4.29): "all" covers every PSID not indicated explicitly by
// another group of the same certificate; "explicit" lists PsidSspRange values.
// * SspRange consistency (6.4.29/6.4.30): an omitted ssp needs a range "all" (or an
// empty opaque entry); an opaque ssp must duplicate one opaque entry; a bitmapSsp
// must equal sspValue in every bit position where sspBitmask is 1 and may not be
// shorter than the last 1 bit of the mask nor longer than the mask.
// * A subordinate CA's own PsidSspRange must nest inside its issuer's (6.4.30): the
// issuer's range is "all", or for every 1 bit of the issuer's mask the subordinate's
// mask bit is 1 and its value bit equals the issuer's; opaque entries must duplicate
// the issuer's; "all" needs "all".
// Unknown CHOICE alternatives are critical information (5.2.6): fail closed.
namespace consistency {
using Group = Vanetza_Security_PsidGroupPermissions_t;
using Groups = Vanetza_Security_SequenceOfPsidGroupPermissions_t;
using Range = Vanetza_Security_PsidSspRange_t;
using Ssp = Vanetza_Security_ServiceSpecificPermissions_t;
bool octets_equal(const OCTET_STRING_t& a, const OCTET_STRING_t& b) {
return a.size == b.size && (a.size == 0 || std::equal(a.buf, a.buf + a.size, b.buf));
}
bool ee_type_app(const Group& group) {
if (!group.eeType || group.eeType->size == 0) return true; // DEFAULT {app}
return (group.eeType->buf[0] & 0x80) != 0; // bit 0: app
}
bool chain_length_permits(const Group& group, unsigned depth) {
const long min = group.minChainLength ? *group.minChainLength : 1;
const long range = group.chainLengthRange;
if (min < 1 || depth < static_cast<unsigned long>(min)) return false;
return range < 0 || depth <= static_cast<unsigned long>(min + range);
}
const Range* find_range(const Group& group, long psid) {
if (group.subjectPermissions.present != Vanetza_Security_SubjectPermissions_PR_explicit) return nullptr;
const auto& list = group.subjectPermissions.choice.Explicit.list;
for (int i = 0; i < list.count; ++i) {
if (list.array[i] && list.array[i]->psid == psid) return list.array[i];
}
return nullptr;
}
bool listed_explicitly(const Groups& groups, long psid) {
for (int i = 0; i < groups.list.count; ++i) {
if (groups.list.array[i] && find_range(*groups.list.array[i], psid)) return true;
}
return false;
}
// end-entity ssp (nullptr: omitted) within a PsidSspRange's sspRange (nullptr: all)
bool ssp_within(const Ssp* ssp, const Vanetza_Security_SspRange_t* range) {
if (!range || range->present == Vanetza_Security_SspRange_PR_all) return true;
if (range->present == Vanetza_Security_SspRange_PR_opaque) {
const auto& entries = range->choice.opaque.list;
for (int i = 0; i < entries.count; ++i) {
const OCTET_STRING_t* entry = entries.array[i];
if (!entry) continue;
if (!ssp) { if (entry->size == 0) return true; }
else if (ssp->present == Vanetza_Security_ServiceSpecificPermissions_PR_opaque &&
octets_equal(*entry, ssp->choice.opaque)) return true;
}
return false;
}
if (range->present == Vanetza_Security_SspRange_PR_bitmapSspRange) {
if (!ssp || ssp->present != Vanetza_Security_ServiceSpecificPermissions_PR_bitmapSsp) return false;
const OCTET_STRING_t& value = range->choice.bitmapSspRange.sspValue;
const OCTET_STRING_t& mask = range->choice.bitmapSspRange.sspBitmask;
const OCTET_STRING_t& bits = ssp->choice.bitmapSsp;
if (value.size != mask.size || bits.size > mask.size) return false;
for (std::size_t i = 0; i < mask.size; ++i) {
if (i >= bits.size) { if (mask.buf[i]) return false; continue; }
if ((bits.buf[i] & mask.buf[i]) != (value.buf[i] & mask.buf[i])) return false;
}
return true;
}
return false;
}
// an ancestor's certIssuePermissions cover one appPermissions entry of the end entity at
// the given chain length
bool covers(const Groups& groups, const Vanetza_Security_PsidSsp_t& entry, unsigned depth) {
const bool elsewhere = listed_explicitly(groups, entry.psid);
for (int i = 0; i < groups.list.count; ++i) {
const Group* group = groups.list.array[i];
if (!group || !ee_type_app(*group) || !chain_length_permits(*group, depth)) continue;
if (group->subjectPermissions.present == Vanetza_Security_SubjectPermissions_PR_all) {
if (!elsewhere) return true;
} else if (const Range* range = find_range(*group, entry.psid)) {
if (ssp_within(entry.ssp, range->sspRange)) return true;
}
}
return false;
}
// a subordinate CA's PsidSspRange nests inside the issuer's range for the same PSID
bool range_within(const Vanetza_Security_SspRange_t* sub, const Vanetza_Security_SspRange_t* issuer) {
if (!issuer || issuer->present == Vanetza_Security_SspRange_PR_all) return true;
if (!sub || sub->present == Vanetza_Security_SspRange_PR_all) return false;
if (issuer->present == Vanetza_Security_SspRange_PR_opaque) {
if (sub->present != Vanetza_Security_SspRange_PR_opaque) return false;
const auto& subs = sub->choice.opaque.list;
const auto& issuers = issuer->choice.opaque.list;
for (int i = 0; i < subs.count; ++i) {
bool found = false;
for (int j = 0; j < issuers.count && !found; ++j) {
found = subs.array[i] && issuers.array[j] && octets_equal(*subs.array[i], *issuers.array[j]);
}
if (!found) return false;
}
return true;
}
if (issuer->present == Vanetza_Security_SspRange_PR_bitmapSspRange) {
if (sub->present != Vanetza_Security_SspRange_PR_bitmapSspRange) return false;
const auto& r = issuer->choice.bitmapSspRange;
const auto& p = sub->choice.bitmapSspRange;
if (r.sspValue.size != r.sspBitmask.size || p.sspValue.size != p.sspBitmask.size) return false;
for (std::size_t i = 0; i < r.sspBitmask.size; ++i) {
const std::uint8_t fixed = r.sspBitmask.buf[i];
if (!fixed) continue;
if (i >= p.sspBitmask.size) return false;
if ((p.sspBitmask.buf[i] & fixed) != fixed) return false;
if ((p.sspValue.buf[i] & fixed) != (r.sspValue.buf[i] & fixed)) return false;
}
return true;
}
return false;
}
bool nests(const Groups& sub, const Groups& issuer) {
for (int i = 0; i < sub.list.count; ++i) {
const Group* group = sub.list.array[i];
if (!group) continue;
if (group->subjectPermissions.present == Vanetza_Security_SubjectPermissions_PR_all) {
bool found = false;
for (int j = 0; j < issuer.list.count && !found; ++j) {
found = issuer.list.array[j] &&
issuer.list.array[j]->subjectPermissions.present == Vanetza_Security_SubjectPermissions_PR_all;
}
if (!found) return false;
continue;
}
if (group->subjectPermissions.present != Vanetza_Security_SubjectPermissions_PR_explicit) return false;
const auto& ranges = group->subjectPermissions.choice.Explicit.list;
for (int k = 0; k < ranges.count; ++k) {
const Range* range = ranges.array[k];
if (!range) continue;
const bool elsewhere = listed_explicitly(issuer, range->psid);
bool found = false;
for (int j = 0; j < issuer.list.count && !found; ++j) {
const Group* candidate = issuer.list.array[j];
if (!candidate) continue;
if (candidate->subjectPermissions.present == Vanetza_Security_SubjectPermissions_PR_all) found = !elsewhere;
else if (const Range* r = find_range(*candidate, range->psid)) found = range_within(range->sspRange, r->sspRange);
}
if (!found) return false;
}
}
return true;
}
// chain[0] is the end entity, chain.back() the anchor
bool chain_consistent(const std::vector<const Vanetza_Security_EtsiTs103097Certificate_t*>& chain) {
if (chain.empty()) return false;
const auto* app = chain.front()->toBeSigned.appPermissions;
if (!app) return false;
for (std::size_t depth = 1; depth < chain.size(); ++depth) {
const Groups* issuing = chain[depth]->toBeSigned.certIssuePermissions;
if (!issuing) return false;
for (int i = 0; i < app->list.count; ++i) {
if (!app->list.array[i] || !covers(*issuing, *app->list.array[i], depth)) return false;
}
}
for (std::size_t k = 1; k + 1 < chain.size(); ++k) { // subordinate CAs below the anchor
const Groups* sub = chain[k]->toBeSigned.certIssuePermissions;
const Groups* issuer = chain[k + 1]->toBeSigned.certIssuePermissions;
if (!sub || !issuer || !nests(*sub, *issuer)) return false;
}
return true;
}
// IEEE Std 1609.2-2025 GeographicRegion (6.4.17): "a certificate is not valid if any part
// of the region indicated in its scope field lies outside the region indicated in the
// scope of its issuer". Geometric issuer regions are decided by the upstream geometry
// (CertificateView::region_is_within). An identifiedRegion issuer (6.4.21 to 6.4.24:
// countryOnly, countryAndRegions, countryAndSubregions) contains an identifiedRegion
// subject when every subject entry lies in an issuer entry of the same country: a whole
// country covers everything in it, regions cover their subregions, lists must nest.
// Whether a circle, rectangle or polygon lies inside a country needs a border database
// this library does not carry; that case follows the station's
// VerificationPolicy::permissive_identified_region, as the location check does.
using Identified = Vanetza_Security_IdentifiedRegion_t;
bool contains_all(const Vanetza_Security_SequenceOfUint8_t& outer, const Vanetza_Security_SequenceOfUint8_t& inner) {
for (int i = 0; i < inner.list.count; ++i) {
bool found = false;
for (int j = 0; j < outer.list.count && !found; ++j) found = inner.list.array[i] && outer.list.array[j] && *inner.list.array[i] == *outer.list.array[j];
if (!found) return false;
}
return true;
}
bool contains_all(const Vanetza_Security_SequenceOfUint16_t& outer, const Vanetza_Security_SequenceOfUint16_t& inner) {
for (int i = 0; i < inner.list.count; ++i) {
bool found = false;
for (int j = 0; j < outer.list.count && !found; ++j) found = inner.list.array[i] && outer.list.array[j] && *inner.list.array[i] == *outer.list.array[j];
if (!found) return false;
}
return true;
}
// region r (with the given subregions, nullptr: the whole region) inside one issuer entry
bool region_in_entry(long country, long region, const Vanetza_Security_SequenceOfUint16_t* subregions, const Identified& entry) {
switch (entry.present) {
case Vanetza_Security_IdentifiedRegion_PR_countryOnly:
return entry.choice.countryOnly == country;
case Vanetza_Security_IdentifiedRegion_PR_countryAndRegions: {
if (entry.choice.countryAndRegions.countryOnly != country) return false;
const auto& regions = entry.choice.countryAndRegions.regions.list;
for (int j = 0; j < regions.count; ++j) if (regions.array[j] && *regions.array[j] == region) return true;
return false;
}
case Vanetza_Security_IdentifiedRegion_PR_countryAndSubregions: {
if (entry.choice.countryAndSubregions.country != country || !subregions) return false;
const auto& entries = entry.choice.countryAndSubregions.regionAndSubregions.list;
for (int j = 0; j < entries.count; ++j) {
if (entries.array[j] && entries.array[j]->region == region) return contains_all(entries.array[j]->subregions, *subregions);
}
return false;
}
default:
return false;
}
}
bool identified_within(const Vanetza_Security_SequenceOfIdentifiedRegion_t& inner, const Vanetza_Security_SequenceOfIdentifiedRegion_t& outer) {
for (int i = 0; i < inner.list.count; ++i) {
const Identified* entry = inner.list.array[i];
if (!entry) return false;
bool covered = false;
for (int j = 0; j < outer.list.count && !covered; ++j) {
const Identified* candidate = outer.list.array[j];
if (!candidate) continue;
switch (entry->present) {
case Vanetza_Security_IdentifiedRegion_PR_countryOnly:
covered = candidate->present == Vanetza_Security_IdentifiedRegion_PR_countryOnly &&
candidate->choice.countryOnly == entry->choice.countryOnly;
break;
case Vanetza_Security_IdentifiedRegion_PR_countryAndRegions: {
const auto& regions = entry->choice.countryAndRegions.regions.list;
covered = regions.count > 0;
for (int r = 0; r < regions.count && covered; ++r)
covered = regions.array[r] && region_in_entry(entry->choice.countryAndRegions.countryOnly, *regions.array[r], nullptr, *candidate);
break;
}
case Vanetza_Security_IdentifiedRegion_PR_countryAndSubregions: {
const auto& entries = entry->choice.countryAndSubregions.regionAndSubregions.list;
covered = entries.count > 0;
for (int r = 0; r < entries.count && covered; ++r)
covered = entries.array[r] && region_in_entry(entry->choice.countryAndSubregions.country, entries.array[r]->region,
&entries.array[r]->subregions, *candidate);
break;
}
default:
covered = false;
}
}
if (!covered) return false;
}
return true;
}
bool region_within(const Vanetza_Security_EtsiTs103097Certificate_t& subject, const Vanetza_Security_EtsiTs103097Certificate_t& issuer,
bool permissive_identified_region) {
const auto* outer = issuer.toBeSigned.region;
const auto* inner = subject.toBeSigned.region;
if (!outer) return true;
if (!inner) return false;
if (outer->present != Vanetza_Security_GeographicRegion_PR_identifiedRegion)
return v3::CertificateView(&subject).region_is_within(v3::CertificateView(&issuer)); // upstream geometry
switch (inner->present) {
case Vanetza_Security_GeographicRegion_PR_identifiedRegion:
return identified_within(inner->choice.identifiedRegion, outer->choice.identifiedRegion);
case Vanetza_Security_GeographicRegion_PR_circularRegion:
case Vanetza_Security_GeographicRegion_PR_rectangularRegion:
case Vanetza_Security_GeographicRegion_PR_polygonalRegion:
return permissive_identified_region; // no border database: policy
default:
return false;
}
}
} // namespace consistency
} // namespace
bool chain_permissions_consistent(const std::vector<const Certificate*>& chain) {
std::vector<const Vanetza_Security_EtsiTs103097Certificate_t*> raw;
for (const auto* certificate : chain) {
if (!certificate || !certificate->content()) return false;
raw.push_back(certificate->content());
}
return consistency::chain_consistent(raw);
}
bool region_within(const Certificate& subject, const Certificate& issuer, bool permissive_identified_region) {
if (!subject.content() || !issuer.content()) return false;
return consistency::region_within(*subject.content(), *issuer.content(), permissive_identified_region);
}
// ---- SecurityEntity ---------------------------------------------------------
#if VIDF_SECURITY_VERIFY
namespace {
// Issuer lookup over the provisioned authorities and the ones learned by P2P distribution.
class CombinedIssuerLookup : public v3::IssuerLookup {
public:
explicit CombinedIssuerLookup(const v3::IssuerLookup& provisioned) : provisioned_(provisioned) {}
const Certificate* find_issuer(const HashedId8& digest) const override {
if (const auto* found = provisioned_.find_issuer(digest)) return found;
for (const auto& learned : learned_) {
if (learned.calculate_digest() == digest) return &learned;
}
return nullptr;
}
std::deque<Certificate> learned_;
private:
const v3::IssuerLookup& provisioned_;
};
// TS 102 940 clause 6 chain: the upstream validator checks anchoring, time, ITS-AID and
// assurance consistency; this adds the certificate signatures up the chain (IEEE Std
// 1609.2 clause 5.3.1), the permission consistency of clause 5.1.2 and the region
// consistency of 6.4.17 (above; the upstream region check knows no identifiedRegion
// issuer and is switched off), remembering tickets already verified.
class ChainValidator {
public:
using Verdict = v3::CertificateValidator::Verdict;
ChainValidator(Backend& backend, v3::DefaultCertificateValidator& base, const v3::IssuerLookup& issuers) :
backend_(backend), base_(base), issuers_(issuers) {}
std::size_t capacity = 64;
bool permissive_identified_region = true;
Verdict valid_for_signing(const Vanetza_Security_EtsiTs103097Certificate_t& signing_cert, ItsAid its_aid) {
const v3::CertificateView view { &signing_cert };
const auto verdict = base_.valid_for_signing(view, its_aid);
if (verdict != Verdict::Valid) return verdict;
const auto digest = view.calculate_digest();
if (!digest) return Verdict::Untrusted;
if (verified_.count(*digest)) return Verdict::Valid;
// walk up to a self-signed anchor, verifying every signature on the way
std::vector<const Vanetza_Security_EtsiTs103097Certificate_t*> chain {&signing_cert};
bool anchored = false;
for (unsigned depth = 0; depth < 4 && !anchored; ++depth) {
const Vanetza_Security_EtsiTs103097Certificate_t* subject = chain.back();
const v3::CertificateView subject_view { subject };
if (subject_view.issuer_is_self()) {
if (!verify_certificate_signature(backend_, *subject, nullptr)) return Verdict::Untrusted;
anchored = true;
break;
}
const auto issuer_digest = subject_view.issuer_digest();
const Certificate* issuer = issuer_digest ? issuers_.find_issuer(*issuer_digest) : nullptr;
if (!issuer || !issuer->content()) return Verdict::Untrusted;
if (!verify_certificate_signature(backend_, *subject, issuer)) return Verdict::Untrusted;
chain.push_back(issuer->content());
anchored = issuer->issuer_is_self(); // the anchor was provisioned by the application
}
if (!anchored) return Verdict::Untrusted;
if (chain.size() > 1 && !consistency::chain_consistent(chain)) return Verdict::InconsistentChain;
for (std::size_t k = 0; k + 1 < chain.size(); ++k) {
if (!consistency::region_within(*chain[k], *chain[k + 1], permissive_identified_region)) return Verdict::InconsistentChain;
}
if (verified_.size() >= capacity) verified_.clear();
verified_.insert(*digest);
return Verdict::Valid;
}
private:
Backend& backend_;
v3::DefaultCertificateValidator& base_;
const v3::IssuerLookup& issuers_;
std::unordered_set<HashedId8> verified_;
};
} // namespace
#endif
class SecurityEntity::Impl {
public:
Runtime& runtime;
Backend& backend;
const TrustConfiguration& trust;
CertificatePool& pool;
v3::DefaultLocationChecker location_checker;
v3::DefaultCertificateValidator validator;
Ts103097SignHeaderPolicy policy;
v3::StraightSignService sign_service;
IdentityManager identity;
std::deque<SecurityEvent> events;
std::size_t event_capacity = 16;
Statistics stats;
VerificationPolicy verification;
#if VIDF_SECURITY_VERIFY
CombinedIssuerLookup issuers;
ChainValidator chain;
v3::CertificateCache cache;
std::deque<std::pair<HashedId8, v3::Time64>> accepted; // replay window
#endif
Impl(Runtime& rt, PositionProvider& position, Backend& be, CertificatePool& certificates,
const TrustConfiguration& tc) :
runtime(rt), backend(be), trust(tc), pool(certificates), policy(rt, position, certificates, tc),
sign_service(certificates, be, policy, validator), identity(rt, certificates)
#if VIDF_SECURITY_VERIFY
, issuers(tc.issuers()), chain(be, validator, issuers)
#endif
{
validator.use_runtime(&rt);
validator.use_position_provider(&position);
validator.use_location_checker(&location_checker);
validator.use_trust_store(&tc.roots());
validator.use_revocation_lookup(&tc.revocations()); // TS 102 941 clause 6.3.6 CRL use
location_checker.set_permissive_identified_region(verification.permissive_identified_region);
#if VIDF_SECURITY_VERIFY
validator.use_issuer_lookup(&issuers);
// upstream's is_within() has no identifiedRegion issuer case (an EU root would make
// every chain inconsistent); ChainValidator applies the region rule of 6.4.17 instead
validator.disable_region_consistency_checks(true);
chain.permissive_identified_region = verification.permissive_identified_region;
#else
validator.use_issuer_lookup(&tc.issuers());
#endif
identity.on_committed([this](const Identifier&) { policy.reset_after_identifier_change(); });
}
#if VIDF_SECURITY_VERIFY
// TS 103 097 clause 7.1.1: a requestedCertificate carries a CA certificate answering a
// P2P request; keep it once its signature chains to a provisioned or learned issuer.
void learn_authority(const Vanetza_Security_Certificate* asn) {
if (!asn) return;
Certificate candidate { *reinterpret_cast<const Vanetza_Security_EtsiTs103097Certificate_t*>(asn) };
if (!candidate.is_ca_certificate() || candidate.issuer_is_self()) return;
const auto digest = candidate.calculate_digest();
if (!digest || issuers.find_issuer(*digest)) return; // known already
const auto issuer_digest = candidate.issuer_digest();
const Certificate* issuer = issuer_digest ? issuers.find_issuer(*issuer_digest) : nullptr;
if (!issuer || !verify_certificate_signature(backend, candidate, issuer)) return;
while (issuers.learned_.size() >= std::max<std::size_t>(1, verification.learned_authority_limit)) issuers.learned_.pop_front();
issuers.learned_.push_back(std::move(candidate));
++stats.learned_authorities;
}
// IEEE Std 1609.2 clause 5.2 processing of an EtsiTs103097Data-Signed, following the
// upstream StraightVerifyService::verify(const v3::SecuredMessage&) step by step (that
// translation unit also carries the v2 path and is not compiled into the port): signer
// lookup (cache or inline certificate), P2P certificate distribution hooks of TS 103 097
// clause 7.1.1 through the header policy, ticket validity through the chain validator,
// message hash with the signing certificate (clause 5.3.1) and the ECDSA check.
VerifyConfirm verify_signed(const v3::SecuredMessage& msg) {
VerifyConfirm confirm;
confirm.report = VerificationReport::Incompatible_Protocol;
confirm.its_aid = msg.its_aid(); // header value until verified
const auto signature = msg.signature();
if (!signature) { confirm.report = VerificationReport::Unsigned_Message; return confirm; }
const auto signer_identifier = msg.signer_identifier();
const auto maybe_digest = v3::get_certificate_id(signer_identifier);
const ItsAid its_aid = msg.its_aid();
if (maybe_digest) {
// "known" station tracking: a CAM from a station seen for the first time makes the
// next own CAM carry the certificate (clause 7.1.1)
const bool was_unknown = cache.announce(*maybe_digest);
if (was_unknown && its_aid == aid::CA) policy.request_certificate();
}
if (its_aid == aid::CA) {
const auto& header = msg->content->choice.signedData->tbsData->headerInfo;
if (header.inlineP2pcdRequest) {
for (int i = 0; i < header.inlineP2pcdRequest->list.count; ++i)
policy.enqueue_p2p_request(create_hashed_id3(*header.inlineP2pcdRequest->list.array[i]));
}
if (header.requestedCertificate) {
if (const auto included = v3::calculate_digest(*reinterpret_cast<const Vanetza_Security_EtsiTs103097Certificate_t*>(header.requestedCertificate)))
policy.discard_p2p_request(truncate(*included));
}
}
const v3::asn1::Certificate* certificate = nullptr;
if (const auto* const* inline_cert = boost::get<const v3::asn1::Certificate*>(&signer_identifier)) {
certificate = *inline_cert;
} else if (maybe_digest) {
if (const auto* cached = cache.lookup(*maybe_digest)) certificate = cached->content();
}
if (!certificate) {
if (its_aid == aid::CA && maybe_digest) policy.request_unrecognized_certificate(*maybe_digest);
confirm.report = VerificationReport::Signer_Certificate_Not_Found;
return confirm;
}
const v3::CertificateView view { certificate };
const auto verdict = chain.valid_for_signing(*certificate, its_aid);
if (verdict != ChainValidator::Verdict::Valid) {
// an AT issued by an unknown AA: ask for the AA (clause 7.1.1 inlineP2pcdRequest)
if (const auto aa = view.issuer_digest()) {
if (!issuers.find_issuer(*aa) && !cache.is_known(*aa)) policy.request_unrecognized_certificate(*aa);
}
// TS 102 723-8 V1.1.1 Table 27 report codes: INVALID_CERTIFICATE for the certificate
// itself, INCONSISTENT_CHAIN when the chain's permissions do not fit (IEEE 1609.2 5.1.2)
confirm.report = VerificationReport::Invalid_Certificate;
switch (verdict) {
case ChainValidator::Verdict::Revoked: confirm.report = VerificationReport::Revoked_Certificate; break;
case ChainValidator::Verdict::Expired: confirm.certificate_validity = CertificateInvalidReason::Off_Time_Period; break;
case ChainValidator::Verdict::Untrusted: confirm.certificate_validity = CertificateInvalidReason::Unknown_Signer; break;
case ChainValidator::Verdict::InconsistentChain:
confirm.report = VerificationReport::Inconsistent_Chain;
confirm.certificate_validity = CertificateInvalidReason::Inconsistent_With_Signer;
break;
case ChainValidator::Verdict::OutsideRegion: confirm.certificate_validity = CertificateInvalidReason::Off_Region; break;
case ChainValidator::Verdict::InsufficientPermission: confirm.certificate_validity = CertificateInvalidReason::Insufficient_ITS_AID; break;
default: break;
}
return confirm;
}
const auto public_key = v3::get_public_key(*certificate);
if (!public_key) {
confirm.report = VerificationReport::Invalid_Certificate;
confirm.certificate_validity = CertificateInvalidReason::Missing_Public_Key;
return confirm;
}
const ByteBuffer digest = v3::calculate_message_hash(backend, msg.hash_id(), msg.signing_payload(), view);
if (!backend.verify_digest(*public_key, digest, *signature)) {
confirm.report = VerificationReport::False_Signature;
return confirm;
}
confirm.its_aid = its_aid;
confirm.permissions = v3::get_app_permissions(*certificate, its_aid);
confirm.certificate_id = maybe_digest ? maybe_digest : view.calculate_digest();
confirm.report = VerificationReport::Success;
if (confirm.certificate_id && v3::contains_certificate(signer_identifier) && !cache.lookup(*confirm.certificate_id)) {
if (its_aid == aid::CA) policy.request_certificate(); // clause 7.1.1: first CAM from this station
cache.store(v3::Certificate { *certificate });
}
return confirm;
}
DecapConfirm verify(const v3::SecuredMessage& msg, const DecapRequest& request) {
const auto profile = check_profile(msg);
if (profile != VerificationReport::Success) {
++stats.rejected_profile;
DecapConfirm confirm;
confirm.report = profile;
confirm.its_aid = msg.its_aid();
confirm.plaintext_payload = get_payload_copy(request.sec_packet);
return confirm;
}
if (msg->content->choice.signedData->tbsData->headerInfo.requestedCertificate)
learn_authority(msg->content->choice.signedData->tbsData->headerInfo.requestedCertificate);
auto verified = verify_signed(msg);
if (verified.report == VerificationReport::Success) {
// generationTime plausibility and replay (VerificationPolicy)
const auto generation = msg.generation_time();
const auto now = v3::convert_time64(runtime.now());
const auto future = static_cast<v3::Time64>(std::chrono::duration_cast<std::chrono::microseconds>(verification.generation_time_future_tolerance).count());
const auto age = static_cast<v3::Time64>(std::chrono::duration_cast<std::chrono::microseconds>(verification.generation_time_max_age).count());
if (!generation || *generation > now + future || *generation + age < now) {
++stats.rejected_time;
verified.report = VerificationReport::Invalid_Timestamp;
} else if (verification.replay_window && verified.certificate_id) {
const std::pair<HashedId8, v3::Time64> key {*verified.certificate_id, *generation};
if (std::find(accepted.begin(), accepted.end(), key) != accepted.end()) {
++stats.replayed;
verified.report = VerificationReport::Duplicate_Message;
} else {
while (accepted.size() >= verification.replay_window) accepted.pop_front();
accepted.push_back(key);
}
}
}
switch (verified.report) {
case VerificationReport::Success: ++stats.verified; break;
case VerificationReport::Invalid_Timestamp: case VerificationReport::Duplicate_Message: break; // counted above
case VerificationReport::Signer_Certificate_Not_Found: case VerificationReport::Unsupported_Signer_Identifier_Type: ++stats.rejected_signer; break;
case VerificationReport::False_Signature: ++stats.rejected_signature; break;
default: ++stats.rejected_certificate; break;
}
if (cache.size() > verification.certificate_cache_limit) cache = v3::CertificateCache {}; // bounded on the device
return DecapConfirm::from(std::move(verified), request.sec_packet);
}
#endif
};
SecurityEntity::SecurityEntity(Runtime& rt, PositionProvider& position, Backend& backend, CertificatePool& pool,
const TrustConfiguration& trust) :
impl_(std::make_unique<Impl>(rt, position, backend, pool, trust)) {}
SecurityEntity::~SecurityEntity() = default;
EncapConfirm SecurityEntity::encapsulate_packet(EncapRequest&& request) {
// Table 24 -> Table 25. Fail closed without a ticket or during PREPARE..COMMIT.
auto& stats = impl_->stats;
if (impl_->pool.empty()) {
++stats.refused_no_ticket;
return EncapConfirm::from(SignConfirm::failure(SignConfirmError::No_Certificate));
}
if (impl_->identity.change_pending()) {
++stats.refused_change_pending;
return EncapConfirm::from(SignConfirm::failure(SignConfirmError::No_Certificate));
}
try {
auto confirm = dispatch(std::move(request), &impl_->sign_service);
if (confirm.secured_message()) ++stats.signed_messages;
else ++stats.refused_permission; // validator verdict != Valid for this ITS-AID
return confirm;
} catch (const std::exception&) {
++stats.failed;
return EncapConfirm::from(SignConfirm::failure(SignConfirmError::Unspecified));
}
}
const SecurityEntity::Statistics& SecurityEntity::statistics() const { return impl_->stats; }
DecapConfirm SecurityEntity::decapsulate_packet(DecapRequest&& request) {
// Table 26 -> Table 27.
struct Visitor : boost::static_visitor<DecapConfirm> {
Impl& impl;
const DecapRequest& request;
Visitor(Impl& i, const DecapRequest& r) : impl(i), request(r) {}
DecapConfirm operator()(const v2::SecuredMessage& msg) const {
DecapConfirm confirm;
confirm.report = VerificationReport::Incompatible_Protocol; // TS 103 097 v1.x envelope
confirm.its_aid = 0;
confirm.plaintext_payload = get_payload_copy(SecuredMessage {msg});
return confirm;
}
DecapConfirm operator()(const v3::SecuredMessage& msg) const {
#if VIDF_SECURITY_VERIFY
try {
return impl.verify(msg, request);
} catch (const std::exception&) {
++impl.stats.failed;
DecapConfirm confirm;
confirm.report = VerificationReport::Incompatible_Protocol; // malformed structure
confirm.its_aid = msg.its_aid();
return confirm;
}
#else
// Without verification (GAP-SEC-001) the report never claims success.
(void)impl;
DecapConfirm confirm;
confirm.report = msg.is_signed() ? VerificationReport::Configuration_Problem
: VerificationReport::Unsigned_Message;
confirm.its_aid = msg.its_aid(); // header value, not verified
confirm.certificate_id = msg.certificate_id();
confirm.plaintext_payload = msg.payload();
return confirm;
#endif
}
};
Visitor visitor(*impl_, request);
return request.sec_packet.apply_visitor(visitor);
}
void SecurityEntity::set_verification_policy(const VerificationPolicy& policy) {
impl_->verification = policy;
impl_->location_checker.set_permissive_identified_region(policy.permissive_identified_region);
#if VIDF_SECURITY_VERIFY
impl_->chain.permissive_identified_region = policy.permissive_identified_region;
#endif
#if VIDF_SECURITY_VERIFY
impl_->chain.capacity = std::max<std::size_t>(1, policy.verified_chain_cache);
#endif
}
const VerificationPolicy& SecurityEntity::verification_policy() const { return impl_->verification; }
const std::deque<Certificate>& SecurityEntity::learned_authorities() const {
#if VIDF_SECURITY_VERIFY
return impl_->issuers.learned_;
#else
static const std::deque<Certificate> none;
return none;
#endif
}
void SecurityEntity::log_security_event(SecurityEvent event) {
while (impl_->events.size() >= impl_->event_capacity) impl_->events.pop_front();
impl_->events.push_back(std::move(event));
}
std::deque<SecurityEvent> SecurityEntity::drain_security_events() {
std::deque<SecurityEvent> out;
out.swap(impl_->events);
return out;
}
void SecurityEntity::set_security_event_capacity(std::size_t capacity) {
impl_->event_capacity = std::max<std::size_t>(1, capacity);
while (impl_->events.size() > impl_->event_capacity) impl_->events.pop_front();
}
IdChangeService& SecurityEntity::id_change() { return impl_->identity; }
const IdChangeService& SecurityEntity::id_change() const { return impl_->identity; }
IdentityManager& SecurityEntity::identity_manager() { return impl_->identity; }
Ts103097SignHeaderPolicy& SecurityEntity::header_policy() { return impl_->policy; }
v3::DefaultCertificateValidator& SecurityEntity::validator() { return impl_->validator; }
CertificatePool& SecurityEntity::certificates() { return impl_->pool; }
} // namespace vanetza_idf::security
@@ -0,0 +1,36 @@
// Cryptographic digest adapter for upstream certificate/envelope helpers.
// Algorithms are supplied by ESP-IDF PSA Crypto or host OpenSSL, never a dummy.
#include <vanetza/security/sha.hpp>
#include <stdexcept>
#ifdef ESP_PLATFORM
#include <psa/crypto.h>
#else
#include <openssl/sha.h>
#endif
namespace vanetza::security {
Sha256Digest calculate_sha256_digest(const std::uint8_t* data, std::size_t size) {
Sha256Digest digest {};
#ifdef ESP_PLATFORM
std::size_t written = 0;
if (psa_crypto_init() != PSA_SUCCESS ||
psa_hash_compute(PSA_ALG_SHA_256, data, size, digest.data(), digest.size(), &written) != PSA_SUCCESS ||
written != digest.size()) throw std::runtime_error("SHA-256 unavailable");
#else
if (!SHA256(data, size, digest.data())) throw std::runtime_error("SHA-256 failed");
#endif
return digest;
}
Sha384Digest calculate_sha384_digest(const std::uint8_t* data, std::size_t size) {
Sha384Digest digest {};
#ifdef ESP_PLATFORM
std::size_t written = 0;
if (psa_crypto_init() != PSA_SUCCESS ||
psa_hash_compute(PSA_ALG_SHA_384, data, size, digest.data(), digest.size(), &written) != PSA_SUCCESS ||
written != digest.size()) throw std::runtime_error("SHA-384 unavailable");
#else
if (!SHA384(data, size, digest.data())) throw std::runtime_error("SHA-384 failed");
#endif
return digest;
}
}
@@ -0,0 +1,155 @@
// Signer identifier and header selection per TS 103 097 V2.2.1 clause 7.1 and
// TS 103 300-3 V2.3.1 clause 6.5.3. See the class comment in security.hpp.
#include <vanetza_idf/security.hpp>
#include <vanetza/common/position_fix.hpp>
#include <vanetza/security/peer_request_tracker.hpp>
#include <vanetza/security/sign_service.hpp>
#include <vanetza/security/v2/basic_elements.hpp>
#include <vanetza/security/v3/secured_message.hpp>
#include <chrono>
#include <map>
namespace vanetza::security::v3 {
// Defined in the upstream sign_header_policy.cpp translation unit (not declared in its header):
// ThreeDLocation from a PositionFix (IEEE 1609.2 clause 6.3.10 encoding).
asn1::ThreeDLocation build_location(const PositionFix& fix);
}
namespace vanetza_idf::security {
using namespace vanetza;
using namespace vanetza::security;
class Ts103097SignHeaderPolicy::Impl {
public:
const Runtime& runtime;
PositionProvider& positioning;
CertificatePool& pool;
const TrustConfiguration& trust;
// CAM, clause 7.1.1
boost::optional<Clock::time_point> cam_last_certificate;
bool cam_certificate_requested = false;
PeerRequestTracker incoming_requests; // inlineP2pcdRequest digests received from peers
PeerRequestTracker outgoing_requests; // certificates unknown to us
// VAM, TS 103 300-3 clause 6.5.3
boost::optional<Clock::time_point> vam_last_certificate;
bool new_cam_signer = false;
// generic profile: per ITS-AID
std::map<ItsAid, Clock::time_point> generic_last_certificate;
Impl(const Runtime& rt, PositionProvider& pp, CertificatePool& certificates, const TrustConfiguration& t) :
runtime(rt), positioning(pp), pool(certificates), trust(t) {}
static constexpr std::chrono::milliseconds one_second {1000};
static constexpr std::chrono::milliseconds half_second {500};
template<class Optional>
static bool elapsed(const Optional& last, Clock::time_point now, std::chrono::milliseconds interval) {
return !last || now - *last >= interval;
}
void cam(const SignRequest&, v3::SecuredMessage& message, const Certificate& at, Clock::time_point now) {
const auto digest = at.calculate_digest();
// Inline P2PCD request naming our own AT: include the certificate immediately.
if (digest && incoming_requests.is_pending(truncate(*digest))) {
cam_certificate_requested = true;
incoming_requests.discard_request(truncate(*digest));
}
bool full_certificate = cam_certificate_requested || elapsed(cam_last_certificate, now, one_second);
if (full_certificate) {
message.set_signer_identifier(at);
cam_last_certificate = now; // "the timer for the next inclusion ... shall be restarted"
cam_certificate_requested = false;
} else {
message.set_signer_identifier(*digest);
}
// Digests of certificates unknown to this station (clause 7.1.1, inlineP2pcdRequest).
message.set_inline_p2pcd_request(outgoing_requests.all());
// requestedCertificate: a known CA certificate that a peer asked for, only while our
// signer is the digest ("unless the component signer ... is of choice certificate").
if (!full_certificate) {
while (auto requested = incoming_requests.next_one()) {
for (const auto& authority : trust.authorities()) {
const auto authority_digest = authority.calculate_digest();
if (authority_digest && truncate(*authority_digest) == *requested) {
message.set_requested_certificate(authority);
return;
}
}
}
}
}
void denm(v3::SecuredMessage& message, const Certificate& at) {
message.set_signer_identifier(at);
message.set_generation_location(v3::build_location(positioning.position_fix()));
}
void vam(const SignRequest& request, v3::SecuredMessage& message, const Certificate& at, Clock::time_point now) {
const bool cluster = request.context_information == context::vam_cluster;
bool attach;
if (cluster) {
attach = elapsed(vam_last_certificate, now, half_second);
} else {
attach = new_cam_signer || elapsed(vam_last_certificate, now, one_second);
}
if (attach) {
message.set_signer_identifier(at);
vam_last_certificate = now;
new_cam_signer = false;
} else if (auto digest = at.calculate_digest()) {
message.set_signer_identifier(*digest);
} else {
message.set_signer_identifier(at);
}
}
void generic(ItsAid aid, v3::SecuredMessage& message, const Certificate& at, Clock::time_point now) {
auto it = generic_last_certificate.find(aid);
const auto digest = at.calculate_digest();
if (it == generic_last_certificate.end() || now - it->second >= one_second || !digest) {
message.set_signer_identifier(at);
generic_last_certificate[aid] = now;
} else {
message.set_signer_identifier(*digest);
}
}
};
Ts103097SignHeaderPolicy::Ts103097SignHeaderPolicy(const Runtime& rt, PositionProvider& pp, CertificatePool& pool,
const TrustConfiguration& trust) :
impl_(std::make_unique<Impl>(rt, pp, pool, trust)) {}
Ts103097SignHeaderPolicy::~Ts103097SignHeaderPolicy() = default;
void Ts103097SignHeaderPolicy::prepare_header(const SignRequest& request, v3::SecuredMessage& message) {
const auto now = impl_->runtime.now();
// Clause 5.2: psid and generationTime always present.
message.set_its_aid(request.its_aid);
message.set_generation_time(v2::convert_time64(now));
const Certificate& at = impl_->pool.own_certificate();
if (request.self_signed) {
// TS 102 941 clause 6.2.3.2 inner enrolment structures are self-signed.
message.set_signer_identifier_self();
return;
}
switch (request.its_aid) {
case aid::CA: impl_->cam(request, message, at, now); break;
case aid::DEN: impl_->denm(message, at); break;
case aid::VRU: impl_->vam(request, message, at, now); break;
default: impl_->generic(request.its_aid, message, at, now); break;
}
}
void Ts103097SignHeaderPolicy::request_unrecognized_certificate(HashedId8 id) {
impl_->outgoing_requests.add_request(truncate(id));
}
void Ts103097SignHeaderPolicy::request_certificate() { impl_->cam_certificate_requested = true; }
void Ts103097SignHeaderPolicy::enqueue_p2p_request(HashedId3 id) { impl_->incoming_requests.add_request(id); }
void Ts103097SignHeaderPolicy::discard_p2p_request(HashedId3 id) { impl_->incoming_requests.discard_request(id); }
void Ts103097SignHeaderPolicy::report_new_cam_signer() { impl_->new_cam_signer = true; }
void Ts103097SignHeaderPolicy::reset_after_identifier_change() {
impl_->cam_last_certificate = boost::none;
impl_->cam_certificate_requested = false;
impl_->vam_last_certificate = boost::none;
impl_->generic_last_certificate.clear();
}
} // namespace vanetza_idf::security
@@ -0,0 +1,333 @@
#include <vanetza_idf/stack.hpp>
#if VIDF_SECURITY
#include <vanetza_idf/security.hpp>
#endif
#include <vanetza/access/access_category.hpp>
#include <vanetza/btp/header.hpp>
#include <vanetza/btp/header_conversion.hpp>
#include <vanetza/dcc/interface.hpp>
#include <vanetza/dcc/mapping.hpp>
#include <vanetza/dcc/data_request.hpp>
#include <vanetza/geonet/dcc_information_sharing.hpp>
#include <vanetza/geonet/router.hpp>
#include <vanetza/geonet/transport_interface.hpp>
#include <vanetza/net/packet.hpp>
#include <vanetza/net/packet_variant.hpp>
#include <vanetza/common/byte_view.hpp>
#include <algorithm>
#include <cmath>
#include <stdexcept>
#include <utility>
namespace vanetza_idf {
namespace gn = vanetza::geonet;
using namespace vanetza;
StackConfig::StackConfig() {
// Explicit ITS-G5 subset. Defaults are upstream MIB, not R2 certification.
mib.itsGnIfType = gn::InterfaceType::ITS_G5;
mib.itsGnSnDecapResultHandling = gn::SecurityDecapHandling::Strict;
mib.itsGnSecurity = true;
}
class Stack::Impl : public dcc::RequestInterface, public gn::TransportInterface {
public:
StackConfig cfg;
ManualRuntime& runtime;
Access& access;
vanetza::security::SecurityEntity* security;
security::IdChangeService* id_change;
gn::Router router;
// SYS-DCC-003 / GAP-DCC-001: Release-2 DCC_NET. Declared after router: it holds a
// reference to router's location table, which must already be constructed.
gn::DccInformationSharing dcc_information_sharing;
std::optional<dcc::ChannelLoad> global_cbr;
Receive receive;
ReceiveGn receive_gn;
Report report;
bool position_valid = false;
bool change_pending = false;
std::optional<security::SubscriptionHandle> subscription;
Impl(StackConfig c, ManualRuntime& rt, Access& al, vanetza::security::SecurityEntity* sec,
security::IdChangeService* ids) :
cfg(std::move(c)), runtime(rt), access(al), security(sec), id_change(ids), router(rt, cfg.mib),
// TS 103 836-4-2 clause 5.3: "all ITS-S shall start with a random time offset" so that
// stations do not all recompute CBR_G in lockstep. No RNG dependency (host and ESP-IDF
// newlib without configured hardware entropy behave differently): the station's own
// startup time already differs from its neighbours', so its microsecond-level position
// within the 100 ms trigger interval (itsGNCBRGTriggerInterval) serves as that offset.
dcc_information_sharing(rt, router.get_location_table(), dcc::ChannelLoad(0.62),
UnitInterval(double(rt.now().time_since_epoch().count() % 100000) / 100000.0)) {
router.set_access_interface(this);
router.set_security_entity(sec);
router.set_dcc_field_generator(&dcc_information_sharing);
dcc_information_sharing.on_global_cbr_update = [this](const gn::CbrAggregator& agg) {
global_cbr = agg.get_global_cbr();
};
// MIB.itsGnLocalGnAddr is otherwise inert: Router::update_position only
// touches timestamp/latitude/longitude/speed/heading, never the address,
// and the router's own m_local_position_vector.gn_addr starts at
// vanetza::geonet::Address()'s default (all-zero mid, not manually
// configured) until this is called. Every GN packet's source position
// vector carries that address, so leaving this out silently transmits
// the wrong (default) station address regardless of what the caller
// configured -- undetected by BTP's official ATS, which never checks
// the source GN address, but not so for GeoNetworking's.
router.set_address(cfg.mib.itsGnLocalGnAddr);
router.set_transport_handler(gn::UpperProtocol::BTP_A, this);
router.set_transport_handler(gn::UpperProtocol::BTP_B, this);
router.set_transport_handler(gn::UpperProtocol::Unknown, this);
// TS 103 836-4-1 V2.2.1 clause 10.2.1.4: the GN core of an anonymously addressed
// station subscribes to the identifier-change service at startup (SN-IDCHANGE-SUBSCRIBE)
// and derives its MID from the identifier of the security entity (TS 102 940 clause 6.5).
if (cfg.mib.itsGnLocalAddrConfMethod == gn::AddrConfMethod::Anonymous && id_change) {
subscription = id_change->subscribe(
[this](security::IdChangeCommand command, const security::Identifier& id, const ByteBuffer&,
std::shared_ptr<security::IdChangeResponder> responder) { on_id_change(command, id, responder); });
const auto current = id_change->current_identifier();
if (current != security::Identifier {}) apply_identifier(current);
}
}
~Impl() override {
// clause 10.2.1.4: unsubscribe when the router shuts down (SN-IDCHANGE-UNSUBSCRIBE)
if (subscription && id_change) id_change->unsubscribe(*subscription);
}
// TS 102 940 V2.1.1 clause 6.5: the 48 least significant bits of the HashedId8 become the
// MAC-layer / GN MID identifier. A source address must be individual (IEEE Std 802 clause
// 8.2.2, I/G bit 0) and this one is not OUI-assigned (U/L bit 1).
void apply_identifier(const security::Identifier& id) {
MacAddress mid;
std::copy(id.begin() + 2, id.end(), mid.octets.begin());
mid.octets[0] = (mid.octets[0] & 0xfe) | 0x02;
cfg.mib.itsGnLocalGnAddr.mid(mid);
router.set_address(cfg.mib.itsGnLocalGnAddr);
}
// TS 102 723-8 V1.1.1 clauses 6.3.1.2 and 6.3.1.3: hook function of the GN core.
void on_id_change(security::IdChangeCommand command, const security::Identifier& id,
const std::shared_ptr<security::IdChangeResponder>& responder) {
switch (command) {
case security::IdChangeCommand::PREPARE:
change_pending = true;
router.flush_forwarding_buffers(); // caches shall be flushed
if (responder) responder->respond(true);
break;
case security::IdChangeCommand::COMMIT:
apply_identifier(id);
change_pending = false;
if (responder) responder->respond(true);
break;
case security::IdChangeCommand::ABORT:
change_pending = false;
break;
case security::IdChangeCommand::DEREG:
change_pending = false;
subscription.reset();
break;
}
}
void request(const dcc::DataRequest& req, std::unique_ptr<ChunkPacket> packet) override {
auto out = cfg.radio_parameters;
out.source = req.source;
out.destination = req.destination;
// DCC profile to IEEE 802.1D priority: upstream TS 102 687 mapping.
// DCC enforcement belongs to the injected Access adapter; no fake CBR.
out.priority = access::user_priority(dcc::map_profile_onto_ac(req.dcc_profile));
const auto view = create_byte_view(*packet, OsiLayer::Network, max_osi_layer());
out.data.assign(view.begin(), view.end());
auto result = validate(out, cfg.maximum_gnpdu);
if (result == Result::accepted) result = access.request(std::move(out));
if (report) report(result);
}
void indicate(const gn::DataIndication& ind, std::unique_ptr<UpPacket> packet) override {
if (ind.upper_protocol == gn::UpperProtocol::Unknown) {
// GN-DATA.indication for a packet with no registered upper protocol
// (Common Header next_header "Any"): the raw SDU, no BTP header to strip.
if (!receive_gn) return;
const auto view = create_byte_view(*packet, OsiLayer::Transport, max_osi_layer());
receive_gn(GnIndication {ind, ByteBuffer(view.begin(), view.end())});
return;
}
// IF-NF-002: validate length BEFORE parsing; upstream header parsing
// alone does not reject a short BTP PDU. Never read an incomplete port.
const auto view = create_byte_view(*packet, OsiLayer::Transport, max_osi_layer());
if (view.size() < 4 || !receive) return;
BtpIndication out {};
out.type = ind.upper_protocol == gn::UpperProtocol::BTP_A ? BtpType::a : BtpType::b;
out.destination_port = (std::uint16_t(view[0]) << 8) | view[1];
const std::uint16_t second = (std::uint16_t(view[2]) << 8) | view[3];
if (out.type == BtpType::a) out.source_port = second;
else out.destination_port_info = second;
out.gn = ind;
// SN-DECAP.confirm -> NF-SAP: TS 102 723-8 V2.0.0 clause 5 /
// V1.1.1 Table 27 and TS 103 836-5-1 Annex A.3.
if (ind.certificate_id)
out.certificate_id = ByteBuffer(ind.certificate_id->begin(), ind.certificate_id->end());
out.data.assign(view.begin() + 4, view.end());
receive(std::move(out));
}
};
Stack::Stack(StackConfig config, ManualRuntime& runtime, Access& access,
vanetza::security::SecurityEntity* security, security::IdChangeService* id_change) {
if (config.mib.itsGnMaxSduSize < 4 || config.mib.itsGnMaxSduSize > 65535 ||
config.maximum_gnpdu < config.mib.itsGnMaxSduSize ||
config.mib.itsGnIfType != gn::InterfaceType::ITS_G5 ||
config.mib.itsGnSnDecapResultHandling != gn::SecurityDecapHandling::Strict)
throw std::invalid_argument("Invalid ITS-G5 stack configuration");
#if VIDF_SECURITY
if (!id_change) {
if (auto* own = dynamic_cast<security::SecurityEntity*>(security)) id_change = &own->id_change();
}
#endif
impl_ = std::make_unique<Impl>(std::move(config), runtime, access, security, id_change);
}
Stack::~Stack() = default;
const StackConfig& Stack::config() const { return impl_->cfg; }
security::IdChangeService* Stack::id_change() { return impl_->subscription ? impl_->id_change : nullptr; }
vanetza::security::SecurityEntity* Stack::security_entity() { return impl_->security; }
bool Stack::identity_change_pending() const { return impl_->change_pending; }
const gn::Address& Stack::address() const { return impl_->cfg.mib.itsGnLocalGnAddr; }
Result Stack::set_address(const gn::Address& address) {
if (impl_->cfg.mib.itsGnLocalAddrConfMethod != gn::AddrConfMethod::Managed) return Result::unsupported;
if (impl_->change_pending) return Result::identity_change_pending;
impl_->cfg.mib.itsGnLocalGnAddr = address;
impl_->router.set_address(address);
return Result::accepted;
}
void Stack::on_receive(Receive receive) { impl_->receive = std::move(receive); }
void Stack::on_receive_gn(ReceiveGn receive) { impl_->receive_gn = std::move(receive); }
void Stack::on_access_result(Report report) { impl_->report = std::move(report); }
void Stack::report_local_channel_load(dcc::ChannelLoad load) { impl_->dcc_information_sharing.update_local_cbr(load); }
void Stack::report_tx_power(unsigned dbm) { impl_->dcc_information_sharing.set_tx_power(dbm); }
std::optional<dcc::ChannelLoad> Stack::global_channel_busy_ratio() const { return impl_->global_cbr; }
Result Stack::advance(Clock::time_point time) {
if (time < impl_->runtime.now()) return Result::time_regression;
impl_->runtime.trigger(time);
return Result::accepted;
}
Result Stack::update_position(const PositionFix& fix) {
if (!has_horizontal_position(fix) || fix.latitude.value() < -90 || fix.latitude.value() > 90 ||
fix.longitude.value() < -180 || fix.longitude.value() > 180 ||
fix.timestamp > impl_->runtime.now()) return Result::invalid_argument;
impl_->router.update_position(fix);
impl_->position_valid = true;
return Result::accepted;
}
Result Stack::indicate(AlDataIndication ind) {
if (ind.data.empty() || ind.data.size() > impl_->cfg.maximum_gnpdu) return Result::invalid_argument;
try {
auto packet = std::make_unique<UpPacket>(CohesivePacket(std::move(ind.data), OsiLayer::Network));
impl_->router.indicate(std::move(packet), ind.source, ind.destination);
return Result::accepted; // submitted for processing, NOT a receive verdict
} catch (const std::bad_alloc&) { return Result::resource_limit; }
catch (const std::exception&) { return Result::rejected; }
}
Result Stack::request(BtpRequest req) {
// IF-NF-001: mutually exclusive BTP header variants and conditional fields.
if ((req.type != BtpType::a && req.type != BtpType::b) ||
(req.type == BtpType::a && (!req.source_port || req.destination_port_info)) ||
(req.type == BtpType::b && req.source_port)) return Result::invalid_argument;
if (req.data.size() > impl_->cfg.mib.itsGnMaxSduSize - 4u) return Result::resource_limit;
if (req.communication_profile != gn::CommunicationProfile::ITS_G5 &&
req.communication_profile != gn::CommunicationProfile::Unspecified) return Result::unsupported;
if (req.security_profile && *req.security_profile != impl_->cfg.security_profile) return Result::unsupported;
if (impl_->cfg.mib.itsGnSecurity && !impl_->security) return Result::security_unavailable;
if (impl_->change_pending) return Result::identity_change_pending;
if (!impl_->position_valid) return Result::rejected;
if (req.maximum_hop_limit && (*req.maximum_hop_limit == 0 || *req.maximum_hop_limit > 255))
return Result::invalid_argument;
if (req.transport != gn::TransportType::SHB && req.transport != gn::TransportType::GBC)
return Result::unsupported; // Upstream GUC, GAC, TSB are unimplemented: GAP-GN-001.
if (req.transport == gn::TransportType::GBC && !boost::get<gn::Area>(&req.destination))
return Result::invalid_argument;
if (req.transport == gn::TransportType::SHB && !boost::get<std::nullptr_t>(&req.destination))
return Result::invalid_argument;
try {
auto packet = std::make_unique<DownPacket>();
// TS 103 836-5-1 clauses 7 and 8.2: both headers occupy four octets.
if (req.type == BtpType::a) {
btp::HeaderA header { host_cast(req.destination_port), host_cast(*req.source_port) };
(*packet)[OsiLayer::Transport] = header;
} else {
btp::HeaderB header { host_cast(req.destination_port), host_cast(req.destination_port_info.value_or(0)) };
(*packet)[OsiLayer::Transport] = header;
}
(*packet)[OsiLayer::Application] = std::move(req.data);
auto apply = [&](gn::DataRequest& gn_req) {
gn_req.upper_protocol = req.type == BtpType::a ? gn::UpperProtocol::BTP_A : gn::UpperProtocol::BTP_B;
gn_req.communication_profile = req.communication_profile;
gn_req.its_aid = req.its_aid;
gn_req.permissions = std::move(req.permissions);
gn_req.security_context = std::move(req.security_context);
gn_req.traffic_class = req.traffic_class;
if (req.maximum_lifetime) gn_req.maximum_lifetime = *req.maximum_lifetime;
if (req.maximum_hop_limit) gn_req.max_hop_limit = *req.maximum_hop_limit;
if (req.repetition) gn_req.repetition = *req.repetition;
};
gn::DataConfirm confirm;
if (req.transport == gn::TransportType::SHB) {
gn::ShbDataRequest gn_req(impl_->cfg.mib);
apply(gn_req);
confirm = impl_->router.request(gn_req, std::move(packet));
} else {
gn::GbcDataRequest gn_req(impl_->cfg.mib);
apply(gn_req);
gn_req.destination = boost::get<gn::Area>(req.destination);
confirm = impl_->router.request(gn_req, std::move(packet));
}
return confirm.accepted() ? Result::accepted : Result::rejected;
} catch (const std::bad_alloc&) { return Result::resource_limit; }
catch (const std::exception&) { return Result::rejected; }
}
Result Stack::request(GnRequest req) {
if (req.data.size() > impl_->cfg.mib.itsGnMaxSduSize) return Result::resource_limit;
if (req.communication_profile != gn::CommunicationProfile::ITS_G5 &&
req.communication_profile != gn::CommunicationProfile::Unspecified) return Result::unsupported;
if (impl_->cfg.mib.itsGnSecurity && !impl_->security) return Result::security_unavailable;
if (impl_->change_pending) return Result::identity_change_pending;
if (!impl_->position_valid) return Result::rejected;
if (req.maximum_hop_limit && (*req.maximum_hop_limit == 0 || *req.maximum_hop_limit > 255))
return Result::invalid_argument;
if (req.transport != gn::TransportType::SHB && req.transport != gn::TransportType::GBC)
return Result::unsupported; // Upstream GUC, GAC, TSB are unimplemented: GAP-GN-001.
if (req.transport == gn::TransportType::GBC && !boost::get<gn::Area>(&req.destination))
return Result::invalid_argument;
if (req.transport == gn::TransportType::SHB && !boost::get<std::nullptr_t>(&req.destination))
return Result::invalid_argument;
try {
auto packet = std::make_unique<DownPacket>();
(*packet)[OsiLayer::Application] = std::move(req.data);
auto apply = [&](gn::DataRequest& gn_req) {
gn_req.upper_protocol = gn::UpperProtocol::Unknown;
gn_req.communication_profile = req.communication_profile;
gn_req.its_aid = req.its_aid;
gn_req.permissions = std::move(req.permissions);
gn_req.security_context = std::move(req.security_context);
gn_req.traffic_class = req.traffic_class;
if (req.maximum_lifetime) gn_req.maximum_lifetime = *req.maximum_lifetime;
if (req.maximum_hop_limit) gn_req.max_hop_limit = *req.maximum_hop_limit;
if (req.repetition) gn_req.repetition = *req.repetition;
};
gn::DataConfirm confirm;
if (req.transport == gn::TransportType::SHB) {
gn::ShbDataRequest gn_req(impl_->cfg.mib);
apply(gn_req);
confirm = impl_->router.request(gn_req, std::move(packet));
} else {
gn::GbcDataRequest gn_req(impl_->cfg.mib);
apply(gn_req);
gn_req.destination = boost::get<gn::Area>(req.destination);
confirm = impl_->router.request(gn_req, std::move(packet));
}
return confirm.accepted() ? Result::accepted : Result::rejected;
} catch (const std::bad_alloc&) { return Result::resource_limit; }
catch (const std::exception&) { return Result::rejected; }
}
}
@@ -0,0 +1,40 @@
#pragma once
#include <cstdio>
#include <stdexcept>
#include <string>
#ifdef ESP_PLATFORM
#include <esp_heap_caps.h>
#endif
// Shared assertion helper of the port regression: one counter across all test
// translation units, failures throw so the first broken check stops the run.
namespace vidf_test {
inline unsigned checks = 0;
inline const char* section_name = "";
// Names the running test section so a device log shows where an exception came from;
// on the device the free heap is reported alongside (allocation failures surface as
// asn1c RC_FAIL results, indistinguishable from malformed data without this line).
inline void section(const char* name) {
section_name = name;
#ifdef ESP_PLATFORM
std::printf("-- %s (free heap %u, largest block %u)\n", name,
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_DEFAULT)),
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT)));
#else
std::printf("-- %s\n", name);
#endif
std::fflush(stdout);
}
inline void check(bool ok, const char* description) {
++checks;
if (!ok) throw std::runtime_error(description);
}
inline std::string hex(const void* data, std::size_t size) {
std::string result;
const char* digits = "0123456789abcdef";
const auto* bytes = static_cast<const unsigned char*>(data);
for (std::size_t i = 0; i < size; ++i) { result += digits[bytes[i] >> 4]; result += digits[bytes[i] & 15]; }
return result;
}
template<class Container> std::string hex(const Container& bytes) { return hex(bytes.data(), bytes.size()); }
}
@@ -0,0 +1,15 @@
[MODULE_PARAMETERS]
LibItsBtp_Pixits.PX_SOURCE_PORT := 1234
LibItsBtp_Pixits.PX_DESTINATION_PORT := 2009
LibItsBtp_Pixits.PX_DESTINATION_PORT_INFO := 43981
LibItsBtp_Pixits.PX_UNKNOWN_DESTINATION_PORT := 65534
LibItsGeoNetworking_Pics.PICS_GN_SECURITY := false
LibItsGeoNetworking_Pics.PICS_IS_IUT_SECURED := false
[LOGGING]
LogFile := "%e.%h-%r.%s"
FileMask := LOG_ALL
ConsoleMask := VERDICTOP | ERROR | WARNING | EXECUTOR
[EXECUTE]
ItsBtp_TestControl.control
@@ -0,0 +1,197 @@
// External TITAN SUT adapter for the official AtsBTP port interfaces.
// Testcase sources and verdict logic are unchanged. This adapter invokes a
// separate host/device SUT process and decodes its actual boundary outputs.
#include "UpperTesterPort_BTP.hh"
#include "BtpPort.hh"
#include <functional>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <stdexcept>
#include <unistd.h>
#include <poll.h>
#include <signal.h>
#include <sys/wait.h>
namespace {
using Bytes = std::vector<unsigned char>;
using namespace LibItsBtp__TypesAndValues;
std::function<void(const BtpInd&)> lower_indication;
std::function<void(const UtBtpEventInd&)> upper_indication;
unsigned read16(const Bytes& b, unsigned i) { return (b.at(i) << 8) | b.at(i + 1); }
void u16(Bytes& b, unsigned n) { b.push_back(n >> 8); b.push_back(n); }
void u32(Bytes& b, unsigned n) { for (int shift = 24; shift >= 0; shift -= 8) b.push_back(n >> shift); }
class Process {
pid_t pid_ = -1;
int input_ = -1, output_ = -1;
public:
~Process() {
if (input_ >= 0) close(input_);
if (output_ >= 0) close(output_);
if (pid_ > 0) { kill(pid_, SIGTERM); waitpid(pid_, nullptr, 0); }
}
Bytes exchange(const Bytes& bytes) {
if (pid_ < 0) {
const char* executable = std::getenv("VIDF_SUT_EXECUTABLE");
if (!executable) throw std::runtime_error("Set VIDF_SUT_EXECUTABLE to the external SUT process");
int in[2], out[2];
if (pipe(in) || pipe(out)) throw std::runtime_error("Cannot create SUT pipes");
pid_ = fork();
if (pid_ == 0) {
dup2(in[0], STDIN_FILENO); dup2(out[1], STDOUT_FILENO);
close(in[0]); close(in[1]); close(out[0]); close(out[1]);
const char* script = std::getenv("VIDF_SUT_SCRIPT");
const char* port = std::getenv("VIDF_SUT_PORT");
if (script && port) execl(executable, executable, script, "--port", port, static_cast<char*>(nullptr));
else execl(executable, executable, static_cast<char*>(nullptr));
_exit(127);
}
close(in[0]); close(out[1]); input_ = in[1]; output_ = out[0];
if (pid_ < 0) throw std::runtime_error("Cannot fork SUT process");
}
const char* digits = "0123456789abcdef";
std::string line;
for (auto b : bytes) { line += digits[b >> 4]; line += digits[b & 15]; }
line += '\n';
for (std::size_t sent = 0; sent < line.size();) {
const auto count = write(input_, line.data() + sent, line.size() - sent);
if (count <= 0) throw std::runtime_error("SUT pipe write failed");
sent += count;
}
line.clear();
while (line.size() <= 8192) {
pollfd fd {output_, POLLIN, 0};
if (poll(&fd, 1, 12000) <= 0) throw std::runtime_error("SUT response timeout");
char c;
if (read(output_, &c, 1) != 1) throw std::runtime_error("SUT process ended");
if (c == '\n') break;
if (c != '\r') line += c;
}
if (line.size() > 8192 || line.size() % 2) throw std::runtime_error("Invalid SUT response size");
Bytes result;
auto hex = [](char c) -> unsigned {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
throw std::runtime_error("Non-hex SUT response");
};
for (unsigned i = 0; i < line.size(); i += 2) result.push_back((hex(line[i]) << 4) | hex(line[i + 1]));
return result;
}
};
Process sut;
bool transact(const Bytes& command) {
const auto reply = sut.exchange(command);
if (reply.size() < 2) throw std::runtime_error("Truncated SUT response");
unsigned offset = 2;
for (unsigned n = 0; n < reply[1]; ++n) {
const auto kind = reply.at(offset);
const auto size = read16(reply, offset + 1);
offset += 3;
if (offset + size > reply.size()) throw std::runtime_error("Truncated SUT record");
Bytes b(reply.begin() + offset, reply.begin() + offset + size); offset += size;
if (kind == 1) {
// NT1 lower observation: GN Basic/Common/SHB headers precede BTP.
// No fields are copied from the stimulus to manufacture an output.
if (b.size() < 44 || (b[0] & 15) != 1 || b[5] != 0x50 ||
read16(b, 8) != b.size() - 40) throw std::runtime_error("Unexpected lower-layer GN frame");
BtpInd indication;
auto& packet = indication.msgIn();
if ((b[4] >> 4) == 1) {
packet.header().btpAHeader().destinationPort() = read16(b, 40);
packet.header().btpAHeader().sourcePort() = read16(b, 42);
} else if ((b[4] >> 4) == 2) {
packet.header().btpBHeader().destinationPort() = read16(b, 40);
packet.header().btpBHeader().destinationPortInfo() = read16(b, 42);
} else throw std::runtime_error("Unknown BTP upper protocol");
packet.payload() = OCTETSTRING(b.size() - 44, b.data() + 44);
if (lower_indication) lower_indication(indication);
} else if (kind == 2) {
if (b.size() < 5) throw std::runtime_error("Truncated BTP indication");
UtBtpEventInd indication;
indication.rawPayload() = OCTETSTRING(b.size() - 5, b.data() + 5);
if (upper_indication) upper_indication(indication);
} else throw std::runtime_error("Unexpected SUT record kind");
}
if (offset != reply.size()) throw std::runtime_error("Trailing SUT response bytes");
return reply[0] == 0;
}
}
namespace LibItsBtp__TestSystem {
UpperTesterPort::UpperTesterPort(const char* name) : UpperTesterPort_BASE(name) {}
UpperTesterPort::~UpperTesterPort() = default;
void UpperTesterPort::set_parameter(const char*, const char*) {}
void UpperTesterPort::Handle_Fd_Event_Error(int) {}
void UpperTesterPort::Handle_Fd_Event_Writable(int) {}
void UpperTesterPort::Handle_Fd_Event_Readable(int) {}
void UpperTesterPort::user_map(const char*) {
upper_indication = [this](const UtBtpEventInd& p) { incoming_message(p); };
}
void UpperTesterPort::user_unmap(const char*) { upper_indication = {}; }
void UpperTesterPort::user_start() {}
void UpperTesterPort::user_stop() {}
void UpperTesterPort::outgoing_send(const UtBtpInitialize&) {
try {
UtBtpResults result; result.utBtpInitializeResult() = transact({0}); incoming_message(result);
} catch (const std::exception& e) { TTCN_error("SUT initialization: %s", e.what()); }
}
void UpperTesterPort::outgoing_send(const UtBtpTrigger& trigger) {
try {
Bytes command {1};
if (trigger.get_selection() == UtBtpTrigger::ALT_btpA) {
command.push_back(0);
u16(command, static_cast<int>(trigger.btpA().btpAHeader().destinationPort()));
u16(command, static_cast<int>(trigger.btpA().btpAHeader().sourcePort()));
} else {
command.push_back(1);
u16(command, static_cast<int>(trigger.btpB().btpBHeader().destinationPort()));
u16(command, static_cast<int>(trigger.btpB().btpBHeader().destinationPortInfo()));
}
command.push_back(0); // unspecified generation payload chosen by test application
UtBtpResults result; result.utBtpTriggerResult() = transact(command); incoming_message(result);
} catch (const std::exception& e) { TTCN_error("SUT BTP trigger: %s", e.what()); }
}
BtpPort::BtpPort(const char* name) : BtpPort_BASE(name), _layer(nullptr) {}
BtpPort::~BtpPort() = default;
void BtpPort::set_parameter(const char*, const char*) {}
void BtpPort::Handle_Fd_Event_Error(int) {}
void BtpPort::Handle_Fd_Event_Writable(int) {}
void BtpPort::Handle_Fd_Event_Readable(int) {}
void BtpPort::user_map(const char*) { lower_indication = [this](const BtpInd& p) { incoming_message(p); }; }
void BtpPort::user_unmap(const char*) { lower_indication = {}; }
void BtpPort::user_start() {}
void BtpPort::user_stop() {}
void BtpPort::receiveMsg(const BtpInd& p, const params&) { incoming_message(p); }
void BtpPort::outgoing_send(const BtpReq& request) {
try {
const auto& packet = request.msgOut();
const bool type_b = packet.header().get_selection() == BtpHeader::ALT_btpBHeader;
Bytes btp;
if (type_b) {
u16(btp, static_cast<int>(packet.header().btpBHeader().destinationPort()));
u16(btp, static_cast<int>(packet.header().btpBHeader().destinationPortInfo()));
} else {
u16(btp, static_cast<int>(packet.header().btpAHeader().destinationPort()));
u16(btp, static_cast<int>(packet.header().btpAHeader().sourcePort()));
}
if (packet.payload().ispresent()) {
const OCTETSTRING& data = packet.payload()();
const unsigned char* ptr = data;
btp.insert(btp.end(), ptr, ptr + data.lengthof());
}
// Independent lower tester GN SHB carrier (TS 103 836-4-1 headers).
// Station/position are test PIXIT fixtures; the SUT never generates it.
Bytes gn {0x11, 0, 0x05, 1, static_cast<unsigned char>(type_b ? 0x20 : 0x10), 0x50, 0, 0x80};
u16(gn, btp.size()); gn.push_back(1); gn.push_back(0);
gn.insert(gn.end(), {0, 0, 2, 0, 0, 0, 0, 2});
u32(gn, 0); u32(gn, 520000000); u32(gn, 130000000);
u16(gn, 0); u16(gn, 0); u32(gn, 0);
gn.insert(gn.end(), btp.begin(), btp.end());
Bytes command {2, 2,0,0,0,0,2, 255,255,255,255,255,255};
command.insert(command.end(), gn.begin(), gn.end());
if (!transact(command)) TTCN_error("SUT rejected lower tester submission");
} catch (const std::exception& e) { TTCN_error("SUT lower tester: %s", e.what()); }
}
}
@@ -0,0 +1,7 @@
[
"TC_BTP_PGA_BV_01",
"TC_BTP_PGB_BV_01",
"TC_BTP_PGB_BV_02",
"TC_BTP_PP_BV_01",
"TC_BTP_PP_BV_02"
]
@@ -0,0 +1,56 @@
[MODULE_PARAMETERS]
// SHB source generation only (GAP-GN-001): the upstream router implements SHB
// and GBC, but this adapter currently wires up SHB triggering/observation only.
// Every PICS below that defaults to true in LibItsGeoNetworking_Pics.ttcn is
// explicitly disabled unless it is implemented and verified end to end.
// Must match the actual IUT GN address baked into hil_sut.cpp's Sut::reset()
// (config.mib.itsGnLocalGnAddr.mid only; type-of-address/station-type/reserved
// stay at vanetza::geonet::Address's defaults -- see etsi_geonetworking_adapter.cpp's
// iut_gn_address()). f_acGetLongPosVector compares the adapter's AcGnResponse
// against this exact value, so this is not an arbitrary PICS choice.
LibItsGeoNetworking_Pics.PICS_GN_LOCAL_GN_ADDR := {
typeOfAddress := e_initial,
stationType := e_unknown,
reserved := 0,
mid := '020000000001'O
}
LibItsGeoNetworking_Pics.PICS_GN_SECURITY := false
LibItsGeoNetworking_Pics.PICS_IS_IUT_SECURED := false
// FDV_BAH/FDV_COH send deliberately malformed Basic/Common headers and expect
// the IUT to reject them; this adapter does not implement that negative path.
LibItsGeoNetworking_Pics.PICS_GN_BASIC_HEADER := false
LibItsGeoNetworking_Pics.PICS_GN_COMMON_HEADER := false
LibItsGeoNetworking_Pics.PICS_GN_GUC := false
LibItsGeoNetworking_Pics.PICS_GN_GUC_SRC := false
LibItsGeoNetworking_Pics.PICS_GN_GUC_DST := false
LibItsGeoNetworking_Pics.PICS_GN_GUC_FWD := false
LibItsGeoNetworking_Pics.PICS_GN_GBC := false
LibItsGeoNetworking_Pics.PICS_GN_GBC_SRC := false
LibItsGeoNetworking_Pics.PICS_GN_GBC_DST := false
LibItsGeoNetworking_Pics.PICS_GN_GBC_FWD := false
LibItsGeoNetworking_Pics.PICS_GN_GAC := false
LibItsGeoNetworking_Pics.PICS_GN_GAC_SRC := false
LibItsGeoNetworking_Pics.PICS_GN_GAC_DST := false
LibItsGeoNetworking_Pics.PICS_GN_GAC_FWD := false
LibItsGeoNetworking_Pics.PICS_GN_TSB := false
LibItsGeoNetworking_Pics.PICS_GN_TSB_SRC := false
LibItsGeoNetworking_Pics.PICS_GN_TSB_DST := false
LibItsGeoNetworking_Pics.PICS_GN_TSB_FWD := false
LibItsGeoNetworking_Pics.PICS_GN_BEACON_SRC := false
LibItsGeoNetworking_Pics.PICS_GN_BEACON_DST := false
LibItsGeoNetworking_Pics.PICS_GN_LS_REQ_SRC := false
LibItsGeoNetworking_Pics.PICS_GN_LS_REQ_RETRANSMISSION := false
LibItsGeoNetworking_Pics.PICS_GN_LS_REQ_DST := false
LibItsGeoNetworking_Pics.PICS_GN_LS_REP_DST := false
LibItsGeoNetworking_Pics.PICS_GN_LS_FWD := false
LibItsGeoNetworking_Pics.PICS_GN_DAD := false
LibItsGeoNetworking_Pics.PICS_GN_SHB_DST := false
LibItsCommon_Pixits.PX_GNSS_SCENARIO_SUPPORT := false
[LOGGING]
LogFile := "%e.%h-%r.%s"
FileMask := LOG_ALL
ConsoleMask := VERDICTOP | ERROR | WARNING | EXECUTOR
[EXECUTE]
ItsGeoNetworking_TestControl.control
@@ -0,0 +1,290 @@
// External TITAN SUT adapter for the official AtsGeoNetworking port interfaces.
// Testcase sources and verdict logic are unchanged. This adapter invokes a
// separate host/device SUT process and decodes its actual boundary outputs
// using the ATS's own generated codec (fx_enc/fx_dec), not a hand-rolled parser.
//
// Scope: SHB source generation only (GAP-GN-001; GUC/GAC/TSB/GBC triggers,
// beaconing simulation and GNSS scenarios are not implemented -- PICS in
// etsi_geonetworking.cfg disables the corresponding test-control branches).
#include "UpperTesterPort_GN.hh"
#include "GeoNetworkingPort.hh"
#include "AdapterControlPort_GN.hh"
#include "LibItsGeoNetworking_EncdecDeclarations.hh"
#include <functional>
#include <vector>
#include <string>
#include <cstdint>
#include <cstdlib>
#include <cstdio>
#include <stdexcept>
#include <unistd.h>
#include <poll.h>
#include <signal.h>
#include <sys/wait.h>
namespace {
using Bytes = std::vector<unsigned char>;
using namespace LibItsGeoNetworking__TypesAndValues;
std::function<void(const GeoNetworkingInd&)> lower_indication;
unsigned read16(const Bytes& b, unsigned i) { return (b.at(i) << 8) | b.at(i + 1); }
class Process {
pid_t pid_ = -1;
int input_ = -1, output_ = -1;
public:
~Process() {
if (input_ >= 0) close(input_);
if (output_ >= 0) close(output_);
if (pid_ > 0) { kill(pid_, SIGTERM); waitpid(pid_, nullptr, 0); }
}
Bytes exchange(const Bytes& bytes) {
if (pid_ < 0) {
const char* executable = std::getenv("VIDF_SUT_EXECUTABLE");
if (!executable) throw std::runtime_error("Set VIDF_SUT_EXECUTABLE to the external SUT process");
int in[2], out[2];
if (pipe(in) || pipe(out)) throw std::runtime_error("Cannot create SUT pipes");
pid_ = fork();
if (pid_ == 0) {
dup2(in[0], STDIN_FILENO); dup2(out[1], STDOUT_FILENO);
close(in[0]); close(in[1]); close(out[0]); close(out[1]);
const char* script = std::getenv("VIDF_SUT_SCRIPT");
const char* port = std::getenv("VIDF_SUT_PORT");
if (script && port) execl(executable, executable, script, "--port", port, static_cast<char*>(nullptr));
else execl(executable, executable, static_cast<char*>(nullptr));
_exit(127);
}
close(in[0]); close(out[1]); input_ = in[1]; output_ = out[0];
if (pid_ < 0) throw std::runtime_error("Cannot fork SUT process");
}
const char* digits = "0123456789abcdef";
std::string line;
for (auto b : bytes) { line += digits[b >> 4]; line += digits[b & 15]; }
line += '\n';
for (std::size_t sent = 0; sent < line.size();) {
const auto count = write(input_, line.data() + sent, line.size() - sent);
if (count <= 0) throw std::runtime_error("SUT pipe write failed");
sent += count;
}
line.clear();
while (line.size() <= 8192) {
pollfd fd {output_, POLLIN, 0};
if (poll(&fd, 1, 12000) <= 0) throw std::runtime_error("SUT response timeout");
char c;
if (read(output_, &c, 1) != 1) throw std::runtime_error("SUT process ended");
if (c == '\n') break;
if (c != '\r') line += c;
}
if (line.size() > 8192 || line.size() % 2) throw std::runtime_error("Invalid SUT response size");
Bytes result;
auto hex = [](char c) -> unsigned {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
throw std::runtime_error("Non-hex SUT response");
};
for (unsigned i = 0; i < line.size(); i += 2) result.push_back((hex(line[i]) << 4) | hex(line[i + 1]));
return result;
}
};
Process sut;
// GN_Address baked into hil_sut.cpp's Sut::reset() (config.mib.itsGnLocalGnAddr.mid
// only; type-of-address/station-type/reserved stay at vanetza::geonet::Address's
// defaults). f_acGetLongPosVector's caller compares this exactly against the wire
// source address (LibItsGeoNetworking_Templates.ttcn mw_longPosVectorPosition), so
// it must match vanetza/geonet/address.cpp's Address() defaults, not be invented.
GN__Address iut_gn_address() {
GN__Address address;
address.typeOfAddress() = TypeOfAddress::e__initial; // Address::m_manually_configured == false
address.stationType() = StationType::e__unknown; // Address::m_station_type == StationType::Unknown
address.reserved() = 0; // Address::m_country_code == 0
const unsigned char mid[] = {2, 0, 0, 0, 0, 1};
address.mid() = OCTETSTRING(6, mid);
return address;
}
// Position/speed/heading baked into the same reset(): 52.0N, 13.0E, stationary.
LongPosVector iut_position() {
LongPosVector position;
position.gnAddr() = iut_gn_address();
position.timestamp__() = 0;
position.latitude() = 520000000; // 1/10 microdegree, matching LongPosVector's encoding
position.longitude() = 130000000;
const unsigned char zero_bit = 0;
position.pai() = BITSTRING(1, &zero_bit);
position.speed() = 0;
position.heading() = 0;
return position;
}
bool transact(const Bytes& command) {
const auto reply = sut.exchange(command);
if (reply.size() < 2) throw std::runtime_error("Truncated SUT response");
unsigned offset = 2;
for (unsigned n = 0; n < reply[1]; ++n) {
const auto kind = reply.at(offset);
const auto size = read16(reply, offset + 1);
offset += 3;
if (offset + size > reply.size()) throw std::runtime_error("Truncated SUT record");
Bytes b(reply.begin() + offset, reply.begin() + offset + size); offset += size;
if (kind == 1) {
// AL_DATA transmission observed independently of what triggered it
// (matches etsi_btp_adapter.cpp's kind==1 handling): decode with the
// ATS's own RAW codec, not a hand-rolled header parser.
BITSTRING bits = oct2bit(OCTETSTRING(static_cast<int>(b.size()), b.data()));
GeoNetworkingPdu pdu;
if (LibItsGeoNetworking__EncdecDeclarations::fx__dec__GeoNetworkingPdu(bits, pdu) != 0)
throw std::runtime_error("GeoNetworkingPdu decode failed");
GeoNetworkingInd indication;
indication.msgIn() = pdu;
const unsigned char broadcast[] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
indication.macDestinationAddress() = OCTETSTRING(6, broadcast);
indication.ssp() = OMIT_VALUE;
indication.its__aid() = OMIT_VALUE;
if (lower_indication) lower_indication(indication);
} else if (kind != 3) {
throw std::runtime_error("Unexpected SUT record kind");
} // kind == 3 (raw GN-DATA.indication) is not exercised by the SHB-source scope.
}
if (offset != reply.size()) throw std::runtime_error("Trailing SUT response bytes");
return reply[0] == 0;
}
}
namespace LibItsGeoNetworking__TestSystem {
UpperTesterPort::UpperTesterPort(const char* name) : UpperTesterPort_BASE(name) {}
UpperTesterPort::~UpperTesterPort() = default;
void UpperTesterPort::set_parameter(const char*, const char*) {}
void UpperTesterPort::Handle_Fd_Event_Error(int) {}
void UpperTesterPort::Handle_Fd_Event_Writable(int) {}
void UpperTesterPort::Handle_Fd_Event_Readable(int) {}
void UpperTesterPort::receiveMsg(const Base_Type&, const params&) {}
void UpperTesterPort::user_map(const char*) {}
void UpperTesterPort::user_unmap(const char*) {}
void UpperTesterPort::user_start() {}
void UpperTesterPort::user_stop() {}
void UpperTesterPort::outgoing_send(const UtGnInitialize&) {
try {
UtGnResults result; result.utGnInitializeResult() = transact({0}); incoming_message(result);
} catch (const std::exception& e) { TTCN_error("SUT initialization: %s", e.what()); }
}
void UpperTesterPort::outgoing_send(const UtGnChangePosition& change) {
// TS 102 871-2 documents these as relative offsets; this port applies them as
// an absolute fix in the same 1/10 microdegree encoding as LongPosVector,
// matching the only currently-exercised caller (none: no in-scope SHB-source
// testcase sends this yet). Revisit before relying on it for a mobile scenario.
try {
Bytes command {4};
const auto i32 = [&](std::int32_t v) {
command.push_back(static_cast<unsigned>(v) >> 24); command.push_back(static_cast<unsigned>(v) >> 16);
command.push_back(static_cast<unsigned>(v) >> 8); command.push_back(static_cast<unsigned>(v));
};
i32(static_cast<std::int32_t>(change.latitude()));
i32(static_cast<std::int32_t>(change.longitude()));
UtGnResults result; result.utGnChangePositionResult() = transact(command); incoming_message(result);
} catch (const std::exception& e) { TTCN_error("SUT position change: %s", e.what()); }
}
void UpperTesterPort::outgoing_send(const UtGnTrigger& trigger) {
try {
if (trigger.get_selection() != UtGnTrigger::ALT_shb) {
// GUC/GAC/TSB/GBC generation: GAP-GN-001, upstream router stubs.
UtGnResults result; result.utGnTriggerResult() = false; incoming_message(result);
return;
}
const auto& shb = trigger.shb();
const auto& tc = shb.trafficClass();
const unsigned char raw = (tc.scf() == SCF::e__scfEnabled ? 0x80 : 0) |
(tc.channelOffload() == ChannelOffload::e__choffEnable ? 0x40 : 0) |
(static_cast<unsigned>(static_cast<long long>(tc.tcId())) & 0x3f);
Bytes command {3, raw};
const OCTETSTRING& payload = shb.payload();
const unsigned char* ptr = payload;
command.insert(command.end(), ptr, ptr + payload.lengthof());
UtGnResults result; result.utGnTriggerResult() = transact(command); incoming_message(result);
} catch (const std::exception& e) { TTCN_error("SUT GN trigger: %s", e.what()); }
}
void UpperTesterPort::outgoing_send(const UtAutoInteropTrigger&) {
UtGnResults result; result.utAutoInteropTriggerResult() = false; incoming_message(result);
}
GeoNetworkingPort::GeoNetworkingPort(const char* name) : GeoNetworkingPort_BASE(name) {}
GeoNetworkingPort::~GeoNetworkingPort() = default;
void GeoNetworkingPort::set_parameter(const char*, const char*) {}
void GeoNetworkingPort::Handle_Fd_Event_Error(int) {}
void GeoNetworkingPort::Handle_Fd_Event_Writable(int) {}
void GeoNetworkingPort::Handle_Fd_Event_Readable(int) {}
void GeoNetworkingPort::receiveMsg(const GeoNetworkingInd&, const params&) {}
void GeoNetworkingPort::user_map(const char*) { lower_indication = [this](const GeoNetworkingInd& p) { incoming_message(p); }; }
void GeoNetworkingPort::user_unmap(const char*) { lower_indication = {}; }
void GeoNetworkingPort::user_start() {}
void GeoNetworkingPort::user_stop() {}
void GeoNetworkingPort::outgoing_send(const GeoNetworkingReq& send_par) {
// The lower tester injecting a packet toward the IUT (e.g. a neighbour's
// GBC/beacon): encode via the ATS's own codec, submit as AL_DATA.indication.
// Not exercised by the SHB-source scope, kept complete for the DST direction.
try {
const BITSTRING encoded = LibItsGeoNetworking__EncdecDeclarations::fx__enc__GeoNetworkingPdu(send_par.msgOut());
const OCTETSTRING raw = bit2oct(encoded);
const unsigned char* ptr = raw;
const unsigned char* mac = send_par.macDestinationAddress();
const unsigned char source[] = {0x02, 0, 0, 0, 0, 2}; // distinct locally-administered lower-tester address
Bytes command {2};
command.insert(command.end(), source, source + 6);
command.insert(command.end(), mac, mac + 6);
command.insert(command.end(), ptr, ptr + raw.lengthof());
if (!transact(command)) TTCN_error("SUT rejected lower tester submission");
} catch (const std::exception& e) { TTCN_error("SUT lower tester: %s", e.what()); }
}
AdapterControlPort::AdapterControlPort(const char* name) : AdapterControlPort_BASE(name) {}
AdapterControlPort::~AdapterControlPort() = default;
void AdapterControlPort::set_parameter(const char*, const char*) {}
void AdapterControlPort::Handle_Fd_Event_Error(int) {}
void AdapterControlPort::Handle_Fd_Event_Writable(int) {}
void AdapterControlPort::Handle_Fd_Event_Readable(int) {}
void AdapterControlPort::user_map(const char*) {}
void AdapterControlPort::user_unmap(const char*) {}
void AdapterControlPort::user_start() {}
void AdapterControlPort::user_stop() {}
void AdapterControlPort::outgoing_send(const AcGnPrimitive& primitive) {
// f_acTriggerEvent (LibItsGeoNetworking_Functions.ttcn) is fire-and-forget for
// every AcGnPrimitive except getLongPosVector, which f_acGetLongPosVector
// waits on synchronously.
if (primitive.get_selection() == AcGnPrimitive::ALT_getLongPosVector) {
AcGnResponse response; response.getLongPosVector() = iut_position();
incoming_message(response);
} else if (primitive.get_selection() == AcGnPrimitive::ALT_startBeaconing) {
// Not fire-and-forget in effect: an SHB/GBC request with store-carry-forward
// enabled (TrafficClass.scf) is buffered by the upstream router rather than
// transmitted immediately unless its location table already has a neighbour
// (vanetza/geonet/router.cpp, Router::request(const ShbDataRequest&, ...)).
// f_startBeingNeighbour's whole purpose is to establish that neighbour, so
// this feeds the supplied beacon PDU into the IUT exactly as GeoNetworkingPort
// would for any other lower-tester-injected packet.
try {
const BITSTRING encoded = LibItsGeoNetworking__EncdecDeclarations::fx__enc__GeoNetworkingPdu(
primitive.startBeaconing().beaconPacket());
const OCTETSTRING raw = bit2oct(encoded);
const unsigned char* raw_bytes = raw;
const unsigned char source[] = {0x02, 0, 0, 0, 0, 3};
const unsigned char broadcast[] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
Bytes command {2};
command.insert(command.end(), source, source + 6);
command.insert(command.end(), broadcast, broadcast + 6);
command.insert(command.end(), raw_bytes, raw_bytes + raw.lengthof());
transact(command); // Best-effort neighbour setup; no confirm exists to report failure through.
} catch (const std::exception&) {
// f_startBeingNeighbour does not check a result; swallow and let the
// subsequent testcase behaviour (e.g. store-carry-forward buffering)
// surface the real problem instead of aborting the whole testcase here.
}
}
}
void AdapterControlPort::outgoing_send(const LibItsIpv6OverGeoNetworking__TypesAndValues::AcGn6Primitive&) {}
void AdapterControlPort::outgoing_send(const LibItsCommon__TypesAndValues::AcGnssPrimitive&) {
// Only reached if PX_GNSS_SCENARIO_SUPPORT is true; the supplied config sets
// it false, so f_acLoadScenario/f_acStartScenario/f_acAwaitTimeInRunningScenario
// never call this (GAP-GN-001: no GNSS scenario simulation implemented).
}
void AdapterControlPort::outgoing_send(const LibItsCommon__TypesAndValues::AcSecPrimitive&) {}
}
@@ -0,0 +1,5 @@
[
"TC_GEONW_FDV_SHB_BV_01",
"TC_GEONW_PON_FPB_BV_11_05",
"TC_GEONW_PON_SHB_BV_01"
]
@@ -0,0 +1,540 @@
// External TITAN SUT adapter for the official AtsSecurity port interfaces
// (system ItsSecSystem: the GeoNetworking ports plus the CAM and DENM upper
// tester ports). Testcase sources and verdict logic are unchanged. The
// adapter drives a separate host SUT process (vidf_sut --security-pool ...)
// through the diagnostic protocol of hil_sut.hpp and decodes what that SUT
// actually transmits with the ATS's own generated codec (fx_enc/fx_dec).
//
// Scope: sending behaviour of the IUT (GN-MGMT beacons through the GN core,
// CAM and DENM carriers emitted by the test application, TS 103 097 security
// profiles) and, with a SUT built with VIDF_SECURITY_VERIFY, the receiving
// behaviour: secured packets the test system sends through the GeoNetworking
// port are injected as AL_DATA.indication and whatever the SUT passes up after
// SN-DECAP is reported as UtGnEventInd. The test system signs those packets in
// TTCN-3 (fx_signWithEcdsa*), not in this adapter. etsi_security_gn.cfg,
// etsi_security_facilities.cfg and etsi_security_receive.cfg keep the PICS honest
// and select the stimulus configuration (CAM carrier on or off).
//
// Time: the SUT owns an ITS clock that this adapter advances to wall-clock
// ITS time every 100 ms (timerfd on the GeoNetworking port); spontaneous
// transmissions (beacons, periodic CAM carrier) come back with each tick.
#include "UpperTesterPort_GN.hh"
#include "GeoNetworkingPort.hh"
#include "AdapterControlPort_GN.hh"
#include "UpperTesterPort_CAM.hh"
#include "UpperTesterPort_DENM.hh"
#include "LibItsGeoNetworking_EncdecDeclarations.hh"
#include "geonetworking_codec.hh"
#include "security_services_its.hh"
#include "params_its.hh"
#include <vanetza_idf/its_time.hpp>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
#include <unistd.h>
#include <poll.h>
#include <signal.h>
#include <sys/mman.h>
#include <sys/timerfd.h>
#include <sys/wait.h>
#include <cerrno>
#include <pthread.h>
namespace {
using Bytes = std::vector<unsigned char>;
using namespace LibItsGeoNetworking__TypesAndValues;
std::function<void(const GeoNetworkingInd&)> lower_indication;
// GN upper tester event: what the SUT passed up to its facilities layer (UtGnEventInd, the
// receiving-side testcases compare its rawPayload with the GN payload they sent).
std::function<void(const UtGnEventInd&)> upper_indication;
unsigned read16(const Bytes& b, unsigned i) { return (b.at(i) << 8) | b.at(i + 1); }
// GN layer parameters of the test system, taken from the geoNetworkingPort "params"
// test port parameter in the framework's own "GN(key=value,...)/..." syntax so the cfg
// reads like an official one. Only the security keys are meaningful here:
// enable_security_checks (failed verification discards the packet instead of warning)
// and sec_db_path (certificate pool loaded at map time, as geonetworking_layer does).
params_its gn_params;
bool security_checks_default = false;
bool security_checks = false;
void parse_gn_params(const std::string& value) {
const auto begin = value.find("GN(");
if (begin == std::string::npos) return;
const auto end = value.find(')', begin);
params::convert(gn_params, value.substr(begin + 3, end == std::string::npos ? std::string::npos : end - begin - 3));
security_checks_default = gn_params.count(params_its::enable_security_checks) && gn_params[params_its::enable_security_checks] == "1";
security_checks = security_checks_default;
}
// ITS time (TAI microseconds since 2004-01-01T00:00:00Z), the library's tested conversion.
std::uint64_t its_now_us() {
return static_cast<std::uint64_t>(vanetza_idf::its_time::since_epoch(std::chrono::system_clock::now()).count());
}
// The external SUT process. TITAN's parallel runtime forks the MTC and every PTC from
// the host controller, and a test case may drive the IUT from several components (the
// DENM cases trigger from a PTC while the MTC observes the GN port). One SUT instance
// is therefore started in the host controller before any fork, its pipes are inherited
// by every component, and a process-shared robust mutex keeps each command/reply
// exchange atomic across components. Only the process that spawned the SUT stops it.
class Process {
pid_t pid_ = -1, owner_ = -1;
int input_ = -1, output_ = -1;
pthread_mutex_t* lock_ = nullptr;
public:
Process() {
const char* executable = std::getenv("VIDF_SUT_EXECUTABLE");
if (!executable) return; // reported on first use
void* shared = mmap(nullptr, sizeof(pthread_mutex_t), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (shared == MAP_FAILED) return;
lock_ = static_cast<pthread_mutex_t*>(shared);
pthread_mutexattr_t attributes;
pthread_mutexattr_init(&attributes);
pthread_mutexattr_setpshared(&attributes, PTHREAD_PROCESS_SHARED);
pthread_mutexattr_setrobust(&attributes, PTHREAD_MUTEX_ROBUST);
pthread_mutex_init(lock_, &attributes);
pthread_mutexattr_destroy(&attributes);
// VIDF_SUT_ARGS: space separated arguments, e.g. "--security-pool /path/to/pool"
std::vector<std::string> args {executable};
if (const char* extra = std::getenv("VIDF_SUT_ARGS")) {
std::istringstream stream(extra);
for (std::string arg; stream >> arg;) args.push_back(arg);
}
int in[2], out[2];
if (pipe(in) || pipe(out)) return;
pid_ = fork();
if (pid_ == 0) {
dup2(in[0], STDIN_FILENO); dup2(out[1], STDOUT_FILENO);
close(in[0]); close(in[1]); close(out[0]); close(out[1]);
std::vector<char*> argv;
for (auto& arg : args) argv.push_back(arg.data());
argv.push_back(nullptr);
execv(executable, argv.data());
_exit(127);
}
close(in[0]); close(out[1]); input_ = in[1]; output_ = out[0];
owner_ = getpid();
}
~Process() {
if (pid_ > 0 && getpid() == owner_) { kill(pid_, SIGTERM); waitpid(pid_, nullptr, 0); }
}
Bytes exchange(const Bytes& bytes) {
if (pid_ <= 0 || !lock_) throw std::runtime_error("Set VIDF_SUT_EXECUTABLE to the external SUT process");
struct Guard {
pthread_mutex_t* lock;
explicit Guard(pthread_mutex_t* l) : lock(l) {
if (pthread_mutex_lock(lock) == EOWNERDEAD) pthread_mutex_consistent(lock); // a component died mid-exchange
}
~Guard() { pthread_mutex_unlock(lock); }
} guard(lock_);
const char* digits = "0123456789abcdef";
std::string line;
for (auto b : bytes) { line += digits[b >> 4]; line += digits[b & 15]; }
line += '\n';
for (std::size_t sent = 0; sent < line.size();) {
const auto count = write(input_, line.data() + sent, line.size() - sent);
if (count <= 0) throw std::runtime_error("SUT pipe write failed");
sent += count;
}
line.clear();
while (line.size() <= 8192) {
pollfd fd {output_, POLLIN, 0};
if (poll(&fd, 1, 12000) <= 0) throw std::runtime_error("SUT response timeout");
char c;
if (read(output_, &c, 1) != 1) throw std::runtime_error("SUT process ended");
if (c == '\n') break;
if (c != '\r') line += c;
}
if (line.size() > 8192 || line.size() % 2) throw std::runtime_error("Invalid SUT response size");
Bytes result;
auto hex = [](char c) -> unsigned {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
throw std::runtime_error("Non-hex SUT response");
};
for (unsigned i = 0; i < line.size(); i += 2) result.push_back((hex(line[i]) << 4) | hex(line[i + 1]));
return result;
}
};
Process sut; // static initialisation: runs in the host controller before TITAN forks components
// GN address and position baked into hil_sut.cpp's Sut::reset(); see the GeoNetworking
// adapter for why these must match vanetza::geonet::Address's defaults exactly.
GN__Address iut_gn_address() {
GN__Address address;
address.typeOfAddress() = TypeOfAddress::e__initial;
address.stationType() = StationType::e__unknown;
address.reserved() = 0;
const unsigned char mid[] = {2, 0, 0, 0, 0, 1};
address.mid() = OCTETSTRING(6, mid);
return address;
}
LongPosVector iut_position() {
LongPosVector position;
position.gnAddr() = iut_gn_address();
position.timestamp__() = 0;
position.latitude() = 520000000;
position.longitude() = 130000000;
const unsigned char zero_bit = 0;
position.pai() = BITSTRING(1, &zero_bit);
position.speed() = 0;
position.heading() = 0;
return position;
}
bool transact(const Bytes& command) {
const auto reply = sut.exchange(command);
if (reply.size() < 2) throw std::runtime_error("Truncated SUT response");
unsigned offset = 2;
for (unsigned n = 0; n < reply[1]; ++n) {
const auto kind = reply.at(offset);
const auto size = read16(reply, offset + 1);
offset += 3;
if (offset + size > reply.size()) throw std::runtime_error("Truncated SUT record");
Bytes b(reply.begin() + offset, reply.begin() + offset + size); offset += size;
if (kind == 1) {
// AL_DATA transmission observed on the lower boundary, processed the way the
// framework's geonetworking_layer::receive_data does it: a secured packet (basic
// header next header 2, TS 103 836-4-1 clause 9.6.1) is verified and unwrapped
// by the framework's own security services (IEEE 1609.2 / TS 103 097 codec,
// signature check against the certificate pool), then the basic header plus the
// extracted GN payload is decoded with the ATS codec and the secured message is
// attached to the indication (mw_geoNwSecPdu matches on it).
OCTETSTRING data(static_cast<int>(b.size()), b.data());
params_its params;
Ieee1609Dot2::Ieee1609Dot2Data secured_message;
if (b.size() > 4 && (b[0] & 0x0f) == 2) {
const OCTETSTRING secured(static_cast<int>(b.size() - 4), b.data() + 4);
OCTETSTRING unsecured;
const int verified = security_services_its::get_instance().verify_and_extract_gn_payload(
secured, security_checks, secured_message, unsecured, params);
if (verified != 0) {
TTCN_warning("Secured GN packet failed the test system's security processing (checks %s)",
security_checks ? "enforced: discarded" : "not enforced: passed up");
if (security_checks) continue;
}
data = OCTETSTRING(4, b.data()) + unsecured;
}
geonetworking_codec codec;
GeoNetworkingPdu pdu;
if (codec.decode(data, pdu, &params) == -1) throw std::runtime_error("GeoNetworkingPdu decode failed");
if (secured_message.is_bound()) pdu.gnPacket().securedMsg() = OPTIONAL<Ieee1609Dot2::Ieee1609Dot2Data>(secured_message);
GeoNetworkingInd indication;
indication.msgIn() = pdu;
const unsigned char broadcast[] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
indication.macDestinationAddress() = OCTETSTRING(6, broadcast);
if (params.count(params_its::ssp)) indication.ssp() = oct2bit(str2oct(CHARSTRING(params[params_its::ssp].c_str())));
else indication.ssp() = OMIT_VALUE;
if (params.count(params_its::its_aid)) indication.its__aid() = INTEGER(std::stoi(params[params_its::its_aid]));
else indication.its__aid() = OMIT_VALUE;
if (lower_indication) lower_indication(indication);
} else if (kind == 2 || kind == 3) {
// A packet the SUT accepted (SN-DECAP success, or an unsecured one in the unsecured
// profile) and delivered to its upper layer: BTP-DATA.indication (kind 2, [type][dest
// port][source port or destination port info][SDU]) or a raw GN-DATA.indication (kind 3).
// Reported as the GN payload the test system compares against: for BTP the 4-octet
// BTP-A/B header in front of the SDU (TS 103 836-5-1 clause 7), else the payload as is.
Bytes raw;
if (kind == 2) {
if (b.size() < 5) throw std::runtime_error("Truncated BTP indication record");
raw.assign(b.begin() + 1, b.end());
} else {
raw = b;
}
if (upper_indication) {
UtGnEventInd event;
event.rawPayload() = OCTETSTRING(static_cast<int>(raw.size()), raw.data());
upper_indication(event);
}
} else {
throw std::runtime_error("Unexpected SUT record kind");
}
}
if (offset != reply.size()) throw std::runtime_error("Trailing SUT response bytes");
return reply[0] == 0;
}
// One SUT tick: advance the ITS clock to now; transmissions arrive through transact().
void tick() {
const std::uint64_t now = its_now_us();
Bytes command {5};
for (int shift = 56; shift >= 0; shift -= 8) command.push_back(static_cast<unsigned char>(now >> shift));
transact(command);
}
// Stimulus configuration of the test application behind the GN upper tester port
// (utPort "params": cam_carrier_ms=<period>). A running CAM carrier restarts the beacon
// timer with every SHB it sends (TS 103 836-4-1 clause 10.3.5), so the GN-MGMT cases
// run without it and the CAM/DENM cases with it: two campaign configurations, no
// per-testcase switching inside the adapter.
unsigned cam_carrier_ms = 0;
int timer_fd = -1;
unsigned denm_sequence = 0;
}
namespace LibItsGeoNetworking__TestSystem {
UpperTesterPort::UpperTesterPort(const char* name) : UpperTesterPort_BASE(name) {}
UpperTesterPort::~UpperTesterPort() = default;
void UpperTesterPort::set_parameter(const char* name, const char* value) {
if (std::strcmp(name, "params") != 0) return;
params ut_params;
params::convert(ut_params, value);
if (ut_params.count("cam_carrier_ms")) cam_carrier_ms = static_cast<unsigned>(std::atoi(ut_params["cam_carrier_ms"].c_str()));
}
void UpperTesterPort::Handle_Fd_Event_Error(int) {}
void UpperTesterPort::Handle_Fd_Event_Writable(int) {}
void UpperTesterPort::Handle_Fd_Event_Readable(int) {}
void UpperTesterPort::receiveMsg(const Base_Type&, const params&) {}
void UpperTesterPort::user_map(const char*) {
upper_indication = [this](const UtGnEventInd& event) { incoming_message(event); };
}
void UpperTesterPort::user_unmap(const char*) { upper_indication = {}; }
void UpperTesterPort::user_start() {}
void UpperTesterPort::user_stop() {}
void UpperTesterPort::outgoing_send(const UtGnInitialize&) {
// m_secGnInitialize carries the HashedId8 of the certificate the IUT shall use; the SUT
// is started with that certificate (VIDF_SUT_ARGS --at ...), a mismatch shows up as a
// signature/digest failure in the testcase rather than being papered over here.
try {
bool ok = transact({0});
tick();
// Periodic CAM carrier (test-application behaviour, TS 103 097 clause 7.1.1 profile):
// the CAM cases wait for CAMs the IUT sends on its own.
if (ok && cam_carrier_ms)
ok = transact({7, 0, static_cast<unsigned char>(cam_carrier_ms >> 8), static_cast<unsigned char>(cam_carrier_ms)});
UtGnResults result; result.utGnInitializeResult() = ok; incoming_message(result);
} catch (const std::exception& e) { TTCN_error("SUT initialization: %s", e.what()); }
}
void UpperTesterPort::outgoing_send(const UtGnChangePosition& change) {
try {
Bytes command {4};
const auto i32 = [&](std::int32_t v) {
command.push_back(static_cast<unsigned>(v) >> 24); command.push_back(static_cast<unsigned>(v) >> 16);
command.push_back(static_cast<unsigned>(v) >> 8); command.push_back(static_cast<unsigned>(v));
};
i32(static_cast<std::int32_t>(change.latitude()));
i32(static_cast<std::int32_t>(change.longitude()));
UtGnResults result; result.utGnChangePositionResult() = transact(command); incoming_message(result);
} catch (const std::exception& e) { TTCN_error("SUT position change: %s", e.what()); }
}
void UpperTesterPort::outgoing_send(const UtGnTrigger& trigger) {
try {
if (trigger.get_selection() != UtGnTrigger::ALT_shb) {
UtGnResults result; result.utGnTriggerResult() = false; incoming_message(result); // GAP-GN-001
return;
}
const auto& shb = trigger.shb();
const auto& tc = shb.trafficClass();
const unsigned char raw = (tc.scf() == SCF::e__scfEnabled ? 0x80 : 0) |
(tc.channelOffload() == ChannelOffload::e__choffEnable ? 0x40 : 0) |
(static_cast<unsigned>(static_cast<long long>(tc.tcId())) & 0x3f);
Bytes command {3, raw};
const OCTETSTRING& payload = shb.payload();
const unsigned char* ptr = payload;
command.insert(command.end(), ptr, ptr + payload.lengthof());
UtGnResults result; result.utGnTriggerResult() = transact(command); incoming_message(result);
} catch (const std::exception& e) { TTCN_error("SUT GN trigger: %s", e.what()); }
}
void UpperTesterPort::outgoing_send(const UtAutoInteropTrigger&) {
UtGnResults result; result.utAutoInteropTriggerResult() = false; incoming_message(result);
}
GeoNetworkingPort::GeoNetworkingPort(const char* name) : GeoNetworkingPort_BASE(name) {}
GeoNetworkingPort::~GeoNetworkingPort() = default;
void GeoNetworkingPort::set_parameter(const char* name, const char* value) {
if (std::strcmp(name, "params") == 0) parse_gn_params(value);
}
void GeoNetworkingPort::Handle_Fd_Event_Error(int) {}
void GeoNetworkingPort::Handle_Fd_Event_Writable(int) {}
void GeoNetworkingPort::Handle_Fd_Event_Readable(int fd) {
if (fd != timer_fd) return;
std::uint64_t expirations = 0;
if (read(timer_fd, &expirations, sizeof(expirations)) != sizeof(expirations)) return;
try { tick(); } catch (const std::exception& e) { TTCN_error("SUT tick: %s", e.what()); }
}
void GeoNetworkingPort::receiveMsg(const GeoNetworkingInd&, const params&) {}
void GeoNetworkingPort::user_map(const char*) {
lower_indication = [this](const GeoNetworkingInd& p) { incoming_message(p); };
// Certificate pool for the verification of IUT transmissions. The testcases load the
// same pool through fx_loadCertificates (PX_CERTIFICATE_POOL_PATH/PX_IUT_SEC_CONFIG_NAME)
// in this component's process; a sec_db_path port parameter makes the mapping
// self-sufficient, mirroring geonetworking_layer::setup_secured_mode.
if (gn_params.count(params_its::sec_db_path) && security_services_its::get_instance().setup(gn_params) != 0)
TTCN_error("Certificate pool %s could not be loaded", gn_params[params_its::sec_db_path].c_str());
timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
if (timer_fd < 0) TTCN_error("timerfd_create failed");
itimerspec period {};
period.it_interval.tv_nsec = 100 * 1000 * 1000;
period.it_value.tv_nsec = 100 * 1000 * 1000;
timerfd_settime(timer_fd, 0, &period, nullptr);
Handler_Add_Fd_Read(timer_fd);
}
void GeoNetworkingPort::user_unmap(const char*) {
if (timer_fd >= 0) { Handler_Remove_Fd_Read(timer_fd); close(timer_fd); timer_fd = -1; }
lower_indication = {};
}
void GeoNetworkingPort::user_start() {}
void GeoNetworkingPort::user_stop() {}
void GeoNetworkingPort::outgoing_send(const GeoNetworkingReq& send_par) {
// Lower tester packet toward the IUT (e.g. the test system acting as a neighbour).
try {
const BITSTRING encoded = LibItsGeoNetworking__EncdecDeclarations::fx__enc__GeoNetworkingPdu(send_par.msgOut());
const OCTETSTRING raw = bit2oct(encoded);
const unsigned char* ptr = raw;
const unsigned char* mac = send_par.macDestinationAddress();
const unsigned char source[] = {0x02, 0, 0, 0, 0, 2};
Bytes command {2};
command.insert(command.end(), source, source + 6);
command.insert(command.end(), mac, mac + 6);
command.insert(command.end(), ptr, ptr + raw.lengthof());
if (!transact(command)) TTCN_error("SUT rejected lower tester submission");
} catch (const std::exception& e) { TTCN_error("SUT lower tester: %s", e.what()); }
}
AdapterControlPort::AdapterControlPort(const char* name) : AdapterControlPort_BASE(name) {}
AdapterControlPort::~AdapterControlPort() = default;
void AdapterControlPort::set_parameter(const char*, const char*) {}
void AdapterControlPort::Handle_Fd_Event_Error(int) {}
void AdapterControlPort::Handle_Fd_Event_Writable(int) {}
void AdapterControlPort::Handle_Fd_Event_Readable(int) {}
void AdapterControlPort::user_map(const char*) {}
void AdapterControlPort::user_unmap(const char*) {}
void AdapterControlPort::user_start() {}
void AdapterControlPort::user_stop() {}
void AdapterControlPort::outgoing_send(const AcGnPrimitive& primitive) {
if (primitive.get_selection() == AcGnPrimitive::ALT_getLongPosVector) {
AcGnResponse response; response.getLongPosVector() = iut_position();
incoming_message(response);
} else if (primitive.get_selection() == AcGnPrimitive::ALT_startBeaconing) {
// f_startBeingNeighbour: feed the test system's beacon into the IUT so its location
// table holds a neighbour (see the GeoNetworking adapter for the SCF rationale).
try {
const BITSTRING encoded = LibItsGeoNetworking__EncdecDeclarations::fx__enc__GeoNetworkingPdu(
primitive.startBeaconing().beaconPacket());
const OCTETSTRING raw = bit2oct(encoded);
const unsigned char* raw_bytes = raw;
const unsigned char source[] = {0x02, 0, 0, 0, 0, 3};
const unsigned char broadcast[] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
Bytes command {2};
command.insert(command.end(), source, source + 6);
command.insert(command.end(), broadcast, broadcast + 6);
command.insert(command.end(), raw_bytes, raw_bytes + raw.lengthof());
transact(command);
} catch (const std::exception&) {}
}
// startPassBeaconing/stopPassBeaconing: this adapter passes every IUT transmission up anyway.
}
void AdapterControlPort::outgoing_send(const LibItsIpv6OverGeoNetworking__TypesAndValues::AcGn6Primitive&) {}
void AdapterControlPort::outgoing_send(const LibItsCommon__TypesAndValues::AcGnssPrimitive&) {}
void AdapterControlPort::outgoing_send(const LibItsCommon__TypesAndValues::AcSecPrimitive& primitive) {
// f_acTriggerSecEvent waits for AdapterControlResults{acSecResponse}. AcEnableSecurity
// names the certificate the *test system* signs with and whether security failures
// are enforced (geonetworking_layer::enable_secured_mode). This adapter signs no
// test-system packets (receiving-side cases are not wired), so enabling only takes
// the enforcement flag for the verification of IUT transmissions and succeeds when
// the named certificate exists in the loaded pool; disabling restores the port
// parameter default. The SUT's own profile is fixed at process start (VIDF_SUT_ARGS).
using LibItsCommon__TypesAndValues::AcSecPrimitive;
bool ok = true;
if (primitive.get_selection() == AcSecPrimitive::ALT_acEnableSecurity) {
OCTETSTRING certificate;
ok = security_services_its::get_instance().read_certificate(primitive.acEnableSecurity().certificateId(), certificate) == 0;
if (ok) security_checks = primitive.acEnableSecurity().enforceSecurity();
} else {
security_checks = security_checks_default;
}
LibItsCommon__TypesAndValues::AdapterControlResults results;
results.acSecResponse() = ok;
incoming_message(results);
}
} // namespace LibItsGeoNetworking__TestSystem
namespace LibItsCam__TestSystem {
using namespace LibItsCam__TypesAndValues;
UpperTesterPort::UpperTesterPort(const char* name) : UpperTesterPort_BASE(name) {}
UpperTesterPort::~UpperTesterPort() = default;
void UpperTesterPort::set_parameter(const char*, const char*) {}
void UpperTesterPort::Handle_Fd_Event_Error(int) {}
void UpperTesterPort::Handle_Fd_Event_Writable(int) {}
void UpperTesterPort::Handle_Fd_Event_Readable(int) {}
void UpperTesterPort::receiveMsg(const Base_Type&, const params&) {}
void UpperTesterPort::user_map(const char*) {}
void UpperTesterPort::user_unmap(const char*) {}
void UpperTesterPort::user_start() {}
void UpperTesterPort::user_stop() {}
void UpperTesterPort::outgoing_send(const UtCamInitialize&) {
UtCamResults result; result.utCamInitializeResult() = true; incoming_message(result); // carrier already running
}
void UpperTesterPort::outgoing_send(const UtCamChangePosition&) {
UtCamResults result; result.utCamChangePositionResult() = false; incoming_message(result);
}
void UpperTesterPort::outgoing_send(const UtCamChangePseudonym&) {
UtCamResults result; result.utCamChangePseudonymResult() = false; incoming_message(result); // no CA service
}
void UpperTesterPort::outgoing_send(const UtCamTrigger&) {
UtCamResults result; result.utCamTriggerResult() = false; incoming_message(result); // no CA service
}
void UpperTesterPort::outgoing_send(const UtActivatePositionTime&) {
UtCamResults result; result.utActivatePositionTimeResult() = false; incoming_message(result);
}
void UpperTesterPort::outgoing_send(const UtDeactivatePositionTime&) {
UtCamResults result; result.utDeactivatePositionTimeResult() = false; incoming_message(result);
}
} // namespace LibItsCam__TestSystem
namespace LibItsDenm__TestSystem {
using namespace LibItsDenm__TypesAndValues;
UpperTesterPort::UpperTesterPort(const char* name) : UpperTesterPort_BASE(name) {}
UpperTesterPort::~UpperTesterPort() = default;
void UpperTesterPort::set_parameter(const char*, const char*) {}
void UpperTesterPort::Handle_Fd_Event_Error(int) {}
void UpperTesterPort::Handle_Fd_Event_Writable(int) {}
void UpperTesterPort::Handle_Fd_Event_Readable(int) {}
void UpperTesterPort::receiveMsg(const Base_Type&, const params&) {}
void UpperTesterPort::user_map(const char*) {}
void UpperTesterPort::user_unmap(const char*) {}
void UpperTesterPort::user_start() {}
void UpperTesterPort::user_stop() {}
void UpperTesterPort::outgoing_send(const UtDenmInitialize&) {
UtDenmResults result; result.utDenmInitializeResult() = true; incoming_message(result);
}
void UpperTesterPort::outgoing_send(const UtDenmTrigger&) {
// One syntactically valid DENM carrier per trigger (TS 103 097 clause 7.1.2 profile under
// test); the situation/validity of the trigger is not acted upon: no DEN service.
try {
// Deferred to the next clock advance: the transmission must reach the GN port of
// the component that drives the clock, not this trigger component.
const unsigned sequence = ++denm_sequence;
const bool ok = transact({8, 1, static_cast<unsigned char>(sequence >> 8), static_cast<unsigned char>(sequence)});
UtDenmResults result;
result.utDenmTriggerResult().result() = ok;
result.utDenmTriggerResult().actionId().originatingStationId() = 42;
result.utDenmTriggerResult().actionId().sequenceNumber() = sequence;
incoming_message(result);
} catch (const std::exception& e) { TTCN_error("SUT DENM carrier: %s", e.what()); }
}
void UpperTesterPort::outgoing_send(const UtDenmUpdate&) {
UtDenmResults result; result.utDenmUpdateResult().result() = false; incoming_message(result);
}
void UpperTesterPort::outgoing_send(const UtDenmTermination&) {
// A carrier DENM is sent once and never repeated, so there is nothing left to terminate.
UtDenmResults result; result.utDenmTerminationResult() = true; incoming_message(result);
}
void UpperTesterPort::outgoing_send(const UtDenmChangePosition&) {
UtDenmResults result; result.utDenmChangePositionResult() = false; incoming_message(result);
}
void UpperTesterPort::outgoing_send(const UtDenmChangePseudonym&) {
UtDenmResults result; result.utDenmChangePseudonymResult() = false; incoming_message(result);
}
} // namespace LibItsDenm__TestSystem
@@ -0,0 +1,70 @@
[MODULE_PARAMETERS]
// AtsSecurity, sending side of the IUT, CAM and DENM profiles (TS 103 097 clauses
// 7.1.1 and 7.1.2): the test application behind the upper tester emits a CAM
// carrier every second and a DENM carrier on UtDenmTrigger; both are signed by the
// SUT's security entity. The GN-MGMT profile cases live in etsi_security_gn.cfg.
// The test system verifies every transmission with the pool it loads from
// ./certificates (run_etsi.py --pool copies the generated pool there). Every PICS
// that would select receiving-side or unimplemented behaviour is disabled.
LibItsGeoNetworking_Pics.PICS_GN_LOCAL_GN_ADDR := {
typeOfAddress := e_initial,
stationType := e_unknown,
reserved := 0,
mid := '020000000001'O
}
LibItsGeoNetworking_Pics.PICS_GN_SECURITY := true
LibItsGeoNetworking_Pics.PICS_IS_IUT_SECURED := true
LibItsGeoNetworking_Pics.PICS_GN_BEACON_SRC := true
LibItsGeoNetworking_Pics.PICS_GN_BEACON_DST := false
LibItsGeoNetworking_Pics.PICS_GN_GUC := false
LibItsGeoNetworking_Pics.PICS_GN_GBC := false
LibItsGeoNetworking_Pics.PICS_GN_GAC := false
LibItsGeoNetworking_Pics.PICS_GN_TSB := false
LibItsGeoNetworking_Pics.PICS_GN_DAD := false
LibItsGeoNetworking_Pics.PICS_GN_SHB_DST := false
LibItsCommon_Pixits.PX_GNSS_SCENARIO_SUPPORT := false
// CAM and DENM are carried over BTP-B (TS 103 836-5-1 clause 7.2; ports 2001/2002).
LibItsGeoNetworking_Pixits.PX_GN_UPPER_LAYER := e_btpB
LibItsSecurity_Pixits.PX_CERTIFICATE_POOL_PATH := "."
LibItsSecurity_Pixits.PX_IUT_SEC_CONFIG_NAME := "certificates"
LibItsSecurity_Pixits.PX_IUT_DEFAULT_CERTIFICATE := "CERT_IUT_A_AT"
LibItsCommon_Pixits.PX_CERT_FOR_TS := "CERT_TS_A_AT"
LibItsSecurity_Pixits.PX_OTHER_ITS_AID := 141
LibItsSecurity_Pics.PICS_SEC_ITS_AID_OTHER := true
LibItsSecurity_Pics.PICS_SEC_SHA256 := true
LibItsSecurity_Pics.PICS_SEC_SHA384 := false
LibItsSecurity_Pics.PICS_SEC_NIST_P256 := true
LibItsSecurity_Pics.PICS_SEC_BRAINPOOL_P256R1 := false
LibItsSecurity_Pics.PICS_SEC_BRAINPOOL_P384R1 := false
// Receiving side and P2P distribution need SN-DECAP verification (GAP-SEC-001).
LibItsSecurity_Pics.PICS_SEC_P2P_AT_DISTRIBUTION := false
LibItsSecurity_Pics.PICS_SEC_P2P_AA_DISTRIBUTION := false
LibItsSecurity_Pics.PICS_SEC_CERTIFICATE_SELECTION := false
LibItsSecurity_Pics.PICS_SEC_CIRCULAR_REGION := false
LibItsSecurity_Pics.PICS_SEC_RECTANGULAR_REGION := false
LibItsSecurity_Pics.PICS_SEC_POLYGONAL_REGION := false
LibItsSecurity_Pics.PICS_SEC_IDENTIFIED_REGION := false
LibItsSecurity_Pics.PICS_SEC_BFK_AUTH := false
[TESTPORT_PARAMETERS]
// Official GN layer syntax; only the security keys are read by the adapter. Verification
// failures discard the IUT transmission (enable_security_checks=1) instead of passing it
// up with a warning, so a PASS below implies a signature the framework verified against
// the pool it loaded from ./certificates.
system.geoNetworkingPort.params := "GN(enable_security_checks=1,sec_db_path=./certificates)"
// CAM carrier period of the test application (TS 103 097 clause 7.1.1 profile).
system.utPort.params := "cam_carrier_ms=1000"
[LOGGING]
LogFile := "%e.%h-%r.%s"
FileMask := LOG_ALL
ConsoleMask := VERDICTOP | ERROR | WARNING | EXECUTOR
[EXECUTE]
ItsSecurity_TestCases.TC_SEC_ITSS_SND_CAM_01_BV
ItsSecurity_TestCases.TC_SEC_ITSS_SND_CAM_02_BV
ItsSecurity_TestCases.TC_SEC_ITSS_SND_CAM_03_BV
ItsSecurity_TestCases.TC_SEC_ITSS_SND_CAM_04_BV
ItsSecurity_TestCases.TC_SEC_ITSS_SND_DENM_01_BV
ItsSecurity_TestCases.TC_SEC_ITSS_SND_DENM_02_BV
ItsSecurity_TestCases.TC_SEC_ITSS_SND_DENM_03_BV
@@ -0,0 +1,9 @@
[
"TC_SEC_ITSS_SND_CAM_01_BV",
"TC_SEC_ITSS_SND_CAM_02_BV",
"TC_SEC_ITSS_SND_CAM_03_BV",
"TC_SEC_ITSS_SND_CAM_04_BV",
"TC_SEC_ITSS_SND_DENM_01_BV",
"TC_SEC_ITSS_SND_DENM_02_BV",
"TC_SEC_ITSS_SND_DENM_03_BV"
]
@@ -0,0 +1,70 @@
[MODULE_PARAMETERS]
// AtsSecurity, sending side of the IUT, GN-MGMT profile (TS 103 097 clause 7.1.3):
// the SUT (vidf_sut --security-pool) signs its own beacons through the GN core. No
// facilities traffic is stimulated because an SHB restarts the beacon timer
// (TS 103 836-4-1 clause 10.3.5) and the CAM/DENM profile cases live in
// etsi_security_facilities.cfg. The test system verifies every transmission with
// the pool it loads from ./certificates (run_etsi.py --pool copies the generated
// pool there). Every PICS that would select receiving-side or unimplemented
// behaviour is disabled.
LibItsGeoNetworking_Pics.PICS_GN_LOCAL_GN_ADDR := {
typeOfAddress := e_initial,
stationType := e_unknown,
reserved := 0,
mid := '020000000001'O
}
LibItsGeoNetworking_Pics.PICS_GN_SECURITY := true
LibItsGeoNetworking_Pics.PICS_IS_IUT_SECURED := true
LibItsGeoNetworking_Pics.PICS_GN_BEACON_SRC := true
LibItsGeoNetworking_Pics.PICS_GN_BEACON_DST := false
LibItsGeoNetworking_Pics.PICS_GN_GUC := false
LibItsGeoNetworking_Pics.PICS_GN_GBC := false
LibItsGeoNetworking_Pics.PICS_GN_GAC := false
LibItsGeoNetworking_Pics.PICS_GN_TSB := false
LibItsGeoNetworking_Pics.PICS_GN_DAD := false
LibItsGeoNetworking_Pics.PICS_GN_SHB_DST := false
LibItsCommon_Pixits.PX_GNSS_SCENARIO_SUPPORT := false
LibItsSecurity_Pixits.PX_CERTIFICATE_POOL_PATH := "."
LibItsSecurity_Pixits.PX_IUT_SEC_CONFIG_NAME := "certificates"
LibItsSecurity_Pixits.PX_IUT_DEFAULT_CERTIFICATE := "CERT_IUT_A_AT"
LibItsCommon_Pixits.PX_CERT_FOR_TS := "CERT_TS_A_AT"
LibItsSecurity_Pixits.PX_OTHER_ITS_AID := 141
LibItsSecurity_Pics.PICS_SEC_ITS_AID_OTHER := true
LibItsSecurity_Pics.PICS_SEC_SHA256 := true
LibItsSecurity_Pics.PICS_SEC_SHA384 := false
LibItsSecurity_Pics.PICS_SEC_NIST_P256 := true
LibItsSecurity_Pics.PICS_SEC_BRAINPOOL_P256R1 := false
LibItsSecurity_Pics.PICS_SEC_BRAINPOOL_P384R1 := false
// Receiving side and P2P distribution need SN-DECAP verification (GAP-SEC-001).
LibItsSecurity_Pics.PICS_SEC_P2P_AT_DISTRIBUTION := false
LibItsSecurity_Pics.PICS_SEC_P2P_AA_DISTRIBUTION := false
LibItsSecurity_Pics.PICS_SEC_CERTIFICATE_SELECTION := false
LibItsSecurity_Pics.PICS_SEC_CIRCULAR_REGION := false
LibItsSecurity_Pics.PICS_SEC_RECTANGULAR_REGION := false
LibItsSecurity_Pics.PICS_SEC_POLYGONAL_REGION := false
LibItsSecurity_Pics.PICS_SEC_IDENTIFIED_REGION := false
LibItsSecurity_Pics.PICS_SEC_BFK_AUTH := false
[TESTPORT_PARAMETERS]
// Official GN layer syntax; only the security keys are read by the adapter. Verification
// failures discard the IUT transmission (enable_security_checks=1) instead of passing it
// up with a warning, so a PASS below implies a signature the framework verified against
// the pool it loaded from ./certificates.
system.geoNetworkingPort.params := "GN(enable_security_checks=1,sec_db_path=./certificates)"
// No CAM carrier: the IUT is requested to send secured beacons only.
system.utPort.params := "cam_carrier_ms=0"
[LOGGING]
LogFile := "%e.%h-%r.%s"
FileMask := LOG_ALL
ConsoleMask := VERDICTOP | ERROR | WARNING | EXECUTOR
[EXECUTE]
ItsSecurity_TestCases.TC_SEC_ITSS_SND_GENMSG_01_BV
ItsSecurity_TestCases.TC_SEC_ITSS_SND_GENMSG_02_BV
ItsSecurity_TestCases.TC_SEC_ITSS_SND_GENMSG_03_BV
ItsSecurity_TestCases.TC_SEC_ITSS_SND_GENMSG_04_BV
ItsSecurity_TestCases.TC_SEC_ITSS_SND_GENMSG_05_BV
ItsSecurity_TestCases.TC_SEC_ITSS_SND_GENMSG_06_BV
ItsSecurity_TestCases.TC_SEC_ITSS_SND_GENMSG_07_BV
ItsSecurity_TestCases.TC_SEC_ITSS_SND_GENMSG_08_BV
@@ -0,0 +1,10 @@
[
"TC_SEC_ITSS_SND_GENMSG_01_BV",
"TC_SEC_ITSS_SND_GENMSG_02_BV",
"TC_SEC_ITSS_SND_GENMSG_03_BV",
"TC_SEC_ITSS_SND_GENMSG_04_BV",
"TC_SEC_ITSS_SND_GENMSG_05_BV",
"TC_SEC_ITSS_SND_GENMSG_06_BV",
"TC_SEC_ITSS_SND_GENMSG_07_BV",
"TC_SEC_ITSS_SND_GENMSG_08_BV"
]
@@ -0,0 +1,100 @@
// AtsSecurity, receiving side of the IUT (TS 103 097 V2.2.1 clauses 5.2, 7.1.1, 7.1.2):
// the test system signs CAMs/DENMs in TTCN-3 with CERT_TS_A_AT (or PX_AT_CERTIFICATE)
// from the pool and sends them through the GeoNetworking port; the adapter injects
// them into the SUT (vidf_sut --security-pool, built with VIDF_SECURITY_VERIFY) as
// AL_DATA.indication and reports what the SUT passes up after SN-DECAP as
// UtGnEventInd. The SUT trusts CERT_IUT_A_RCA with CERT_IUT_A_AA and CERT_TS_A_AA.
// Cases gated by PICS this SUT does not claim (implicit certificates, Brainpool)
// are not selected. No CAM carrier: nothing is stimulated on the sending side.
[MODULE_PARAMETERS]
// AtsSecurity, sending side of the IUT, GN-MGMT profile (TS 103 097 clause 7.1.3):
// the SUT (vidf_sut --security-pool) signs its own beacons through the GN core. No
// facilities traffic is stimulated because an SHB restarts the beacon timer
// (TS 103 836-4-1 clause 10.3.5) and the CAM/DENM profile cases live in
// etsi_security_facilities.cfg. The test system verifies every transmission with
// the pool it loads from ./certificates (run_etsi.py --pool copies the generated
// pool there). Every PICS that would select receiving-side or unimplemented
// behaviour is disabled.
LibItsGeoNetworking_Pics.PICS_GN_LOCAL_GN_ADDR := {
typeOfAddress := e_initial,
stationType := e_unknown,
reserved := 0,
mid := '020000000001'O
}
LibItsGeoNetworking_Pics.PICS_GN_SECURITY := true
LibItsGeoNetworking_Pics.PICS_IS_IUT_SECURED := true
LibItsGeoNetworking_Pics.PICS_GN_BEACON_SRC := true
LibItsGeoNetworking_Pics.PICS_GN_BEACON_DST := false
LibItsGeoNetworking_Pics.PICS_GN_GUC := false
LibItsGeoNetworking_Pics.PICS_GN_GBC := false
LibItsGeoNetworking_Pics.PICS_GN_GAC := false
LibItsGeoNetworking_Pics.PICS_GN_TSB := false
LibItsGeoNetworking_Pics.PICS_GN_DAD := false
LibItsGeoNetworking_Pics.PICS_GN_SHB_DST := false
LibItsCommon_Pixits.PX_GNSS_SCENARIO_SUPPORT := false
// CAM and DENM payloads are carried over BTP-B (TS 103 836-5-1 clause 7.2).
LibItsGeoNetworking_Pixits.PX_GN_UPPER_LAYER := e_btpB
LibItsSecurity_Pixits.PX_CERTIFICATE_POOL_PATH := "."
LibItsSecurity_Pixits.PX_IUT_SEC_CONFIG_NAME := "certificates"
LibItsSecurity_Pixits.PX_IUT_DEFAULT_CERTIFICATE := "CERT_IUT_A_AT"
// CERT_TS_B_AT: signing ticket with a circular region around the SUT position (DENM_02_BV_XX, CAM_04_BV_XX)
LibItsSecurity_Pixits.PX_AT_CERTIFICATE := "CERT_TS_B_AT"
LibItsCommon_Pixits.PX_CERT_FOR_TS := "CERT_TS_A_AT"
LibItsSecurity_Pixits.PX_OTHER_ITS_AID := 141
LibItsSecurity_Pics.PICS_SEC_ITS_AID_OTHER := true
LibItsSecurity_Pics.PICS_SEC_SHA256 := true
LibItsSecurity_Pics.PICS_SEC_SHA384 := false
LibItsSecurity_Pics.PICS_SEC_NIST_P256 := true
LibItsSecurity_Pics.PICS_SEC_BRAINPOOL_P256R1 := false
LibItsSecurity_Pics.PICS_SEC_BRAINPOOL_P384R1 := false
// Receiving side and P2P distribution need SN-DECAP verification (GAP-SEC-001).
LibItsSecurity_Pics.PICS_SEC_P2P_AT_DISTRIBUTION := false
LibItsSecurity_Pics.PICS_SEC_P2P_AA_DISTRIBUTION := false
LibItsSecurity_Pics.PICS_SEC_CERTIFICATE_SELECTION := false
LibItsSecurity_Pics.PICS_SEC_CIRCULAR_REGION := false
LibItsSecurity_Pics.PICS_SEC_RECTANGULAR_REGION := false
LibItsSecurity_Pics.PICS_SEC_POLYGONAL_REGION := false
LibItsSecurity_Pics.PICS_SEC_IDENTIFIED_REGION := false
LibItsSecurity_Pics.PICS_SEC_BFK_AUTH := false
[TESTPORT_PARAMETERS]
// Official GN layer syntax; only the security keys are read by the adapter. Verification
// failures discard the IUT transmission (enable_security_checks=1) instead of passing it
// up with a warning, so a PASS below implies a signature the framework verified against
// the pool it loaded from ./certificates.
system.geoNetworkingPort.params := "GN(enable_security_checks=1,sec_db_path=./certificates)"
// No CAM carrier: the IUT is requested to send secured beacons only.
system.utPort.params := "cam_carrier_ms=0"
[LOGGING]
LogFile := "%e.%h-%r.%s"
FileMask := LOG_ALL
ConsoleMask := VERDICTOP | ERROR | WARNING | EXECUTOR
[EXECUTE]
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_MSG_01_BV
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_MSG_01_BO
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_MSG_02_BO
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_CAM_01_BV
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_CAM_02_BV
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_CAM_03_BV
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_CAM_04_BV_XX
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_CAM_01_BO
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_CAM_02_BO
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_CAM_03_BO
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_CAM_04_BO
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_CAM_05_BO
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_CAM_06_BO
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_CAM_07_BO
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_CAM_08_BO
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_CAM_09_BO
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_DENM_01_BV
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_DENM_02_BV_XX
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_DENM_01_BO
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_DENM_02_BO
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_DENM_03_BO
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_DENM_04_BO
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_DENM_05_BO
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_DENM_06_BO
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_DENM_07_BO
ItsSecurity_TestCases.TC_SEC_ITSS_RCV_DENM_09_BO
@@ -0,0 +1,28 @@
[
"TC_SEC_ITSS_RCV_MSG_01_BV",
"TC_SEC_ITSS_RCV_MSG_01_BO",
"TC_SEC_ITSS_RCV_MSG_02_BO",
"TC_SEC_ITSS_RCV_CAM_01_BV",
"TC_SEC_ITSS_RCV_CAM_02_BV",
"TC_SEC_ITSS_RCV_CAM_03_BV",
"TC_SEC_ITSS_RCV_CAM_04_BV_XX",
"TC_SEC_ITSS_RCV_CAM_01_BO",
"TC_SEC_ITSS_RCV_CAM_02_BO",
"TC_SEC_ITSS_RCV_CAM_03_BO",
"TC_SEC_ITSS_RCV_CAM_04_BO",
"TC_SEC_ITSS_RCV_CAM_05_BO",
"TC_SEC_ITSS_RCV_CAM_06_BO",
"TC_SEC_ITSS_RCV_CAM_07_BO",
"TC_SEC_ITSS_RCV_CAM_08_BO",
"TC_SEC_ITSS_RCV_CAM_09_BO",
"TC_SEC_ITSS_RCV_DENM_01_BV",
"TC_SEC_ITSS_RCV_DENM_02_BV_XX",
"TC_SEC_ITSS_RCV_DENM_01_BO",
"TC_SEC_ITSS_RCV_DENM_02_BO",
"TC_SEC_ITSS_RCV_DENM_03_BO",
"TC_SEC_ITSS_RCV_DENM_04_BO",
"TC_SEC_ITSS_RCV_DENM_05_BO",
"TC_SEC_ITSS_RCV_DENM_06_BO",
"TC_SEC_ITSS_RCV_DENM_07_BO",
"TC_SEC_ITSS_RCV_DENM_09_BO"
]
@@ -0,0 +1,57 @@
// Host SUT process: one hex command line in, one hex reply line out.
// vidf_sut [--security-pool DIR [--root NAME] [--aa NAME]... [--at NAME] [--anonymous]]
// [--security-bundle FILE]
// With a pool the reset command builds the secured, beaconing profile from
// DIR/<NAME>.oer and DIR/<at NAME>.vkey (the layout the ETSI ATS certificate
// loader uses as well); a bundle file (credentials.hpp) provisions the same
// through the path a device takes (diagnostic command 9); without either the
// unsecured BTP/GN profile is used.
#include "hil_sut.hpp"
#include <cstdio>
#include <cstring>
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
int main(int argc, char** argv) {
vidf_test::Sut sut;
vidf_test::SecurityProfile profile;
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
const bool has_value = i + 1 < argc;
if (arg == "--security-pool" && has_value) profile.pool = argv[++i];
else if (arg == "--root" && has_value) profile.root = argv[++i];
else if (arg == "--aa" && has_value) { // repeatable; the first use replaces the defaults
static bool replaced = false;
if (!replaced) { profile.authorities.clear(); replaced = true; }
profile.authorities.push_back(argv[++i]);
}
else if (arg == "--at" && has_value) profile.ticket = argv[++i];
else if (arg == "--anonymous") profile.anonymous_address = true;
else if (arg == "--security-bundle" && has_value) {
std::ifstream in(argv[++i], std::ios::binary);
const vanetza::ByteBuffer bundle((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
if (!in || sut.provision(bundle) != vanetza_idf::Result::accepted) { std::fprintf(stderr, "not a credential bundle: %s\n", argv[i]); return 2; }
}
else { std::fprintf(stderr, "unknown argument: %s\n", arg.c_str()); return 2; }
}
sut.configure(profile);
std::string line;
while (std::getline(std::cin, line)) {
if (line.size() > 8192 || line.size() % 2) return 2;
auto nibble = [](char c) -> int {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
};
vanetza::ByteBuffer input;
for (std::size_t i = 0; i < line.size(); i += 2) {
const int a = nibble(line[i]), b = nibble(line[i + 1]);
if (a < 0 || b < 0) return 2;
input.push_back((a << 4) | b);
}
for (auto byte : sut.execute(input)) std::printf("%02x", byte);
std::puts(""); std::fflush(stdout);
}
}
@@ -0,0 +1,279 @@
#include "hil_sut.hpp"
#include <vanetza_idf/nf_sap.hpp>
#if VIDF_SECURITY
#include <vanetza_idf/security.hpp>
#include <vanetza_idf/credentials.hpp>
#include "test_backend.hpp"
#include <fstream>
#include <iterator>
#endif
#if VIDF_CAM || VIDF_DENM
#include <vanetza_idf/facilities.hpp>
#endif
#include <algorithm>
#include <chrono>
namespace vidf_test {
using namespace vanetza_idf;
using vanetza::ByteBuffer;
namespace {
void u16(ByteBuffer& out, std::size_t n) { out.push_back(n >> 8); out.push_back(n); }
std::uint16_t read16(const ByteBuffer& in, std::size_t n) { return (in[n] << 8) | in[n + 1]; }
vanetza::PositionFix fixed_position(double latitude, double longitude, vanetza::Clock::time_point now) {
vanetza::PositionFix fix {};
fix.timestamp = now;
fix.latitude = latitude * vanetza::units::degree;
fix.longitude = longitude * vanetza::units::degree;
fix.speed = 0.0 * vanetza::units::si::meters_per_second;
fix.course = 0.0 * vanetza::units::true_north_degrees;
return fix;
}
}
#if VIDF_SECURITY
// Security profile of the SUT: an application-provisioned trust configuration and ticket
// pool, exactly what a station would load from its own PKI output files.
class Sut::Security : public vanetza::PositionProvider {
public:
TestBackend backend;
security::TrustConfiguration trust;
security::CertificatePool pool {backend};
std::unique_ptr<security::SecurityEntity> entity;
vanetza::PositionFix fix;
const vanetza::PositionFix& position_fix() override { return fix; }
static ByteBuffer read(const std::string& path) {
std::ifstream in(path, std::ios::binary);
if (!in) return {};
return ByteBuffer(std::istreambuf_iterator<char>(in), {});
}
// run-time provisioning: everything the bundle carries, through the same checks
Result load(const ByteBuffer& bundle) {
security::Credentials credentials;
if (!security::decode(bundle, credentials) || credentials.roots.empty() || credentials.tickets.empty())
return Result::invalid_argument;
return security::apply(credentials, trust, pool).result;
}
Result load(const SecurityProfile& profile) {
const auto file = [&](const std::string& name, const char* ext) { return read(profile.pool + "/" + name + ext); };
if (trust.add_root(file(profile.root, ".oer")) != Result::accepted) return Result::invalid_argument;
for (const auto& authority : profile.authorities)
if (trust.add_authority(file(authority, ".oer")) != Result::accepted) return Result::invalid_argument;
const auto key_octets = file(profile.ticket, ".vkey");
vanetza::security::PrivateKey key;
key.type = key_octets.size() == 48 ? vanetza::security::KeyType::BrainpoolP384r1 : vanetza::security::KeyType::NistP256;
key.key = key_octets;
return pool.add(file(profile.ticket, ".oer"), key);
}
};
#else
class Sut::Security {};
#endif
Sut::Sut() = default;
Sut::~Sut() = default;
void Sut::configure(SecurityProfile profile) { profile_ = std::move(profile); }
Result Sut::provision(const ByteBuffer& bundle) {
#if VIDF_SECURITY
security::Credentials probe;
if (!security::decode(bundle, probe)) return Result::invalid_argument; // structure only; reset() applies it
bundle_ = bundle;
return Result::accepted;
#else
(void)bundle;
return Result::unsupported;
#endif
}
void Sut::record(std::uint8_t kind, ByteBuffer bytes) {
if (record_bytes_ + bytes.size() + 3 > 3800 || records_.size() >= 32) { overflow_ = true; return; }
ByteBuffer frame {kind}; u16(frame, bytes.size());
frame.insert(frame.end(), bytes.begin(), bytes.end());
record_bytes_ += frame.size(); records_.push_back(std::move(frame));
}
Result Sut::request(AlDataRequest request) {
record(1, std::move(request.data));
return overflow_ ? Result::resource_limit : Result::accepted;
}
Result Sut::reset() {
stack_.reset(); // Router timer cancellation precedes runtime destruction.
security_.reset();
runtime_ = std::make_unique<vanetza::ManualRuntime>();
StackConfig config;
config.mib.itsGnLocalGnAddr.mid({2, 0, 0, 0, 0, 1});
vanetza::security::SecurityEntity* entity = nullptr;
if (profile_.pool.empty() && bundle_.empty()) {
config.mib.itsGnSecurity = false; // explicit unsecured BTP/GN test PICS
config.mib.vanetzaDisableBeaconing = true;
} else {
#if VIDF_SECURITY
security_ = std::make_unique<Security>();
const auto loaded = bundle_.empty() ? security_->load(profile_) : security_->load(bundle_);
if (loaded != Result::accepted) { security_.reset(); return loaded; }
security_->entity = std::make_unique<security::SecurityEntity>(*runtime_, *security_, security_->backend,
security_->pool, security_->trust);
entity = security_->entity.get();
config.mib.itsGnSecurity = true;
config.mib.vanetzaDisableBeaconing = false; // the Security ATS observes secured beacons (GN-MGMT)
if (profile_.anonymous_address) config.mib.itsGnLocalAddrConfMethod = vanetza::geonet::AddrConfMethod::Anonymous;
#else
return Result::unsupported;
#endif
}
carrier_interval_[0] = carrier_interval_[1] = 0;
carrier_pending_[0] = carrier_pending_[1] = false;
stack_ = std::make_unique<Stack>(config, *runtime_, *this, entity);
stack_->on_receive([this](BtpIndication received) {
auto indication = NF_SAP::BTP_DATA_indication_from(std::move(received));
ByteBuffer data {static_cast<std::uint8_t>(indication.btp_type == BtpType::b)};
u16(data, indication.destination_port);
u16(data, indication.source_port.value_or(indication.destination_port_info.value_or(0)));
data.insert(data.end(), indication.received_fl_sdu.begin(), indication.received_fl_sdu.end());
record(2, std::move(data));
});
stack_->on_receive_gn([this](GnIndication received) {
// GN-DATA.indication with no registered upper protocol (raw test SDU).
record(3, std::move(received.data));
});
return position(52.0, 13.0);
}
// The position vector carries the clock's time; it is refreshed on every tick so the
// router never keeps an unset (epoch) timestamp, which suppresses beacons upstream.
Result Sut::position(double latitude, double longitude) {
fix_ = fixed_position(latitude, longitude, runtime_->now());
#if VIDF_SECURITY
if (security_) security_->fix = fix_;
#endif
return stack_->update_position(fix_);
}
Result Sut::carrier(std::uint8_t kind, std::uint16_t sequence) {
// Syntactically valid Release 2 PDUs so the security profile of the carrier (CAM: TS 103 097
// clause 7.1.1, DENM: clause 7.1.2) can be observed; no CA/DEN service semantics.
#if VIDF_CAM
if (kind == 0) {
facilities::Cam cam;
cam->header.protocolVersion = 2; cam->header.messageId = 2; cam->header.stationId = 42;
auto& cp = cam->cam.camParameters;
cp.basicContainer.stationType = 5;
cp.highFrequencyContainer.present = Vanetza_ITS2_HighFrequencyContainer_PR_rsuContainerHighFrequency;
auto& pos = cp.basicContainer.referencePosition;
pos.latitude = 520000000; pos.longitude = 130000000;
pos.positionConfidenceEllipse.semiMajorAxisLength = 4095;
pos.positionConfidenceEllipse.semiMinorAxisLength = 4095;
pos.positionConfidenceEllipse.semiMajorAxisOrientation = 3601;
pos.altitude.altitudeValue = 800001; pos.altitude.altitudeConfidence = 15;
return facilities::send(*stack_, facilities::Kind::cam, cam.encode(), BtpRequest {});
}
#endif
#if VIDF_DENM
if (kind == 1) {
facilities::Denm denm;
denm->header.protocolVersion = 2; denm->header.messageId = 1; denm->header.stationId = 42;
auto& dm = denm->denm.management;
dm.actionId.originatingStationId = 42;
dm.actionId.sequenceNumber = sequence;
const auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(runtime_->now().time_since_epoch()).count();
if (asn_long2INTEGER(&dm.detectionTime, now_ms) != 0 || asn_long2INTEGER(&dm.referenceTime, now_ms) != 0)
return Result::rejected;
dm.eventPosition.latitude = 520000000; dm.eventPosition.longitude = 130000000;
dm.eventPosition.positionConfidenceEllipse.semiMajorConfidence = 4095;
dm.eventPosition.positionConfidenceEllipse.semiMinorConfidence = 4095;
dm.eventPosition.positionConfidenceEllipse.semiMajorOrientation = 3601;
dm.eventPosition.altitude.altitudeValue = 800001; dm.eventPosition.altitude.altitudeConfidence = 15;
// DENM dissemination is GeoBroadcast into the relevance area around the event
// position (TS 103 831 V2.2.1 clause 5.4.2 / EN 302 637-3 clause 5.4.2), so the
// carrier uses GBC with a circular destination area; only the transport is exercised.
BtpRequest request;
request.transport = vanetza::geonet::TransportType::GBC;
vanetza::geonet::Area area;
vanetza::geonet::Circle circle;
circle.r = 500.0 * vanetza::units::si::meter;
area.shape = circle;
area.position = vanetza::geonet::GeodeticPosition(52.0 * vanetza::units::degree, 13.0 * vanetza::units::degree);
area.angle = vanetza::units::Angle(0.0 * vanetza::units::degree);
request.destination = area;
return facilities::send(*stack_, facilities::Kind::denm, denm.encode(), std::move(request));
}
#endif
return Result::unsupported;
}
ByteBuffer Sut::execute(const ByteBuffer& input) {
records_.clear(); record_bytes_ = 0; overflow_ = false;
Result result = Result::invalid_argument;
try {
if (input.size() == 1 && input[0] == 0) result = reset();
else if (input.size() > 1 && input[0] == 9) {
// credentials for the next reset: [9][credentials.hpp bundle]; allowed before any reset
result = provision(ByteBuffer(input.begin() + 1, input.end()));
}
else if (!stack_) result = Result::rejected;
else if (input.size() >= 6 && input[0] == 1 && input[1] <= 1) {
NF_SAP::BTP_DATA_request request;
request.btp_type = input[1] ? BtpType::b : BtpType::a;
request.destination_port = read16(input, 2);
if (request.btp_type == BtpType::a) request.source_port = read16(input, 4);
else request.destination_port_info = read16(input, 4);
request.fl_sdu.assign(input.begin() + 6, input.end());
request.length = request.fl_sdu.size();
result = NF_SAP::BTP_DATA_request_submit(*stack_, std::move(request));
} else if (input.size() > 13 && input[0] == 2) {
AlDataIndication indication;
std::copy(input.begin() + 1, input.begin() + 7, indication.source.octets.begin());
std::copy(input.begin() + 7, input.begin() + 13, indication.destination.octets.begin());
indication.data.assign(input.begin() + 13, input.end());
result = stack_->indicate(std::move(indication));
} else if (input.size() >= 2 && input[0] == 3) {
// GN-DATA.request, SHB only (GAP-GN-001): [3][raw traffic class][payload].
GnRequest request;
request.traffic_class = vanetza::geonet::TrafficClass(input[1]);
request.data.assign(input.begin() + 2, input.end());
result = stack_->request(std::move(request));
} else if (input.size() == 9 && input[0] == 4) {
// UtGnChangePosition-style fix: [4][lat i32 BE][lon i32 BE], 1e-7 degree units.
const auto i32 = [&](std::size_t at) {
return static_cast<std::int32_t>((std::uint32_t(input[at]) << 24) | (std::uint32_t(input[at + 1]) << 16) |
(std::uint32_t(input[at + 2]) << 8) | input[at + 3]);
};
result = position(i32(1) / 1.0e7, i32(5) / 1.0e7);
} else if (input.size() == 9 && input[0] == 5) {
std::uint64_t microseconds = 0;
for (std::size_t i = 1; i < 9; ++i) microseconds = (microseconds << 8) | input[i];
const vanetza::Clock::time_point time {std::chrono::microseconds(microseconds)};
result = stack_->advance(time); // runs router timers: beacons come back as kind-1 records
if (result == Result::accepted) result = position(fix_.latitude.value(), fix_.longitude.value());
for (std::uint8_t kind = 0; kind < 2 && result == Result::accepted; ++kind) {
if (carrier_pending_[kind]) {
carrier_pending_[kind] = false;
result = carrier(kind, carrier_pending_sequence_[kind]);
} else if (carrier_interval_[kind] && time >= carrier_next_[kind]) {
carrier_next_[kind] = time + std::chrono::milliseconds(carrier_interval_[kind]);
result = carrier(kind, kind == 1 ? ++denm_sequence_ : 0);
}
}
} else if ((input.size() == 2 || input.size() == 4) && (input[0] == 6 || input[0] == 8) && input[1] < 2) {
const std::uint16_t sequence = input.size() == 4 ? read16(input, 2) : 0;
if (input[0] == 6) result = carrier(input[1], sequence);
else {
carrier_pending_[input[1]] = true;
carrier_pending_sequence_[input[1]] = sequence;
result = Result::accepted;
}
} else if (input.size() == 4 && input[0] == 7 && input[1] < 2) {
carrier_interval_[input[1]] = read16(input, 2);
carrier_next_[input[1]] = runtime_->now();
result = Result::accepted;
} else if (!input.empty() && input[0] > 9) result = Result::unsupported;
} catch (const std::bad_alloc&) { result = Result::resource_limit; }
catch (const std::exception&) { result = Result::rejected; }
if (overflow_) result = Result::resource_limit;
ByteBuffer output {static_cast<std::uint8_t>(result), static_cast<std::uint8_t>(records_.size())};
for (const auto& record : records_) output.insert(output.end(), record.begin(), record.end());
return output;
}
}
@@ -0,0 +1,70 @@
#pragma once
#include <vanetza_idf/stack.hpp>
#include <memory>
#include <string>
#include <vector>
namespace vidf_test {
/** Application diagnostic protocol, not an ETSI primitive or verdict codec.
* An external ETSI SUT adapter translates UT operations to these test points.
*
* Commands (first octet):
* 0 reset (unsecured BTP profile, or the security profile when a pool is configured)
* 1 t port info/src sdu BTP-DATA.request
* 2 src dst gnpdu AL_DATA.indication from the lower tester
* 3 tc sdu GN-DATA.request (SHB)
* 4 lat lon position fix (1e-7 degree, i32 BE)
* 5 time advance the ITS clock to the given microseconds since 2004-01-01 (u64 BE);
* spontaneous transmissions (beacons) are reported in the reply
* 6 kind [seq u16] carrier stimulus: send a syntactically valid CAM (0) or DENM (1, actionId
* sequence number seq) through facilities::send; test-application behaviour,
* not a CA/DEN service
* 7 kind interval u16 periodic carrier every interval ms on ticks (0 = stop)
* 8 kind [seq u16] as 6, but sent with the next clock advance (5) so the transmission is
* reported to whoever drives the clock, not to the caller
* Reply: [result][record count]{[kind][u16 length][bytes]}: kind 1 = AL_DATA.request
* transmitted, 2 = BTP-DATA.indication, 3 = GN-DATA.indication.
*/
struct SecurityProfile {
std::string pool; // directory with <name>.oer (COER certificate) and <name>.vkey (raw private key)
std::string root = "CERT_IUT_A_RCA";
// every AA the station trusts as an issuer (its own and, for the receiving-side campaign, the
// test system's); ATs of other AAs must arrive through P2P certificate distribution
std::vector<std::string> authorities {"CERT_IUT_A_AA", "CERT_TS_A_AA"};
std::string ticket = "CERT_IUT_A_AT";
bool anonymous_address = false; // itsGnLocalAddrConfMethod ANONYMOUS: MID from the ticket digest
};
class Sut final : public vanetza_idf::Access {
public:
Sut();
~Sut();
/// configure the security profile used by the next reset; empty pool = unsecured
void configure(SecurityProfile);
/// credentials handed over at run time (diagnostic command 9, a credentials.hpp
/// bundle): used by the next reset instead of the pool directory; empty = none
vanetza_idf::Result provision(const vanetza::ByteBuffer& bundle);
vanetza::ByteBuffer execute(const vanetza::ByteBuffer&);
vanetza_idf::Result request(vanetza_idf::AlDataRequest) override;
private:
class Security;
SecurityProfile profile_;
vanetza::ByteBuffer bundle_;
std::unique_ptr<vanetza::ManualRuntime> runtime_;
std::unique_ptr<Security> security_;
std::unique_ptr<vanetza_idf::Stack> stack_;
std::vector<vanetza::ByteBuffer> records_;
std::size_t record_bytes_ = 0;
bool overflow_ = false;
vanetza::PositionFix fix_;
std::uint16_t carrier_interval_[2] = {0, 0};
vanetza::Clock::time_point carrier_next_[2];
bool carrier_pending_[2] = {false, false};
std::uint16_t carrier_pending_sequence_[2] = {0, 0};
std::uint16_t denm_sequence_ = 0;
void record(std::uint8_t, vanetza::ByteBuffer);
vanetza_idf::Result reset();
vanetza_idf::Result position(double latitude, double longitude);
vanetza_idf::Result carrier(std::uint8_t kind, std::uint16_t sequence);
};
}
@@ -0,0 +1,692 @@
// vidf_issue: host-only issuing tool for a lab trust chain under an existing root
// (TS 103 097 V2.2.1 clause 7.2 certificate profiles, IEEE Std 1609.2 clause 5.3.1
// signatures), writing the pool layout the SUT, the test system and the device
// provisioning read (<id>.oer, <id>.vkey raw private scalar, index.lst).
//
// vidf_issue root --key KEY --name NAME --id ID --out DIR [--start T] [--years N] [--like ROOT.oer]
// vidf_issue authority --issuer CERT.oer --issuer-key KEY --name NAME --id ID --out DIR [--start T] [--years N]
// also writes <id>.ekey: the private half of the ECIES
// encryption key embedded in the certificate (clause 7.2.4)
// vidf_issue ticket --issuer AA.oer --issuer-key KEY --id ID --out DIR [--start T] [--hours H]
// [--permission PSID[:HEXSSP]]... [--region LAT,LON,RADIUS_M] [--root ROOT.oer]
// vidf_issue show CERT.oer digest, validity, permissions, region
// vidf_issue verify CERT.oer [ISSUER.oer [ROOT.oer]] clause 5.3.1 signatures, IEEE 1609.2 clause 5.1.2
// permission/region/time consistency of the chain
// vidf_issue ctl --issuer ROOT.oer --issuer-key KEY --out FILE [--sequence N] [--next-update T]
// [--aa CERT.oer[=URL]]... [--ea CERT.oer[=URL]]... [--dc URL[=HASHEDID8,...]]...
// TS 102 941 clause 6.3.2/6.3.4 RcaCertificateTrustListMessage (FullCtl)
// vidf_issue crl --issuer ROOT.oer --issuer-key KEY --out FILE [--next-update T] [--revoke HASHEDID8]...
// TS 102 941 clause 6.3.3 CertificateRevocationListMessage
// vidf_issue inspect FILE --root ROOT.oer read a CTL or CRL back as an ITS-S would (clause 6.3.6)
//
// TS 102 941 clause 6.2.3 enrolment/authorization, offline steps around a real HTTP
// transport (ports/esp_idf/tools/local_pki.py / pki_client.py carry the bytes; nothing
// here touches a network):
// vidf_issue enrol-request --ea EA.oer --canonical-key KEY --its-id ID [--permission PSID[:HEXSSP]]...
// --out REQUEST.bin --context CONTEXT.bin --out-key EC.vkey [--start T]
// vidf_issue enrol-response --ea EA.oer --context CONTEXT.bin --response RESPONSE.bin --out EC.oer
// vidf_issue authorize-request --ea EA.oer --aa AA.oer --ec EC.oer --ec-key EC.vkey
// [--permission PSID[:HEXSSP]]... [--hours H] [--start T]
// --out REQUEST.bin --context CONTEXT.bin --out-key AT.vkey
// vidf_issue authorize-response --aa AA.oer --context CONTEXT.bin --response RESPONSE.bin --out AT.oer
// vidf_issue ea-respond --ea EA.oer --ea-key KEY --ea-enc-key EA.ekey --request REQUEST.bin --out RESPONSE.bin
// --dir DIR (--canonical-key KEY | --current-ec EC.oer) [--name NAME] [--years N] [--deny CODE]
// issues an EC and stores it under DIR (index.lst) for aa-respond
// vidf_issue aa-respond --aa AA.oer --aa-key KEY --aa-enc-key AA.ekey
// --ea EA.oer --ea-key KEY --ea-enc-key EA.ekey
// --ec-dir DIR --request REQUEST.bin --out RESPONSE.bin [--hours H] [--deny CODE]
// validates entitlement (clause 6.2.3.3, "AA <-> EA")
// against every EC ea-respond stored under --ec-dir
// ea-respond/aa-respond are a lab authority, not a production PKI: --canonical-key is the
// same private key file the enrolling station used (a real EA only ever sees the public
// half, registered out of band; a lab tool run on the same machine is handed the file
// directly), and there is no replay protection, no butterfly keys, no revocation.
//
// KEY is a PEM private key (OpenSSL reads it; an encrypted PEM prompts for the pass
// phrase on the terminal, nothing is echoed or written) or a raw 32-octet .vkey file.
// T is an ISO 8601 UTC instant (2026-09-14T12:00:00Z); the default start is one hour
// before now, never before the issuer's own start. An authority's issuing permissions
// and region are derived from its issuer (IEEE Std 1609.2 6.4.28/6.4.17); a ticket
// inherits the issuer's region unless --region names a circle. --like copies the
// permissions and region of an existing root into a lab root with a throwaway key (a
// rehearsal twin of a real root). Nothing is written when the result would not verify
// as a consistent chain (the library's own rules).
// Curves: NIST P-256 only. This is issuing for a lab or test environment, not a
// certification authority.
#include "pki_authority.hpp"
#include "test_trust_domain.hpp"
#include "test_backend.hpp"
#include <vanetza_idf/its_time.hpp>
#include <vanetza_idf/security.hpp>
#include <vanetza_idf/pki.hpp>
#if VIDF_BACKEND_OPENSSL
#include <vanetza_idf/ecies_openssl.hpp>
#endif
#if VIDF_BACKEND_MBEDTLS
#include <vanetza_idf/ecies_mbedtls.hpp>
#endif
#include <vanetza/common/clock.hpp>
#include <vanetza/security/v3/certificate.hpp>
#include <openssl/bn.h>
#include <openssl/ec.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <iterator>
#include <map>
#include <optional>
#include <sstream>
#include <string>
#include <vector>
using namespace vanetza;
using namespace vanetza::security;
using vanetza::security::v3::Certificate;
namespace {
ByteBuffer read_file(const std::string& path) {
std::ifstream in(path, std::ios::binary);
if (!in) throw std::runtime_error("cannot read " + path);
return ByteBuffer(std::istreambuf_iterator<char>(in), {});
}
void write_file(const std::string& path, const ByteBuffer& bytes) {
std::ofstream out(path, std::ios::binary);
out.write(reinterpret_cast<const char*>(bytes.data()), bytes.size());
if (!out) throw std::runtime_error("cannot write " + path);
}
std::string hex(const ByteBuffer& b) {
static const char* digits = "0123456789ABCDEF";
std::string s;
for (auto v : b) { s += digits[v >> 4]; s += digits[v & 15]; }
return s;
}
std::string digest_hex(const Certificate& c) { // one optional, one pair of iterators
const auto digest = c.calculate_digest();
return digest ? hex(ByteBuffer(digest->begin(), digest->end())) : std::string("?");
}
ByteBuffer from_hex(const std::string& s) {
ByteBuffer out;
for (std::size_t i = 0; i + 1 < s.size(); i += 2) out.push_back(static_cast<std::uint8_t>(std::stoul(s.substr(i, 2), nullptr, 16)));
return out;
}
// A private key from PEM (pass phrase prompted by OpenSSL) or a raw .vkey scalar; P-256 only.
vidf_test::TrustDomain::KeyMaterial load_key(const std::string& path, Backend& backend) {
vidf_test::TrustDomain::KeyMaterial material;
material.priv.type = KeyType::NistP256;
material.pub.type = KeyType::NistP256;
material.pub.compression = KeyCompression::NoCompression;
if (path.size() > 4 && path.compare(path.size() - 4, 4, ".pem") == 0) {
FILE* fp = std::fopen(path.c_str(), "r");
if (!fp) throw std::runtime_error("cannot open " + path);
EVP_PKEY* pkey = PEM_read_PrivateKey(fp, nullptr, nullptr, nullptr); // prompts for an encrypted PEM
std::fclose(fp);
if (!pkey) throw std::runtime_error("not a readable PEM private key (wrong pass phrase?)");
EC_KEY* ec = EVP_PKEY_get1_EC_KEY(pkey);
EVP_PKEY_free(pkey);
if (!ec || EC_GROUP_get_curve_name(EC_KEY_get0_group(ec)) != NID_X9_62_prime256v1) {
if (ec) EC_KEY_free(ec);
throw std::runtime_error("PEM key is not a NIST P-256 key");
}
material.priv.key.assign(32, 0);
BN_bn2binpad(EC_KEY_get0_private_key(ec), material.priv.key.data(), 32);
EC_KEY_free(ec);
} else {
material.priv.key = read_file(path);
if (material.priv.key.size() != 32) throw std::runtime_error("a raw key file must hold 32 octets");
}
// public point from the scalar (OpenSSL, host only), so PEM and raw keys are treated alike
(void)backend;
EC_KEY* ec = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
BIGNUM* scalar = BN_bin2bn(material.priv.key.data(), 32, nullptr);
EC_POINT* point = EC_POINT_new(EC_KEY_get0_group(ec));
BIGNUM* x = BN_new();
BIGNUM* y = BN_new();
const bool ok = ec && scalar && point && EC_POINT_mul(EC_KEY_get0_group(ec), point, scalar, nullptr, nullptr, nullptr) == 1 &&
EC_POINT_get_affine_coordinates(EC_KEY_get0_group(ec), point, x, y, nullptr) == 1;
if (ok) {
material.pub.x.assign(32, 0); material.pub.y.assign(32, 0);
BN_bn2binpad(x, material.pub.x.data(), 32);
BN_bn2binpad(y, material.pub.y.data(), 32);
}
BN_free(x); BN_free(y); EC_POINT_free(point); BN_clear_free(scalar); EC_KEY_free(ec);
if (!ok) throw std::runtime_error("cannot derive the public key");
return material;
}
Clock::time_point parse_time(const std::string& iso) {
int y, mo, d, h, mi, s;
if (std::sscanf(iso.c_str(), "%d-%d-%dT%d:%d:%dZ", &y, &mo, &d, &h, &mi, &s) != 6)
throw std::runtime_error("time must be YYYY-MM-DDTHH:MM:SSZ");
// days from civil (Howard Hinnant), proleptic Gregorian, UTC
y -= mo <= 2;
const std::int64_t era = (y >= 0 ? y : y - 399) / 400;
const std::int64_t yoe = y - era * 400;
const std::int64_t doy = (153 * (mo + (mo > 2 ? -3 : 9)) + 2) / 5 + d - 1;
const std::int64_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
const std::int64_t unix_seconds = (era * 146097 + doe - 719468) * 86400 + h * 3600 + mi * 60 + s;
return Clock::time_point(std::chrono::microseconds(vanetza_idf::its_time::microseconds_since_epoch(unix_seconds)));
}
Clock::time_point now() {
return Clock::time_point(vanetza_idf::its_time::since_epoch(std::chrono::system_clock::now()));
}
Certificate load_certificate(const std::string& path) {
Certificate certificate;
if (!certificate.decode(read_file(path))) throw std::runtime_error("not a COER EtsiTs103097Certificate: " + path);
return certificate;
}
void store(const std::string& dir, const std::string& id, const Certificate& certificate, const PrivateKey* key) {
const auto digest = certificate.calculate_digest();
if (!digest) throw std::runtime_error("certificate has no digest");
write_file(dir + "/" + id + ".oer", certificate.encode());
if (key) write_file(dir + "/" + id + ".vkey", key->key);
std::ofstream index(dir + "/index.lst", std::ios::app);
index << hex(ByteBuffer(digest->begin(), digest->end())) << ' ' << id << ".oer\n";
std::printf("%s HashedId8 %s -> %s/%s.oer%s\n", id.c_str(), hex(ByteBuffer(digest->begin(), digest->end())).c_str(),
dir.c_str(), id.c_str(), key ? " (+ .vkey)" : "");
}
void show(const Certificate& c) {
const auto digest = c.calculate_digest();
const auto validity = c.get_start_and_end_validity();
std::printf("HashedId8 %s\n", digest ? hex(ByteBuffer(digest->begin(), digest->end())).c_str() : "?");
const auto issuer = c.issuer_digest(); // one optional, one pair of iterators
std::printf("issuer %s\n", c.issuer_is_self() ? "self" : issuer ? hex(ByteBuffer(issuer->begin(), issuer->end())).c_str() : "?");
std::printf("type %s\n", c.is_ca_certificate() ? "CA (certIssuePermissions)" : c.is_at_certificate() ? "authorization ticket" : "other");
std::printf("validity Time32 %u .. %u (Unix %lld .. %lld)\n", unsigned(validity.start_validity), unsigned(validity.end_validity),
static_cast<long long>(vanetza_idf::its_time::unix_microseconds(static_cast<std::int64_t>(validity.start_validity) * 1000000) / 1000000),
static_cast<long long>(vanetza_idf::its_time::unix_microseconds(static_cast<std::int64_t>(validity.end_validity) * 1000000) / 1000000));
if (const auto* permissions = c->toBeSigned.appPermissions) {
for (int i = 0; i < permissions->list.count; ++i) {
const auto* entry = permissions->list.array[i];
std::printf("appPermission psid %ld", static_cast<long>(entry->psid));
if (entry->ssp && entry->ssp->present == Vanetza_Security_ServiceSpecificPermissions_PR_bitmapSsp)
std::printf(" ssp %s", hex(ByteBuffer(entry->ssp->choice.bitmapSsp.buf, entry->ssp->choice.bitmapSsp.buf + entry->ssp->choice.bitmapSsp.size)).c_str());
std::printf("\n");
}
}
if (const auto* groups = c->toBeSigned.certIssuePermissions) {
for (int i = 0; i < groups->list.count; ++i) {
const auto* group = groups->list.array[i];
// IEEE Std 1609.2 6.4.28: absent minChainLength = 1, chainLengthRange = 0, eeType = {app}
const long min = group->minChainLength ? *group->minChainLength : 1;
const unsigned ee = group->eeType && group->eeType->size ? group->eeType->buf[0] : 0x80;
std::printf("certIssue chain length %ld..%s, eeType%s%s:", min,
group->chainLengthRange < 0 ? "unbounded" : std::to_string(min + group->chainLengthRange).c_str(),
ee & 0x80 ? " app" : "", ee & 0x40 ? " enrol" : "");
if (group->subjectPermissions.present == Vanetza_Security_SubjectPermissions_PR_all) std::printf(" all");
else if (group->subjectPermissions.present == Vanetza_Security_SubjectPermissions_PR_explicit) {
const auto& ranges = group->subjectPermissions.choice.Explicit.list;
for (int k = 0; k < ranges.count; ++k) {
const auto* range = ranges.array[k];
std::printf(" %ld", static_cast<long>(range->psid));
if (range->sspRange && range->sspRange->present == Vanetza_Security_SspRange_PR_bitmapSspRange) {
const auto& bm = range->sspRange->choice.bitmapSspRange;
std::printf("(%s/%s)", hex(ByteBuffer(bm.sspValue.buf, bm.sspValue.buf + bm.sspValue.size)).c_str(),
hex(ByteBuffer(bm.sspBitmask.buf, bm.sspBitmask.buf + bm.sspBitmask.size)).c_str());
} else if (range->sspRange && range->sspRange->present == Vanetza_Security_SspRange_PR_opaque) {
std::printf("(opaque)");
}
}
}
std::printf("\n");
}
}
if (const auto* region = c->toBeSigned.region) {
switch (region->present) {
case Vanetza_Security_GeographicRegion_PR_circularRegion:
std::printf("region circle %ld,%ld r=%ld m\n", static_cast<long>(region->choice.circularRegion.center.latitude),
static_cast<long>(region->choice.circularRegion.center.longitude), static_cast<long>(region->choice.circularRegion.radius));
break;
case Vanetza_Security_GeographicRegion_PR_identifiedRegion: {
std::printf("region identified (UN M49 country codes):");
const auto& list = region->choice.identifiedRegion.list;
for (int i = 0; i < list.count; ++i) {
const auto* entry = list.array[i];
if (!entry) continue;
if (entry->present == Vanetza_Security_IdentifiedRegion_PR_countryOnly) std::printf(" %ld", static_cast<long>(entry->choice.countryOnly));
else if (entry->present == Vanetza_Security_IdentifiedRegion_PR_countryAndRegions) std::printf(" %ld(regions)", static_cast<long>(entry->choice.countryAndRegions.countryOnly));
else if (entry->present == Vanetza_Security_IdentifiedRegion_PR_countryAndSubregions) std::printf(" %ld(subregions)", static_cast<long>(entry->choice.countryAndSubregions.country));
else std::printf(" ?");
}
std::printf("\n");
break;
}
default:
std::printf("region restricted (rectangular/polygonal)\n");
}
} else {
std::printf("region none\n");
}
}
// The chain the library would verify: signatures, permissions, regions and validity nesting.
// chain[0] is the subject, then its issuer and so on. Prints each finding; false on any.
bool chain_ok(vidf_test::TrustDomain& domain, const std::vector<const Certificate*>& chain) {
bool ok = true;
for (std::size_t i = 0; i < chain.size(); ++i) {
const Certificate& subject = *chain[i];
const Certificate& issuer = i + 1 < chain.size() ? *chain[i + 1] : subject;
if (i + 1 == chain.size() && !subject.issuer_is_self()) { std::printf(" chain ends at a certificate that is not self-signed (issuer not given)\n"); continue; }
const bool signature = domain.verify_chain_signature(subject, issuer);
std::printf(" signature of %zu: %s\n", i, signature ? "verifies" : "does NOT verify");
ok = ok && signature;
if (i + 1 < chain.size()) {
const auto s_valid = subject.get_start_and_end_validity();
const auto i_valid = issuer.get_start_and_end_validity();
const bool time = i_valid.start_validity <= s_valid.start_validity && i_valid.end_validity >= s_valid.end_validity;
std::printf(" validity of %zu inside %zu: %s\n", i, i + 1, time ? "yes" : "NO (IEEE 1609.2: a certificate is not valid outside its issuer's validity)");
const bool region = vanetza_idf::security::region_within(subject, issuer, true);
std::printf(" region of %zu inside %zu: %s\n", i, i + 1, region ? "yes (a geometric region under an identified one is accepted by policy only)" : "NO");
ok = ok && time && region;
}
}
if (chain.size() > 1) {
const bool permissions = vanetza_idf::security::chain_permissions_consistent(chain);
std::printf(" permissions consistent along the chain (IEEE 1609.2 5.1.2): %s\n", permissions ? "yes" : "NO");
ok = ok && permissions;
}
return ok;
}
// default start: an hour ago, but never before the issuer became valid
Clock::time_point default_start(const Certificate* issuer) {
Clock::time_point start = now() - std::chrono::hours(1);
if (issuer) {
const auto issuer_start = Clock::time_point(std::chrono::seconds(issuer->get_start_and_end_validity().start_validity));
if (issuer_start > start) {
start = issuer_start;
std::printf("note: the issuer is valid from Time32 %u only; the start is set to that instant\n",
static_cast<unsigned>(issuer->get_start_and_end_validity().start_validity));
}
}
return start;
}
using Options = std::map<std::string, std::vector<std::string>>;
Options parse(int argc, char** argv, int from) {
Options options;
for (int i = from; i < argc; ++i) {
std::string arg = argv[i];
if (arg.rfind("--", 0) == 0 && i + 1 < argc) options[arg].push_back(argv[++i]);
else options["positional"].push_back(arg);
}
return options;
}
std::string one(const Options& o, const char* name, const char* fallback = nullptr) {
auto it = o.find(name);
if (it != o.end() && !it->second.empty()) return it->second.back();
if (fallback) return fallback;
throw std::runtime_error(std::string("missing ") + name);
}
// --permission PSID[:HEXSSP]..., or a fallback set when none is given.
vidf_test::TrustDomain::Permissions parse_permissions(const Options& options, vidf_test::TrustDomain::Permissions fallback) {
auto it = options.find("--permission");
if (it == options.end()) return fallback;
vidf_test::TrustDomain::Permissions permissions;
for (const auto& spec : it->second) {
const auto colon = spec.find(':');
permissions.emplace_back(static_cast<ItsAid>(std::stoul(spec.substr(0, colon))),
colon == std::string::npos ? ByteBuffer {} : from_hex(spec.substr(colon + 1)));
}
return permissions;
}
// The 32-octet RequestContext (clause 6.2.3.2.1/6.2.3.3.1: the AES key a response is
// encrypted with, and the 16-octet request hash it must echo), carried between the
// *-request and *-response steps as a small file.
void write_context(const std::string& path, const vanetza_idf::pki::RequestContext& context) {
ByteBuffer bytes(context.aes_key.begin(), context.aes_key.end());
bytes.insert(bytes.end(), context.request_hash.begin(), context.request_hash.end());
write_file(path, bytes);
}
vanetza_idf::pki::RequestContext read_context(const std::string& path) {
const auto bytes = read_file(path);
if (bytes.size() != 32) throw std::runtime_error("a context file must hold 32 octets (16 AES key + 16 request hash)");
vanetza_idf::pki::RequestContext context;
std::copy(bytes.begin(), bytes.begin() + 16, context.aes_key.begin());
std::copy(bytes.begin() + 16, bytes.end(), context.request_hash.begin());
return context;
}
#if VIDF_BACKEND_OPENSSL
using ToolEcies = vanetza_idf::pki::EciesOpenSsl;
#else
using ToolEcies = vanetza_idf::pki::EciesMbedTls;
#endif
} // namespace
int main(int argc, char** argv) try {
if (argc < 2) { std::fprintf(stderr, "usage: see the header of issue_tool.cpp\n"); return 2; }
const std::string command = argv[1];
vidf_test::TestBackend backend;
ToolEcies ecies;
vidf_test::TrustDomain domain {backend, now()}; // the building blocks; its own generated chain is unused
const Options options = parse(argc, argv, 2);
if (command == "show") {
show(load_certificate(one(options, "positional")));
return 0;
}
if (command == "inspect") {
const Certificate rca = load_certificate(one(options, "--root"));
const ByteBuffer message = read_file(one(options, "positional"));
if (const auto ctl = vanetza_idf::pki::parse_rca_ctl(backend, message, rca)) {
std::printf("RCA CTL of %s: %s, sequence %u, nextUpdate Time32 %u\n", digest_hex(rca).c_str(),
ctl->full ? "full" : "delta", unsigned(ctl->sequence), unsigned(ctl->next_update));
for (const auto& ea : ctl->ea) std::printf(" ea %s\n", digest_hex(ea).c_str());
for (const auto& aa : ctl->aa) std::printf(" aa %s\n", digest_hex(aa).c_str());
for (const auto& dc : ctl->dc) {
std::printf(" dc %s:", dc.url.c_str());
for (const auto& id : dc.certificates) std::printf(" %s", hex(ByteBuffer(id.begin(), id.end())).c_str());
std::printf("\n");
}
for (const auto& id : ctl->deleted) std::printf(" delete %s\n", hex(ByteBuffer(id.begin(), id.end())).c_str());
return 0;
}
if (const auto crl = vanetza_idf::pki::parse_crl(backend, message, rca)) {
std::printf("CRL of %s: thisUpdate Time32 %u, nextUpdate Time32 %u, %zu revoked\n",
digest_hex(rca).c_str(),
unsigned(crl->this_update), unsigned(crl->next_update), crl->revoked.size());
for (const auto& id : crl->revoked) std::printf(" revoked %s\n", hex(ByteBuffer(id.begin(), id.end())).c_str());
return 0;
}
std::printf("neither a CTL nor a CRL signed by that root\n");
return 1;
}
if (command == "ctl" || command == "crl") {
const Certificate rca = load_certificate(one(options, "--issuer"));
const auto key = load_key(one(options, "--issuer-key"), backend).priv;
const std::string out = one(options, "--out");
const Clock::time_point at = now();
const auto next_update = options.count("--next-update")
? static_cast<vanetza_idf::pki::Time32>(std::chrono::duration_cast<std::chrono::seconds>(parse_time(one(options, "--next-update")).time_since_epoch()).count())
: static_cast<vanetza_idf::pki::Time32>(std::chrono::duration_cast<std::chrono::seconds>(at.time_since_epoch()).count() + 7 * 86400);
std::optional<ByteBuffer> message;
if (command == "ctl") {
vanetza_idf::pki::TrustListEntries entries;
const auto authority = [&](const std::string& spec) {
const auto eq = spec.find('=');
vanetza_idf::pki::TrustListEntries::Authority a;
a.certificate = read_file(spec.substr(0, eq));
if (eq != std::string::npos) a.access_point = spec.substr(eq + 1);
return a;
};
if (auto it = options.find("--ea"); it != options.end()) for (const auto& spec : it->second) entries.ea.push_back(authority(spec));
if (auto it = options.find("--aa"); it != options.end()) for (const auto& spec : it->second) entries.aa.push_back(authority(spec));
if (auto it = options.find("--dc"); it != options.end()) {
for (const auto& spec : it->second) {
vanetza_idf::pki::TrustListEntries::DistributionCentre dc;
const auto eq = spec.find('=');
dc.url = spec.substr(0, eq);
std::string ids = eq == std::string::npos ? std::string() : spec.substr(eq + 1);
if (ids.empty()) dc.certificates.push_back(*rca.calculate_digest()); // clause 6.3.2: at least the RCA itself
for (std::size_t from = 0; from < ids.size();) {
const auto comma = ids.find(',', from);
const ByteBuffer raw = from_hex(ids.substr(from, comma == std::string::npos ? std::string::npos : comma - from));
if (raw.size() != 8) throw std::runtime_error("--dc URL=HASHEDID8,... needs 8-octet hex digests");
HashedId8 id; std::copy(raw.begin(), raw.end(), id.begin());
dc.certificates.push_back(id);
from = comma == std::string::npos ? ids.size() : comma + 1;
}
entries.dc.push_back(std::move(dc));
}
}
if (entries.dc.empty()) throw std::runtime_error("a root CTL needs at least one --dc URL (TS 102 941 clause 6.3.4)");
message = vanetza_idf::pki::build_rca_ctl(backend, at, rca, key, entries, next_update, std::stoul(one(options, "--sequence", "1")));
if (message) {
const auto back = vanetza_idf::pki::parse_rca_ctl(backend, *message, rca);
if (!back) throw std::runtime_error("the CTL does not read back (an entry not issued by this root?)");
}
} else {
std::vector<HashedId8> revoked;
if (auto it = options.find("--revoke"); it != options.end()) {
for (const auto& spec : it->second) {
const ByteBuffer raw = from_hex(spec);
if (raw.size() != 8) throw std::runtime_error("--revoke needs an 8-octet hex HashedId8");
HashedId8 id; std::copy(raw.begin(), raw.end(), id.begin());
revoked.push_back(id);
}
}
const auto this_update = static_cast<vanetza_idf::pki::Time32>(std::chrono::duration_cast<std::chrono::seconds>(at.time_since_epoch()).count());
message = vanetza_idf::pki::build_crl(backend, at, rca, key, revoked, this_update, next_update);
}
if (!message) throw std::runtime_error("the root cannot sign this list (missing CTL/CRL appPermissions, psid 624/622?)");
write_file(out, *message);
std::printf("%s written: %zu octets -> %s\n", command == "ctl" ? "RCA CTL" : "CRL", message->size(), out.c_str());
return 0;
}
if (command == "verify") {
const auto& args = options.at("positional");
std::vector<Certificate> loaded;
for (const auto& path : args) loaded.push_back(load_certificate(path));
std::vector<const Certificate*> chain;
for (const auto& c : loaded) chain.push_back(&c);
const bool ok = chain_ok(domain, chain);
std::printf("%s\n", ok ? "chain verifies" : "chain does NOT verify");
return ok ? 0 : 1;
}
if (command == "enrol-request") {
const Certificate ea = load_certificate(one(options, "--ea"));
const auto canonical = load_key(one(options, "--canonical-key"), backend);
const std::string its_id_str = one(options, "--its-id");
vanetza_idf::pki::EnrolmentRequestParameters params;
params.its_id.assign(its_id_str.begin(), its_id_str.end());
const auto ec_key = ecies.generate_key(KeyType::NistP256);
params.verification_key = ec_key;
params.app_permissions = parse_permissions(options, {{aid::SCR, {0x01, 0xc0}}});
params.outer_signer_key = canonical.priv;
const Clock::time_point at = options.count("--start") ? parse_time(one(options, "--start")) : now();
ByteBuffer request;
vanetza_idf::pki::RequestContext context;
if (vanetza_idf::pki::build_enrolment_request(backend, ecies, at, params, ea, request, context) != vanetza_idf::Result::accepted)
throw std::runtime_error("EnrolmentRequest not built (missing EA encryptionKey or bad parameters)");
write_file(one(options, "--out"), request);
write_context(one(options, "--context"), context);
write_file(one(options, "--out-key"), ec_key.priv.key);
std::printf("EnrolmentRequest written: %zu octets -> %s\n", request.size(), one(options, "--out").c_str());
return 0;
}
if (command == "enrol-response") {
const Certificate ea = load_certificate(one(options, "--ea"));
const auto context = read_context(one(options, "--context"));
const ByteBuffer response = read_file(one(options, "--response"));
vanetza_idf::pki::EnrolmentResponse decoded;
if (vanetza_idf::pki::parse_enrolment_response(backend, ecies, context, ea, response, decoded) != vanetza_idf::Result::accepted)
throw std::runtime_error("EnrolmentResponse rejected (decrypt/signature/requestHash failure)");
if (decoded.response_code != 0 || !decoded.certificate)
throw std::runtime_error("EA declined the request, responseCode " + std::to_string(decoded.response_code));
write_file(one(options, "--out"), decoded.certificate->encode());
std::printf("EC written -> %s (HashedId8 %s)\n", one(options, "--out").c_str(), digest_hex(*decoded.certificate).c_str());
return 0;
}
if (command == "authorize-request") {
const Certificate ea = load_certificate(one(options, "--ea"));
const Certificate aa = load_certificate(one(options, "--aa"));
const Certificate ec = load_certificate(one(options, "--ec"));
const auto ec_key = load_key(one(options, "--ec-key"), backend).priv;
vanetza_idf::pki::AuthorizationRequestParameters params;
const auto at_key = ecies.generate_key(KeyType::NistP256);
params.verification_key = at_key;
params.app_permissions = parse_permissions(options, {{aid::CA, {0x01, 0xff, 0xfc}}, {aid::VRU, {0x01}}});
const unsigned hours = std::stoul(one(options, "--hours", "24"));
const Clock::time_point start = options.count("--start") ? parse_time(one(options, "--start")) : default_start(&ec);
params.validity_period = std::make_pair(start, static_cast<std::uint16_t>(hours));
params.ec = &ec;
params.ec_key = ec_key;
ByteBuffer request;
vanetza_idf::pki::RequestContext context;
if (vanetza_idf::pki::build_authorization_request(backend, ecies, now(), params, ea, aa, request, context) != vanetza_idf::Result::accepted)
throw std::runtime_error("AuthorizationRequest not built");
write_file(one(options, "--out"), request);
write_context(one(options, "--context"), context);
write_file(one(options, "--out-key"), at_key.priv.key);
std::printf("AuthorizationRequest written: %zu octets -> %s\n", request.size(), one(options, "--out").c_str());
return 0;
}
if (command == "authorize-response") {
const Certificate aa = load_certificate(one(options, "--aa"));
const auto context = read_context(one(options, "--context"));
const ByteBuffer response = read_file(one(options, "--response"));
vanetza_idf::pki::AuthorizationResponse decoded;
if (vanetza_idf::pki::parse_authorization_response(backend, ecies, context, aa, response, decoded) != vanetza_idf::Result::accepted)
throw std::runtime_error("AuthorizationResponse rejected (decrypt/signature/requestHash failure)");
if (decoded.response_code != 0 || !decoded.certificate)
throw std::runtime_error("AA declined the request, responseCode " + std::to_string(decoded.response_code));
write_file(one(options, "--out"), decoded.certificate->encode());
std::printf("AT written -> %s (HashedId8 %s)\n", one(options, "--out").c_str(), digest_hex(*decoded.certificate).c_str());
return 0;
}
if (command == "ea-respond") {
vidf_test::Credential ea;
ea.certificate = load_certificate(one(options, "--ea"));
ea.key = load_key(one(options, "--ea-key"), backend).priv;
const auto ea_encryption_key = load_key(one(options, "--ea-enc-key"), backend).priv;
const bool has_canonical = options.count("--canonical-key") != 0;
const bool has_current_ec = options.count("--current-ec") != 0;
if (has_canonical == has_current_ec)
throw std::runtime_error("give exactly one of --canonical-key (initial enrolment) or --current-ec (re-enrolment)");
std::optional<PublicKey> canonical_pub;
std::optional<Certificate> current_ec;
if (has_canonical) canonical_pub = load_key(one(options, "--canonical-key"), backend).pub;
else current_ec = load_certificate(one(options, "--current-ec"));
const ByteBuffer request = read_file(one(options, "--request"));
std::array<std::uint8_t, 16> aes_key {};
auto parsed = vidf_test::parse_enrolment_request(backend, ecies, ea, ea_encryption_key, request,
canonical_pub ? &*canonical_pub : nullptr,
current_ec ? &*current_ec : nullptr, aes_key);
vidf_test::Authority authority {backend, ecies};
ByteBuffer response;
if (!parsed) {
std::printf("ea-respond: request did not decrypt/verify/decode; nothing was issued\n");
return 1;
}
if (options.count("--deny")) {
response = authority.enrolment_response(now(), aes_key, request,
static_cast<std::uint8_t>(std::stoul(one(options, "--deny"))), nullptr, ea);
} else {
const std::string name = one(options, "--name", "vidf-station EC");
const unsigned hours = std::stoul(one(options, "--hours", "8760"));
const Clock::time_point start = options.count("--start") ? parse_time(one(options, "--start")) : now() - std::chrono::hours(1);
const auto ec = domain.issue_credential_for(ea, parsed->verification_key, name, start, hours);
response = authority.enrolment_response(now(), aes_key, request, 0, &ec, ea);
const std::string ec_id = options.count("--id") ? one(options, "--id") : digest_hex(ec);
store(one(options, "--dir"), ec_id, ec, nullptr);
}
write_file(one(options, "--out"), response);
std::printf("EnrolmentResponse written: %zu octets -> %s\n", response.size(), one(options, "--out").c_str());
return 0;
}
if (command == "aa-respond") {
vidf_test::Credential aa;
aa.certificate = load_certificate(one(options, "--aa"));
aa.key = load_key(one(options, "--aa-key"), backend).priv;
const auto aa_encryption_key = load_key(one(options, "--aa-enc-key"), backend).priv;
vidf_test::Credential ea;
ea.certificate = load_certificate(one(options, "--ea"));
ea.key = load_key(one(options, "--ea-key"), backend).priv;
const auto ea_encryption_key = load_key(one(options, "--ea-enc-key"), backend).priv;
const ByteBuffer request = read_file(one(options, "--request"));
std::array<std::uint8_t, 16> aes_key {};
auto parsed = vidf_test::parse_authorization_request(backend, ecies, aa, aa_encryption_key, request, aes_key);
vidf_test::Authority authority {backend, ecies};
if (!parsed) {
std::printf("aa-respond: request did not decrypt/verify/decode; nothing was issued\n");
return 1;
}
std::optional<Certificate> claimant;
{
const std::string ec_dir = one(options, "--ec-dir");
std::ifstream index(ec_dir + "/index.lst");
std::string hex_id, filename;
while (index >> hex_id >> filename) {
try {
Certificate candidate = load_certificate(ec_dir + "/" + filename);
if (vidf_test::validate_entitlement(backend, ecies, ea, ea_encryption_key, *parsed, candidate)) {
claimant = candidate;
break;
}
} catch (const std::exception&) { continue; }
}
}
ByteBuffer response;
if (options.count("--deny") || !claimant) {
const auto code = options.count("--deny") ? static_cast<std::uint8_t>(std::stoul(one(options, "--deny"))) : std::uint8_t(1);
response = authority.authorization_response(now(), aes_key, request, code, nullptr, aa);
if (!claimant) std::printf("aa-respond: no EC under --ec-dir signed this SharedAtRequest; entitlement not validated\n");
} else {
const unsigned hours = std::stoul(one(options, "--hours", "24"));
const Clock::time_point start = options.count("--start") ? parse_time(one(options, "--start")) : now() - std::chrono::hours(1);
const auto at = domain.issue_ticket_for(aa, parsed->verification_key, parsed->app_permissions, start, hours);
response = authority.authorization_response(now(), aes_key, request, 0, &at, aa);
}
write_file(one(options, "--out"), response);
std::printf("AuthorizationResponse written: %zu octets -> %s\n", response.size(), one(options, "--out").c_str());
return 0;
}
const std::string dir = one(options, "--out");
const std::string id = one(options, "--id");
if (command == "root") {
const Clock::time_point start = options.count("--start") ? parse_time(one(options, "--start")) : default_start(nullptr);
const auto key = load_key(one(options, "--key"), backend);
const unsigned years = std::stoul(one(options, "--years", "5"));
const auto certificate = options.count("--like")
? domain.issue_root_like(key.priv, key.pub, one(options, "--name"), start, years, load_certificate(one(options, "--like")))
: domain.issue_root(key.priv, key.pub, one(options, "--name"), start, years);
store(dir, id, certificate, nullptr); // the root key stays where it is
return 0;
}
if (command == "authority") {
vidf_test::Credential issuer;
issuer.certificate = load_certificate(one(options, "--issuer"));
issuer.key = load_key(one(options, "--issuer-key"), backend).priv;
if (issuer.certificate.issuer_is_self() && !domain.verify_chain_signature(issuer.certificate, issuer.certificate))
throw std::runtime_error("issuer certificate does not verify with itself");
const Clock::time_point start = options.count("--start") ? parse_time(one(options, "--start")) : default_start(&issuer.certificate);
PrivateKey encryption_key;
const auto authority = domain.issue_authority(issuer, one(options, "--name"), start, std::stoul(one(options, "--years", "3")), &encryption_key);
if (!authority.certificate.is_ca_certificate())
throw std::runtime_error("the issuer has no certIssuePermissions group reaching two certificates down; nothing to delegate");
if (!chain_ok(domain, {&authority.certificate, &issuer.certificate})) throw std::runtime_error("refusing to write an inconsistent authority certificate");
store(dir, id, authority.certificate, &authority.key);
write_file(dir + "/" + id + ".ekey", encryption_key.key);
std::printf("%s ECIES encryption key -> %s/%s.ekey (clause 6.2.3: needed to answer real requests)\n", id.c_str(), dir.c_str(), id.c_str());
return 0;
}
if (command == "ticket") {
vidf_test::Credential issuer;
issuer.certificate = load_certificate(one(options, "--issuer"));
issuer.key = load_key(one(options, "--issuer-key"), backend).priv;
const auto permissions = parse_permissions(options,
{{aid::CA, {0x01, 0xff, 0xfc}}, {aid::DEN, {0x01, 0xff, 0xff, 0xff}}, {aid::VRU, {0x01}}, {aid::GN_MGMT, {}}});
const unsigned hours = std::stoul(one(options, "--hours", "24"));
const Clock::time_point start = options.count("--start") ? parse_time(one(options, "--start")) : default_start(&issuer.certificate);
vidf_test::Credential ticket;
if (options.count("--region")) {
long lat, lon, radius;
if (std::sscanf(one(options, "--region").c_str(), "%ld,%ld,%ld", &lat, &lon, &radius) != 3)
throw std::runtime_error("--region LAT,LON,RADIUS_M in 1/10 microdegrees and metres");
ticket = domain.issue_ticket(issuer, permissions, start, hours,
vidf_test::TrustDomain::CircularRegion {static_cast<std::int32_t>(lat), static_cast<std::int32_t>(lon), static_cast<std::uint16_t>(radius)});
} else {
ticket = domain.issue_ticket(issuer, permissions, start, hours, issuer.certificate->toBeSigned.region); // inherits the issuer's region
}
std::vector<Certificate> more;
std::vector<const Certificate*> chain {&ticket.certificate, &issuer.certificate};
if (options.count("--root")) { more.push_back(load_certificate(one(options, "--root"))); chain.push_back(&more.back()); }
if (!chain_ok(domain, chain)) throw std::runtime_error("refusing to write a ticket the issuer cannot authorise (permissions, region or validity)");
store(dir, id, ticket.certificate, &ticket.key);
return 0;
}
std::fprintf(stderr, "unknown command %s\n", command.c_str());
return 2;
} catch (const std::exception& e) {
std::fprintf(stderr, "vidf_issue: %s\n", e.what());
return 1;
}
@@ -0,0 +1,24 @@
// Generated by an independent implementation (pyca/cryptography 50.0.1 on OpenSSL 3.3.0):
// per curve a private scalar d, its public point (x, y), the SHA digest of the message
// "vanetza-idf device backend" and an ECDSA signature (r || s) over that digest.
struct KnownAnswer { vanetza::security::KeyType type; std::size_t n; const unsigned char* d; const unsigned char* x; const unsigned char* y; const unsigned char* digest; const unsigned char* sig; };
const unsigned char kat_NistP256_d[] = {0x8d, 0x70, 0x08, 0x34, 0x4b, 0xab, 0x45, 0x6a, 0x61, 0x1a, 0x3b, 0xc2, 0x3a, 0xf3, 0xe4, 0xe9, 0x14, 0xf1, 0x4a, 0x3b, 0x65, 0x54, 0x66, 0x61, 0xf0, 0x65, 0x41, 0xbc, 0xc3, 0x88, 0x42, 0xba};
const unsigned char kat_NistP256_x[] = {0xce, 0x09, 0x1a, 0xab, 0x0e, 0x3d, 0xf1, 0xee, 0x2b, 0x2e, 0xa7, 0x53, 0x7a, 0x6f, 0xb0, 0xd4, 0x92, 0xe9, 0xf7, 0x59, 0xdc, 0xc9, 0x28, 0x2c, 0xe2, 0x20, 0xe1, 0x19, 0x72, 0x49, 0xc0, 0x46};
const unsigned char kat_NistP256_y[] = {0xa8, 0xd7, 0x2f, 0xac, 0x3b, 0x3e, 0x0c, 0xb2, 0x9e, 0x79, 0x75, 0x7d, 0x64, 0xd7, 0xfd, 0x53, 0xc6, 0x58, 0x06, 0xe3, 0x6d, 0x67, 0x51, 0xf3, 0x4c, 0xf5, 0x24, 0x7d, 0xb5, 0x3e, 0x32, 0xd2};
const unsigned char kat_NistP256_digest[] = {0x7e, 0x0b, 0x16, 0xb3, 0x74, 0x31, 0xd6, 0x0f, 0x0c, 0xd0, 0xac, 0x30, 0x06, 0x49, 0x3f, 0x34, 0x50, 0x05, 0x35, 0x19, 0x6e, 0x1e, 0xb4, 0x83, 0x47, 0xa1, 0xad, 0xc9, 0x50, 0x08, 0xe9, 0x4d};
const unsigned char kat_NistP256_sig[] = {0xff, 0x21, 0xa6, 0xf0, 0x22, 0x2c, 0x24, 0xac, 0xff, 0x23, 0x09, 0xdb, 0x55, 0x50, 0x1b, 0x99, 0xd4, 0x17, 0x05, 0x2a, 0xf1, 0xb8, 0xee, 0x33, 0xbe, 0x9f, 0xdc, 0x35, 0x4d, 0x73, 0x8c, 0x50, 0x38, 0x73, 0x28, 0xe6, 0xef, 0x61, 0x7d, 0x56, 0xaa, 0xde, 0xff, 0xca, 0x6b, 0x34, 0xc7, 0x75, 0x9d, 0x12, 0x9c, 0xca, 0x56, 0x5c, 0x91, 0x88, 0xb2, 0x5b, 0x52, 0x74, 0xb9, 0x54, 0x93, 0xbe};
const unsigned char kat_BrainpoolP256r1_d[] = {0x11, 0xc3, 0x15, 0x86, 0x08, 0x2c, 0x9d, 0x3d, 0x4f, 0xf7, 0xa7, 0xcd, 0x99, 0x09, 0xfa, 0xe8, 0x9a, 0xa3, 0x94, 0xa5, 0x32, 0xbc, 0x08, 0x14, 0x60, 0xb6, 0x9d, 0xa6, 0x4f, 0xf0, 0x79, 0xa1};
const unsigned char kat_BrainpoolP256r1_x[] = {0x21, 0xb0, 0x02, 0x12, 0x5c, 0xe7, 0x7c, 0x87, 0x27, 0xe0, 0xa9, 0x9a, 0x89, 0x5a, 0x00, 0xa1, 0xe3, 0x6e, 0x05, 0xbb, 0x5e, 0xc4, 0x41, 0x58, 0x4a, 0x27, 0x45, 0x7b, 0x94, 0x80, 0x0f, 0x9a};
const unsigned char kat_BrainpoolP256r1_y[] = {0x95, 0x9a, 0x70, 0xe3, 0xe2, 0x7e, 0xba, 0x99, 0x8f, 0xb2, 0x33, 0x4b, 0xc6, 0xe6, 0xd2, 0x9c, 0xec, 0x05, 0x20, 0xf8, 0xe9, 0x5f, 0x35, 0xe6, 0x8f, 0x2d, 0x42, 0x2f, 0xac, 0xeb, 0x3f, 0xbc};
const unsigned char kat_BrainpoolP256r1_digest[] = {0x7e, 0x0b, 0x16, 0xb3, 0x74, 0x31, 0xd6, 0x0f, 0x0c, 0xd0, 0xac, 0x30, 0x06, 0x49, 0x3f, 0x34, 0x50, 0x05, 0x35, 0x19, 0x6e, 0x1e, 0xb4, 0x83, 0x47, 0xa1, 0xad, 0xc9, 0x50, 0x08, 0xe9, 0x4d};
const unsigned char kat_BrainpoolP256r1_sig[] = {0x20, 0x54, 0x18, 0x86, 0x23, 0x90, 0xdb, 0xea, 0x77, 0x05, 0x14, 0x76, 0xc6, 0x16, 0x5f, 0x1c, 0x08, 0x3d, 0x53, 0xb3, 0x14, 0x21, 0xd7, 0xde, 0x83, 0x73, 0xd9, 0x69, 0xd8, 0x2a, 0x02, 0x60, 0x7e, 0x10, 0xdc, 0xb1, 0x1b, 0x53, 0xea, 0x77, 0xd3, 0xef, 0x52, 0x1c, 0xe4, 0xaf, 0xbc, 0x19, 0xf3, 0x76, 0xcf, 0xc3, 0x64, 0xfa, 0xf0, 0x60, 0x29, 0x78, 0xab, 0x4e, 0x7a, 0xc1, 0x69, 0x62};
const unsigned char kat_BrainpoolP384r1_d[] = {0x67, 0xc2, 0xb9, 0x59, 0x8f, 0x5f, 0x22, 0x23, 0xba, 0xb2, 0xd5, 0x70, 0xd5, 0x38, 0xa4, 0xb5, 0x25, 0x54, 0xa1, 0xe9, 0xde, 0x62, 0x00, 0x71, 0x05, 0x4f, 0x18, 0x8b, 0x01, 0x3b, 0xb7, 0x0f, 0x36, 0xd8, 0x26, 0xfc, 0xc0, 0x17, 0xba, 0x3f, 0x32, 0xca, 0xc9, 0x84, 0x77, 0x74, 0x46, 0xd1};
const unsigned char kat_BrainpoolP384r1_x[] = {0x01, 0x83, 0x4a, 0x81, 0xe5, 0x49, 0xc1, 0xbb, 0xe7, 0x63, 0xd8, 0xcd, 0xd5, 0x47, 0xc7, 0xe0, 0xea, 0xf1, 0xe3, 0xa8, 0x03, 0xe8, 0x51, 0x43, 0xc7, 0xbd, 0xd2, 0x80, 0xd6, 0xd0, 0xe4, 0xb9, 0x3d, 0x58, 0xa6, 0xae, 0xfc, 0x30, 0x00, 0x41, 0xcf, 0x48, 0xe6, 0xf0, 0x5a, 0x13, 0x74, 0x02};
const unsigned char kat_BrainpoolP384r1_y[] = {0x2b, 0x93, 0x63, 0x05, 0xf5, 0x52, 0xf0, 0x71, 0xe3, 0x9e, 0x5b, 0x36, 0x85, 0x2a, 0x8b, 0x5b, 0x95, 0x00, 0x77, 0x9e, 0x16, 0x6c, 0x04, 0x90, 0x14, 0x09, 0xc4, 0x3d, 0xfe, 0x1c, 0xd5, 0xb2, 0x4e, 0x80, 0xea, 0x33, 0x24, 0x61, 0x18, 0xad, 0x64, 0x87, 0x62, 0x99, 0x4d, 0xd1, 0xc6, 0xe9};
const unsigned char kat_BrainpoolP384r1_digest[] = {0x62, 0x9d, 0xe5, 0x6f, 0x78, 0xfa, 0x26, 0x81, 0x97, 0xfa, 0x1b, 0xc6, 0xd7, 0xb9, 0xd6, 0x69, 0xd5, 0x82, 0xce, 0xca, 0x6f, 0x31, 0x7f, 0xa2, 0xbd, 0x4a, 0x60, 0x97, 0x6b, 0x14, 0x66, 0xf0, 0xda, 0x88, 0xe0, 0x35, 0x1a, 0x43, 0x2a, 0x4e, 0xd0, 0x77, 0xd0, 0x22, 0xdb, 0x89, 0x1b, 0x83};
const unsigned char kat_BrainpoolP384r1_sig[] = {0x1d, 0x75, 0xaf, 0x9b, 0xf8, 0xc7, 0x2d, 0x24, 0x55, 0x93, 0xca, 0x37, 0xde, 0x5a, 0x8c, 0x95, 0x95, 0x6d, 0xee, 0xad, 0x3b, 0x26, 0x15, 0x9b, 0x4a, 0x99, 0xd5, 0xd8, 0x6c, 0xa2, 0x58, 0x02, 0x9b, 0x97, 0x96, 0x6a, 0x7b, 0x97, 0x91, 0x95, 0x9c, 0xc0, 0x87, 0xdc, 0x99, 0xb0, 0xe4, 0x85, 0x56, 0xbe, 0x27, 0x58, 0xf5, 0x12, 0xe7, 0x9a, 0x2f, 0x30, 0x91, 0xcb, 0xf2, 0xc4, 0xbd, 0xb9, 0x68, 0x8a, 0x03, 0x25, 0x95, 0x98, 0xb9, 0x0c, 0x6b, 0x7a, 0x6f, 0xd4, 0x27, 0xf6, 0x4d, 0xdf, 0x9e, 0x25, 0x8d, 0xbe, 0x50, 0x16, 0xec, 0x04, 0x25, 0x86, 0xf6, 0x45, 0xfb, 0x09, 0x9f, 0xc5};
const KnownAnswer known_answers[] = {
{vanetza::security::KeyType::NistP256, 32, kat_NistP256_d, kat_NistP256_x, kat_NistP256_y, kat_NistP256_digest, kat_NistP256_sig},
{vanetza::security::KeyType::BrainpoolP256r1, 32, kat_BrainpoolP256r1_d, kat_BrainpoolP256r1_x, kat_BrainpoolP256r1_y, kat_BrainpoolP256r1_digest, kat_BrainpoolP256r1_sig},
{vanetza::security::KeyType::BrainpoolP384r1, 48, kat_BrainpoolP384r1_d, kat_BrainpoolP384r1_x, kat_BrainpoolP384r1_y, kat_BrainpoolP384r1_digest, kat_BrainpoolP384r1_sig},
};
@@ -0,0 +1,243 @@
#include "pki_authority.hpp"
#include <vanetza_idf/ecc.hpp>
#include <vanetza/asn1/asn1c_wrapper.hpp>
#include <vanetza/asn1/security/EtsiTs102941Data.h>
#include <vanetza/asn1/security/InnerEcRequest.h>
#include <vanetza/asn1/security/PublicVerificationKey.h>
#include <vanetza/asn1/security/SharedAtRequest.h>
#include <vanetza/security/sha.hpp>
#include <vanetza/security/v3/asn1_conversions.hpp>
#include <vanetza/security/v3/secured_message.hpp>
#include <cstring>
#include <stdexcept>
namespace vidf_test {
using namespace vanetza;
using namespace vanetza::security;
using namespace vanetza_idf;
using vanetza::security::v3::Certificate;
using Mgmt = vanetza::asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data>;
namespace {
// Reads a PublicVerificationKey out of a scratch certificate accessor (there is no
// stand-alone converter): plant the key, then read it back through v3::get_public_key.
// A wire verification key is always compressed (TS 103 097 clause 6 canonical form:
// copy_curve_point() then leaves .y empty, only recording the parity); decompress it
// here so the result is safe to feed into code that expects an ordinary x/y key (e.g.
// building a new certificate around it) -- an empty .y silently reads as an all-zero
// coordinate downstream, embedding the wrong point about half the time.
std::optional<PublicKey> public_key_of(const Vanetza_Security_PublicVerificationKey& key) {
Certificate probe;
probe->toBeSigned.verifyKeyIndicator.present = Vanetza_Security_VerificationKeyIndicator_PR_verificationKey;
const auto bytes = vanetza::asn1::encode_oer(asn_DEF_Vanetza_Security_PublicVerificationKey, &key);
void* target = &probe->toBeSigned.verifyKeyIndicator.choice.verificationKey;
if (!vanetza::asn1::decode_oer(asn_DEF_Vanetza_Security_PublicVerificationKey, &target, bytes)) return std::nullopt;
const auto compact = v3::get_public_key(*probe.content());
if (!compact) return std::nullopt;
if (compact->compression == KeyCompression::NoCompression) return *compact;
const auto point = vanetza_idf::ecc::decompress(compact->type, compact->x, compact->compression == KeyCompression::Y1);
if (!point) return std::nullopt;
PublicKey out = *compact;
out.compression = KeyCompression::NoCompression;
out.y = point->y;
return out;
}
// The payload of a self-signed EtsiTs103097Data-Signed, read WITHOUT verifying the
// signature -- used only to bootstrap the candidate key a self-signed proof of possession
// carries in-band. Nothing extracted this way is trusted until the real verify_signed()
// call below succeeds against that same candidate key: a forged payload cannot both name
// an arbitrary key and carry a signature that verifies with it.
std::optional<ByteBuffer> peek_self_signed_payload(const ByteBuffer& encoded, ItsAid expected_psid) {
v3::SecuredMessage data;
if (!data.decode(encoded) || !data.is_signed() || data.protocol_version() != 3) return std::nullopt;
if (data.its_aid() != expected_psid) return std::nullopt;
const auto* signed_data = data->content->choice.signedData;
if (signed_data->signer.present != Vanetza_Security_SignerIdentifier_PR_self) return std::nullopt;
auto payload = data.payload();
const auto* packet = boost::get<CohesivePacket>(&payload);
if (!packet) return std::nullopt;
const auto view = create_byte_view(*packet, OsiLayer::Network, max_osi_layer());
return ByteBuffer(view.begin(), view.end());
}
} // namespace
ByteBuffer Authority::enrolment_response(Clock::time_point now, const std::array<std::uint8_t, 16>& aes_key,
const ByteBuffer& request, std::uint8_t response_code,
const Certificate* ec, const Credential& signer) const {
Mgmt mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
mgmt->version = Vanetza_Security_Version_v1;
mgmt->content.present = Vanetza_Security_EtsiTs102941DataContent_PR_enrolmentResponse;
auto& inner = mgmt->content.choice.enrolmentResponse;
const auto hash = backend.calculate_hash(HashAlgorithm::SHA256, request);
OCTET_STRING_fromBuf(&inner.requestHash, reinterpret_cast<const char*>(hash.data()), 16);
inner.responseCode = response_code;
if (ec) {
auto* certificate = v3::asn1::allocate<Vanetza_Security_EtsiTs103097Certificate_t>();
inner.certificate = reinterpret_cast<struct Vanetza_Security_EtsiTs103097Certificate*>(certificate);
Certificate copy(*ec);
const auto bytes = copy.encode();
void* target = certificate;
if (!vanetza::asn1::decode_oer(asn_DEF_Vanetza_Security_EtsiTs103097Certificate, &target, bytes))
throw std::runtime_error("EC could not be copied into the enrolment response");
}
auto signed_bytes = pki::sign_data(backend, now, HashAlgorithm::SHA256, mgmt.encode(), signer.key, &signer.certificate);
if (!signed_bytes) throw std::runtime_error("EA cannot sign the enrolment response (missing SCR appPermissions?)");
auto encrypted = pki::encrypt_with_psk(ecies, aes_key, *signed_bytes);
if (!encrypted) throw std::runtime_error("EA cannot encrypt the enrolment response");
return *encrypted;
}
ByteBuffer Authority::authorization_response(Clock::time_point now, const std::array<std::uint8_t, 16>& aes_key,
const ByteBuffer& request, std::uint8_t response_code,
const Certificate* at, const Credential& signer) const {
Mgmt mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
mgmt->version = Vanetza_Security_Version_v1;
mgmt->content.present = Vanetza_Security_EtsiTs102941DataContent_PR_authorizationResponse;
auto& inner = mgmt->content.choice.authorizationResponse;
const auto hash = backend.calculate_hash(HashAlgorithm::SHA256, request);
OCTET_STRING_fromBuf(&inner.requestHash, reinterpret_cast<const char*>(hash.data()), 16);
inner.responseCode = response_code;
if (at) {
auto* certificate = v3::asn1::allocate<Vanetza_Security_EtsiTs103097Certificate_t>();
inner.certificate = reinterpret_cast<struct Vanetza_Security_EtsiTs103097Certificate*>(certificate);
Certificate copy(*at);
const auto bytes = copy.encode();
void* target = certificate;
if (!vanetza::asn1::decode_oer(asn_DEF_Vanetza_Security_EtsiTs103097Certificate, &target, bytes))
throw std::runtime_error("AT could not be copied into the authorization response");
}
auto signed_bytes = pki::sign_data(backend, now, HashAlgorithm::SHA256, mgmt.encode(), signer.key, &signer.certificate);
if (!signed_bytes) throw std::runtime_error("AA cannot sign the authorization response (missing SCR appPermissions?)");
auto encrypted = pki::encrypt_with_psk(ecies, aes_key, *signed_bytes);
if (!encrypted) throw std::runtime_error("AA cannot encrypt the authorization response");
return *encrypted;
}
std::optional<ParsedEnrolmentRequest> parse_enrolment_request(Backend& backend, pki::EciesBackend& ecies, const Credential& ea,
const PrivateKey& ea_encryption_key, const ByteBuffer& encoded,
const PublicKey* canonical_key, const Certificate* current_ec,
std::array<std::uint8_t, 16>& aes_key_out) {
if ((canonical_key == nullptr) == (current_ec == nullptr)) return std::nullopt; // exactly one of the two
auto outer = pki::decrypt_as_recipient(backend, ecies, ea.certificate, ea_encryption_key, encoded, &aes_key_out);
if (!outer) return std::nullopt;
auto mgmt_bytes = pki::verify_signed(backend, *outer, canonical_key, current_ec, aid::SCR);
if (!mgmt_bytes) return std::nullopt;
Mgmt mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
if (!mgmt.decode(*mgmt_bytes) || mgmt->version != Vanetza_Security_Version_v1 ||
mgmt->content.present != Vanetza_Security_EtsiTs102941DataContent_PR_enrolmentRequest) return std::nullopt;
const auto pop_bytes = vanetza::asn1::encode_oer(asn_DEF_Vanetza_Security_EtsiTs103097Data, &mgmt->content.choice.enrolmentRequest);
// bootstrap: the PoP layer is self-signed with the very key it is requesting certified
const auto peeked = peek_self_signed_payload(pop_bytes, aid::SCR);
if (!peeked) return std::nullopt;
vanetza::asn1::asn1c_oer_wrapper<Vanetza_Security_InnerEcRequest> peek_inner(asn_DEF_Vanetza_Security_InnerEcRequest);
if (!peek_inner.decode(*peeked)) return std::nullopt;
const auto candidate_key = public_key_of(peek_inner->publicKeys.verificationKey);
if (!candidate_key) return std::nullopt;
// authoritative check: the signature must actually verify with the candidate key
auto inner_bytes = pki::verify_signed(backend, pop_bytes, &*candidate_key, nullptr, aid::SCR);
if (!inner_bytes) return std::nullopt;
vanetza::asn1::asn1c_oer_wrapper<Vanetza_Security_InnerEcRequest> inner(asn_DEF_Vanetza_Security_InnerEcRequest);
if (!inner.decode(*inner_bytes) || inner->certificateFormat != Vanetza_Security_CertificateFormat_ts103097v131)
return std::nullopt;
ParsedEnrolmentRequest result;
result.its_id.assign(inner->itsId.buf, inner->itsId.buf + inner->itsId.size);
result.verification_key = *candidate_key;
result.re_enrolment = (current_ec != nullptr);
if (const auto* permissions = inner->requestedSubjectAttributes.appPermissions) {
for (int i = 0; i < permissions->list.count; ++i) {
const auto* entry = permissions->list.array[i];
if (!entry) continue;
ByteBuffer ssp;
if (entry->ssp && entry->ssp->present == Vanetza_Security_ServiceSpecificPermissions_PR_bitmapSsp)
ssp.assign(entry->ssp->choice.bitmapSsp.buf, entry->ssp->choice.bitmapSsp.buf + entry->ssp->choice.bitmapSsp.size);
result.app_permissions.emplace_back(static_cast<ItsAid>(entry->psid), std::move(ssp));
}
}
return result;
}
std::optional<ParsedAuthorizationRequest> parse_authorization_request(Backend& backend, pki::EciesBackend& ecies, const Credential& aa,
const PrivateKey& aa_encryption_key, const ByteBuffer& encoded,
std::array<std::uint8_t, 16>& aes_key_out) {
auto pop = pki::decrypt_as_recipient(backend, ecies, aa.certificate, aa_encryption_key, encoded, &aes_key_out);
if (!pop) return std::nullopt;
ByteBuffer mgmt_bytes;
// bootstrap: with PoP, self-signed with the very AT key being requested (as for enrolment)
if (const auto peeked = peek_self_signed_payload(*pop, aid::SCR)) {
Mgmt peek_mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
if (!peek_mgmt.decode(*peeked) || peek_mgmt->content.present != Vanetza_Security_EtsiTs102941DataContent_PR_authorizationRequest)
return std::nullopt;
const auto candidate_key = public_key_of(peek_mgmt->content.choice.authorizationRequest.publicKeys.verificationKey);
if (!candidate_key) return std::nullopt;
auto verified = pki::verify_signed(backend, *pop, &*candidate_key, nullptr, aid::SCR);
if (!verified) return std::nullopt;
mgmt_bytes = *verified;
} else {
// clause 6.2.3.3.1 without proof of possession: an unsecured envelope
v3::SecuredMessage unsecured;
if (!unsecured.decode(*pop) || unsecured.is_signed() || unsecured.is_encrypted()) return std::nullopt;
auto payload = unsecured.payload();
const auto* packet = boost::get<CohesivePacket>(&payload);
if (!packet) return std::nullopt;
const auto view = create_byte_view(*packet, OsiLayer::Network, max_osi_layer());
mgmt_bytes.assign(view.begin(), view.end());
}
Mgmt mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
if (!mgmt.decode(mgmt_bytes) || mgmt->content.present != Vanetza_Security_EtsiTs102941DataContent_PR_authorizationRequest)
return std::nullopt;
auto& iar = mgmt->content.choice.authorizationRequest;
const auto key = public_key_of(iar.publicKeys.verificationKey);
if (!key) return std::nullopt;
ParsedAuthorizationRequest result;
result.verification_key = *key;
if (iar.sharedAtRequest.eaId.size != 8) return std::nullopt;
std::copy(iar.sharedAtRequest.eaId.buf, iar.sharedAtRequest.eaId.buf + 8, result.ea_id.begin());
if (const auto* permissions = iar.sharedAtRequest.requestedSubjectAttributes.appPermissions) {
for (int i = 0; i < permissions->list.count; ++i) {
const auto* entry = permissions->list.array[i];
if (!entry) continue;
ByteBuffer ssp;
if (entry->ssp && entry->ssp->present == Vanetza_Security_ServiceSpecificPermissions_PR_bitmapSsp)
ssp.assign(entry->ssp->choice.bitmapSsp.buf, entry->ssp->choice.bitmapSsp.buf + entry->ssp->choice.bitmapSsp.size);
result.app_permissions.emplace_back(static_cast<ItsAid>(entry->psid), std::move(ssp));
}
}
result.shared_at_request = vanetza::asn1::encode_oer(asn_DEF_Vanetza_Security_SharedAtRequest, &iar.sharedAtRequest);
if (iar.ecSignature.present == Vanetza_Security_EcSignature_PR_encryptedEcSignature) {
result.ec_signature_encrypted = true;
result.ec_signature = vanetza::asn1::encode_oer(asn_DEF_Vanetza_Security_EtsiTs103097Data, &iar.ecSignature.choice.encryptedEcSignature);
} else if (iar.ecSignature.present == Vanetza_Security_EcSignature_PR_ecSignature) {
result.ec_signature_encrypted = false;
result.ec_signature = vanetza::asn1::encode_oer(asn_DEF_Vanetza_Security_EtsiTs103097Data, &iar.ecSignature.choice.ecSignature);
} else {
return std::nullopt;
}
return result;
}
bool validate_entitlement(Backend& backend, pki::EciesBackend& ecies, const Credential& ea, const PrivateKey& ea_encryption_key,
const ParsedAuthorizationRequest& req, const Certificate& candidate_ec) {
ByteBuffer ec_signed_bytes;
if (req.ec_signature_encrypted) {
auto decrypted = pki::decrypt_as_recipient(backend, ecies, ea.certificate, ea_encryption_key, req.ec_signature);
if (!decrypted) return false;
ec_signed_bytes = *decrypted;
} else {
ec_signed_bytes = req.ec_signature;
}
// SignedExternalPayload: the visible payload is empty, the signed hash covers the SharedAtRequest
auto external = pki::verify_signed(backend, ec_signed_bytes, nullptr, &candidate_ec, aid::SCR);
if (!external || !external->empty()) return false;
v3::SecuredMessage ext;
if (!ext.decode(ec_signed_bytes)) return false;
const auto* hashed = ext->content->choice.signedData->tbsData->payload->extDataHash;
if (!hashed || hashed->present != Vanetza_Security_HashedData_PR_sha256HashedData || hashed->choice.sha256HashedData.size != 32)
return false;
const auto shared_hash = calculate_sha256_digest(req.shared_at_request.data(), req.shared_at_request.size());
return std::memcmp(hashed->choice.sha256HashedData.buf, shared_hash.data(), 32) == 0;
}
} // namespace vidf_test
@@ -0,0 +1,99 @@
#pragma once
// TS 102 941 V2.2.1 clause 6.2.3 EA/AA-side tooling: decrypt and verify inbound
// enrolment/authorization requests, and sign+encrypt the responses. This is the
// "authority-side tooling" vanetza_idf::pki's own doc comment (pki.hpp) leaves to the
// application; it is built from the same building blocks the ITS-S side already
// exposes as "shared with tests and authority-side tooling" (decrypt_as_recipient,
// verify_signed, sign_data, encrypt_with_psk) plus the parsing test_pki.cpp already
// proved correct in-process. Used both by the regression tests and by vidf_issue's
// ea-respond/aa-respond commands; a lab/test authority, not a production PKI: no
// replay protection, no butterfly keys (clause 6.2.3.5), no CA-side revocation checks.
#include "test_trust_domain.hpp"
#include <vanetza_idf/pki.hpp>
#include <vanetza/common/clock.hpp>
#include <vanetza/common/its_aid.hpp>
#include <vanetza/security/backend.hpp>
#include <vanetza/security/hashed_id.hpp>
#include <vanetza/security/private_key.hpp>
#include <vanetza/security/public_key.hpp>
#include <vanetza/security/v3/certificate.hpp>
#include <array>
#include <cstdint>
#include <optional>
#include <utility>
#include <vector>
namespace vidf_test {
using vanetza::ByteBuffer;
/// Answers an EnrolmentRequest/AuthorizationRequest (clause 6.2.3.2.2/6.2.3.3.2): signs
/// EtsiTs102941Data{...Response} with `signer` and encrypts it with the request's own
/// AES key (pskRecipInfo), the requestHash set to the leftmost 16 octets of SHA-256 of
/// the (still encrypted) request, as clause 6.2.3.2.2/6.2.3.3.2 require.
struct Authority {
vanetza::security::Backend& backend;
vanetza_idf::pki::EciesBackend& ecies;
ByteBuffer enrolment_response(vanetza::Clock::time_point now, const std::array<std::uint8_t, 16>& aes_key,
const ByteBuffer& request, std::uint8_t response_code,
const vanetza::security::v3::Certificate* ec, const Credential& signer) const;
ByteBuffer authorization_response(vanetza::Clock::time_point now, const std::array<std::uint8_t, 16>& aes_key,
const ByteBuffer& request, std::uint8_t response_code,
const vanetza::security::v3::Certificate* at, const Credential& signer) const;
};
/// Clause 6.2.3.2.1 InnerEcRequest, decrypted and verified from the wire.
struct ParsedEnrolmentRequest {
ByteBuffer its_id; // canonical identifier or HashedId8 of the current EC
vanetza::security::PublicKey verification_key; // the new key the request asks to be certified
vanetza_idf::pki::Permissions app_permissions;
bool re_enrolment = false; // its_id names the current EC (outer signer = its digest)
};
/** EA decrypts `encoded` with its own certificate/encryption key, then verifies the outer
* signature: self-signed with *canonical_key (initial enrolment) or by the digest of
* *current_ec (re-enrolment) — exactly one of the two must be given, matching how
* build_enrolment_request itself lets the caller choose. The inner proof of possession is
* verified against the verification key carried inside the request itself (self-consistent:
* a forged key cannot both appear in the payload and produce a valid signature over it
* without the matching private key). std::nullopt on any decrypt/signature/PoP/decode
* failure -- nothing is accepted on decoding alone. */
std::optional<ParsedEnrolmentRequest> parse_enrolment_request(vanetza::security::Backend&, vanetza_idf::pki::EciesBackend&,
const Credential& ea,
const vanetza::security::PrivateKey& ea_encryption_key,
const ByteBuffer& encoded, const vanetza::security::PublicKey* canonical_key,
const vanetza::security::v3::Certificate* current_ec,
std::array<std::uint8_t, 16>& aes_key_out);
/// Clause 6.2.3.3.1 InnerAtRequest/SharedAtRequest, decrypted and verified from the wire.
struct ParsedAuthorizationRequest {
vanetza::security::PublicKey verification_key; // the new AT key the request asks to be certified
vanetza_idf::pki::Permissions app_permissions;
vanetza::security::HashedId8 ea_id {}; // SharedAtRequest.eaId: which EA to validate entitlement with
ByteBuffer shared_at_request; // OER SharedAtRequest, for validate_entitlement's hash check
bool ec_signature_encrypted = false; // privacy: the EC signature is encrypted for the EA
ByteBuffer ec_signature; // EtsiTs103097Data, encrypted (for the EA) or in clear
};
/** AA decrypts `encoded` with its own certificate/encryption key, verifies the proof of
* possession (self-signed with the requested AT key, bootstrapped from the payload the
* same way as parse_enrolment_request), and decodes InnerAtRequest/SharedAtRequest.
* The "no proof of possession" variant (AuthorizationRequestMessage, unsecured envelope)
* is accepted too, per clause 6.2.3.3.1. std::nullopt on any failure. */
std::optional<ParsedAuthorizationRequest> parse_authorization_request(vanetza::security::Backend&, vanetza_idf::pki::EciesBackend&,
const Credential& aa,
const vanetza::security::PrivateKey& aa_encryption_key,
const ByteBuffer& encoded,
std::array<std::uint8_t, 16>& aes_key_out);
/** The "AA <-> EA: validate entitlement" step: the EA decrypts (if privacy was used) the
* EC signature forwarded inside the authorization request and checks it verifies against
* candidate_ec and hashes to req.shared_at_request (TS 102 941 clause 6.2.3.3.1: the EC
* signs the SharedAtRequest as a SignedExternalPayload). True only when candidate_ec is
* the EC that actually signed this exact request. */
bool validate_entitlement(vanetza::security::Backend&, vanetza_idf::pki::EciesBackend&, const Credential& ea,
const vanetza::security::PrivateKey& ea_encryption_key, const ParsedAuthorizationRequest&,
const vanetza::security::v3::Certificate& candidate_ec);
} // namespace vidf_test
@@ -0,0 +1,10 @@
#pragma once
// Backend used by the security entity tests: OpenSSL on the host (the oracle),
// the PSA backend on a device or when only that one is built.
#if VIDF_BACKEND_OPENSSL
#include <vanetza/security/backend_openssl.hpp>
namespace vidf_test { using TestBackend = vanetza::security::BackendOpenSsl; }
#else
#include <vanetza_idf/backend_mbedtls.hpp>
namespace vidf_test { using TestBackend = vanetza_idf::BackendMbedTls; }
#endif
@@ -0,0 +1,65 @@
// Backend known-answer test: runs on the host against both backends and on a
// device against the PSA backend. The vectors come from pyca/cryptography.
#include "check.hpp"
#include "test_backend.hpp"
#if VIDF_BACKEND_MBEDTLS
#include <vanetza_idf/backend_mbedtls.hpp>
#endif
#include <vanetza_idf/ecc.hpp>
#include <vanetza/security/backend.hpp>
#include <vanetza/security/key_type.hpp>
#include <cstring>
#include <vector>
using namespace vanetza;
using namespace vanetza::security;
using vidf_test::check;
namespace {
#include "known_answers.inc"
void run(Backend& backend) {
static const char text[] = "vanetza-idf device backend";
const ByteBuffer message(text, text + std::strlen(text));
for (const auto& kat : known_answers) {
const std::size_t n = kat.n;
PrivateKey priv {kat.type, ByteBuffer(kat.d, kat.d + n)};
PublicKey pub;
pub.type = kat.type; pub.compression = KeyCompression::NoCompression;
pub.x.assign(kat.x, kat.x + n); pub.y.assign(kat.y, kat.y + n);
const ByteBuffer digest(kat.digest, kat.digest + n);
Signature signature;
signature.type = kat.type;
signature.r.assign(kat.sig, kat.sig + n);
signature.s.assign(kat.sig + n, kat.sig + 2 * n);
check(backend.calculate_hash(n == 48 ? HashAlgorithm::SHA384 : HashAlgorithm::SHA256, message) == digest,
"hash of the message matches the independent implementation");
check(backend.verify_digest(pub, digest, signature), "independent ECDSA signature verifies");
PublicKey compressed = pub;
compressed.compression = (pub.y.back() & 1) ? KeyCompression::Y1 : KeyCompression::Y0;
compressed.y.clear();
check(backend.verify_digest(compressed, digest, signature), "verifies with the compressed public key");
auto point = vanetza_idf::ecc::decompress(kat.type, pub.x, pub.y.back() & 1);
check(point && point->y == pub.y, "decompression reproduces the independent y coordinate");
ByteBuffer wrong = digest; wrong[3] ^= 0x10;
check(!backend.verify_digest(pub, wrong, signature), "tampered digest rejected");
Signature bad = signature; bad.r[0] ^= 0x01;
check(!backend.verify_digest(pub, digest, bad), "tampered signature rejected");
const auto own = backend.sign_digest(priv, digest);
check(own.r.size() == n && own.s.size() == n && backend.verify_digest(pub, digest, own),
"own signature with the independent private key verifies under its public key");
}
const auto pair = backend.generate_key_pair();
const auto legacy = backend.sign_data(pair.private_key, message);
check(backend.verify_data(pair.public_key, message, legacy), "generated key pair round trip");
}
} // namespace
void test_crypto_backend_known_answers() {
vidf_test::TestBackend backend;
run(backend);
#if VIDF_BACKEND_OPENSSL && VIDF_BACKEND_MBEDTLS
vanetza_idf::BackendMbedTls psa;
run(psa);
#endif
}
@@ -0,0 +1,125 @@
// Credentials bundle (credentials.hpp): the octets a provisioning path hands over and a
// storage keeps; applied to the trust configuration and the ticket pool through the
// same checks a station's own provisioning goes through. NVS storage on the device.
#include "check.hpp"
#include "test_backend.hpp"
#include "test_trust_domain.hpp"
#include <vanetza_idf/credentials.hpp>
#if VIDF_NVS_CREDENTIALS
#include <vanetza_idf/nvs_credential_store.hpp>
#endif
#include <chrono>
#include <cstdio>
using namespace vanetza;
using namespace vanetza_idf;
using namespace std::chrono_literals;
using vidf_test::check;
namespace sec = vanetza_idf::security;
namespace {
const Clock::time_point t0 = Clock::time_point(std::chrono::seconds(716292005));
const vidf_test::TrustDomain::Permissions vam_permissions {{aid::VRU, {0x01}}, {aid::GN_MGMT, {}}};
bool same(const sec::Credentials& a, const sec::Credentials& b) {
if (a.roots != b.roots || a.authorities != b.authorities || a.tickets.size() != b.tickets.size()) return false;
for (std::size_t i = 0; i < a.tickets.size(); ++i) {
if (a.tickets[i].certificate != b.tickets[i].certificate || a.tickets[i].key.type != b.tickets[i].key.type ||
a.tickets[i].key.key != b.tickets[i].key.key) return false;
}
return true;
}
} // namespace
void test_credentials() {
vidf_test::section("test_credentials");
vidf_test::TestBackend backend;
vidf_test::TrustDomain domain {backend, t0};
auto ticket = domain.issue_ticket(vam_permissions, t0 - 1h, 24);
auto spare = domain.issue_ticket(vam_permissions, t0 - 1h, 24);
sec::Credentials credentials;
credentials.roots.push_back(domain.root.certificate.encode());
credentials.authorities.push_back(domain.aa.certificate.encode());
credentials.tickets.push_back({ticket.certificate.encode(), ticket.key});
credentials.tickets.push_back({spare.certificate.encode(), spare.key});
// 1. The bundle round-trips and is what it says (magic, records).
const ByteBuffer bundle = sec::encode(credentials);
check(bundle.size() > 4 && bundle[0] == 'V' && bundle[1] == 'C' && bundle[2] == 'R' && bundle[3] == '1', "bundle carries the VCR1 magic");
sec::Credentials decoded;
check(sec::decode(bundle, decoded) && same(decoded, credentials), "encode/decode round trip keeps roots, authorities, tickets and keys");
check(sec::decode(sec::encode(sec::Credentials {}), decoded) && decoded.empty(), "an empty bundle decodes to nothing");
// 2. Malformed bundles fail closed and leave the output untouched.
sec::Credentials untouched = credentials;
auto rejects = [&](ByteBuffer bad, const char* what) {
sec::Credentials out = credentials;
check(!sec::decode(bad, out) && same(out, untouched), what);
};
{ ByteBuffer bad = bundle; bad[3] = '2'; rejects(bad, "wrong magic rejected"); }
{ ByteBuffer bad(bundle.begin(), bundle.end() - 5); rejects(bad, "truncated record rejected"); }
{ ByteBuffer bad = bundle; bad.push_back(9); bad.push_back(0); bad.push_back(1); bad.push_back(0); rejects(bad, "unknown record type rejected"); }
{ ByteBuffer bad = bundle; bad.push_back(1); bad.push_back(0); bad.push_back(0); rejects(bad, "empty record rejected"); }
{ // a key record without its ticket, and a ticket without a key
sec::Credentials one; one.tickets.push_back(credentials.tickets[0]);
ByteBuffer bad = sec::encode(one);
const std::size_t key_at = 4 + 3 + one.tickets[0].certificate.size();
ByteBuffer key_only(bad.begin(), bad.begin() + 4);
key_only.insert(key_only.end(), bad.begin() + key_at, bad.end());
rejects(key_only, "key record without a ticket rejected");
ByteBuffer ticket_only(bad.begin(), bad.begin() + key_at);
rejects(ticket_only, "ticket without its key rejected");
bad[key_at + 3] = 7; // curve code
rejects(bad, "unknown curve code rejected");
bad[key_at + 3] = 3; // brainpoolP384r1 needs 48 octets, 32 given
rejects(bad, "key length not matching the curve rejected");
}
// 3. apply(): everything through the trust configuration and the pool, in order.
{
sec::TrustConfiguration trust;
sec::CertificatePool pool {backend};
const auto report = sec::apply(credentials, trust, pool);
check(report.result == Result::accepted && report.roots == 1 && report.authorities == 1 && report.tickets == 2 &&
pool.size() == 2 && trust.authorities().size() == 2, "apply() feeds roots, authorities and tickets");
}
{ // the first refused item stops the application and is reported
sec::Credentials broken = credentials;
broken.tickets[1].key = ticket.key; // the spare ticket with the wrong key
sec::TrustConfiguration trust;
sec::CertificatePool pool {backend};
const auto report = sec::apply(broken, trust, pool);
check(report.result == Result::invalid_argument && report.roots == 1 && report.authorities == 1 && report.tickets == 1 && pool.size() == 1,
"a ticket whose key does not match stops apply() with the count so far");
}
{
sec::Credentials no_anchor = credentials;
no_anchor.roots[0] = credentials.authorities[0]; // an AA is no root
sec::TrustConfiguration trust;
sec::CertificatePool pool {backend};
check(sec::apply(no_anchor, trust, pool).result == Result::invalid_argument, "a non-root as root is refused");
}
#ifndef ESP_PLATFORM
// 4. File storage (hosts): save, load, erase.
{
sec::FileCredentialStore store {"vidf_test_credentials.bin"};
check(store.save(credentials) == Result::accepted, "file store saves the bundle");
sec::Credentials loaded;
check(store.load(loaded) == Result::accepted && same(loaded, credentials), "file store loads it back");
check(store.erase() == Result::accepted && store.load(loaded) == Result::rejected, "erased store loads nothing");
}
#endif
#if VIDF_NVS_CREDENTIALS
// 4. NVS storage (device): save, load, erase; NVS was initialised by the application.
{
sec::NvsCredentialStore store {"vidf_test", "creds"};
store.erase();
sec::Credentials loaded;
check(store.load(loaded) == Result::rejected, "empty NVS store loads nothing");
check(store.save(credentials) == Result::accepted, "NVS store saves the bundle");
check(store.load(loaded) == Result::accepted && same(loaded, credentials), "NVS store loads it back");
check(store.erase() == Result::accepted && store.load(loaded) == Result::rejected, "erased NVS store loads nothing");
}
#endif
}
@@ -0,0 +1,143 @@
// DCC_ACC integration test: AccessStack's Adaptive DCC gate (TS 102 687 V1.2.1 clause 5.4,
// SYS-DCC-001/002 acceptance criterion 4 boundary vectors). The Adaptive approach's own
// formulas (Limeric) and its Annex B gate-keeper (LimericBudget) are upstream, already
// unit-verified elsewhere; this file verifies the wiring in AccessStack::request() -- the gate
// actually blocks/permits transmissions, the CBR_target override takes effect, and independent
// AccessStack instances never share state.
#include "check.hpp"
#include <vanetza_idf/access.hpp>
#include <vanetza/common/manual_runtime.hpp>
#include <cmath>
#include <chrono>
using namespace vanetza;
using namespace vanetza_idf;
using vidf_test::check;
namespace {
struct Radio : Access {
unsigned accepted = 0;
Result request(AlDataRequest) override { ++accepted; return Result::accepted; }
};
bool near(double a, double b, double eps = 1e-9) { return std::fabs(a - b) < eps; }
AlDataRequest small_request() {
AlDataRequest req;
req.source = {2,0,0,0,0,1}; req.destination = {255,255,255,255,255,255};
req.mcs = OfdmMcs::qpsk_1_2; // 6 Mbit/s: well under the 4 ms Ton limit at this size
req.data.assign(100, 0x2a);
return req;
}
AlDataRequest oversized_request() {
AlDataRequest req;
req.source = {2,0,0,0,0,1}; req.destination = {255,255,255,255,255,255};
req.mcs = OfdmMcs::bpsk_1_2; // 3 Mbit/s, the slowest rate
req.data.assign(3000, 0x2a); // ~8.1 ms of airtime at this rate: over the 4 ms limit
return req;
}
// Drive `cycles` periodic Adaptive-approach updates (200 ms each) feeding a constant CBR,
// and return the permitted duty cycle Limeric settles on. TS 102 687 clause 5.4 runs at a
// fixed 200 ms cadence regardless of how often report_channel_load is called in between;
// feeding the same value on both 100 ms sub-intervals of each cycle matches a station that
// measured a genuinely constant channel load.
double converge(ManualRuntime& runtime, AccessStack& stack, dcc::ChannelLoad load, unsigned cycles) {
for (unsigned i = 0; i < cycles; ++i) {
stack.report_channel_load(load);
runtime.trigger(std::chrono::milliseconds(100));
stack.report_channel_load(load);
runtime.trigger(std::chrono::milliseconds(100));
}
return stack.permitted_duty_cycle().value();
}
} // namespace
void test_dcc() {
vidf_test::section("test_dcc");
// -- delta_max ceiling (also exercises the positive gain saturation that reaches it):
// an always-idle channel (CBR 0.0) against the 0.62 target pushes delta up every cycle
// until it saturates at delta_max = 0.03 (SYS-DCC-001's 3 % duty-cycle ceiling).
{
ManualRuntime runtime;
Radio radio;
AccessStack stack(radio, 4096);
stack.enable_dcc(runtime);
check(near(stack.permitted_duty_cycle().value(), 0.0153),
"Initial duty cycle is the TS 102 687 Table 3 midpoint before the first update");
// Numerically verified to first reach the exact clamp at cycle 158 of 250.
const auto delta = converge(runtime, stack, dcc::ChannelLoad(0.0), 250);
check(near(delta, 0.03), "Idle channel converges to and clamps at delta_max = 0.03");
}
// -- delta_min floor (also exercises the negative gain saturation that reaches it):
// a fully congested channel (CBR 1.0) pushes delta down every cycle until it saturates
// at delta_min = 0.0006 (a station is never fully starved).
{
ManualRuntime runtime;
Radio radio;
AccessStack stack(radio, 4096);
stack.enable_dcc(runtime);
// Numerically verified to first reach the exact clamp at cycle 40 of 250.
const auto delta = converge(runtime, stack, dcc::ChannelLoad(1.0), 250);
check(near(delta, 0.0006), "Fully congested channel converges to and clamps at delta_min = 0.0006");
}
// -- CBR exactly at the 0.62 target: clause 5.4 step 2's positive branch requires the
// gap to be strictly positive (sign(0) is not positive), so an exact match gives a zero
// offset every cycle and delta decays toward delta_min, never overshooting it.
{
ManualRuntime runtime;
Radio radio;
AccessStack stack(radio, 4096);
stack.enable_dcc(runtime);
// Slowest of the three cases (pure (1-alpha) decay, no offset): numerically verified
// to first reach the exact clamp at cycle 201 of 250.
const auto delta = converge(runtime, stack, dcc::ChannelLoad(0.62), 250);
check(near(delta, 0.0006), "CBR exactly at target decays to delta_min, not held at the midpoint");
}
// -- per-channel isolation: two independently constructed AccessStack instances (as
// Station::build() creates fresh per rebuild) never share Limeric/LimericBudget state.
{
ManualRuntime runtime_a, runtime_b;
Radio radio_a, radio_b;
AccessStack stack_a(radio_a, 4096), stack_b(radio_b, 4096);
stack_a.enable_dcc(runtime_a);
stack_b.enable_dcc(runtime_b);
converge(runtime_a, stack_a, dcc::ChannelLoad(0.0), 250); // pushes stack_a to delta_max
check(near(stack_b.permitted_duty_cycle().value(), 0.0153),
"An unrelated AccessStack's duty cycle is unaffected by another instance's updates");
}
// -- EN 303 797 clause 4.6.2 Ton limit: a frame whose computed airtime exceeds 4 ms is
// refused outright, never queued or transmitted.
{
ManualRuntime runtime;
Radio radio;
AccessStack stack(radio, 4096);
stack.enable_dcc(runtime);
check(stack.request(oversized_request()) == Result::invalid_argument,
"A frame whose airtime exceeds 4 ms is rejected before reaching the backend");
check(radio.accepted == 0, "The oversized frame never reached the backend");
}
// -- gate enforcement: with the duty cycle at its widest (delta_max, from a preceding
// idle-channel run), a burst of back-to-back small frames must still be spaced out by
// LimericBudget's Annex B gate-keeper (min_interval = 25 ms), not sent unthrottled.
{
ManualRuntime runtime;
Radio radio;
AccessStack stack(radio, 4096);
stack.enable_dcc(runtime);
converge(runtime, stack, dcc::ChannelLoad(0.0), 250); // delta_max, most permissive case
check(stack.request(small_request()) == Result::accepted, "First frame after the gate opens is accepted");
check(stack.request(small_request()) == Result::resource_limit,
"An immediate second frame is gated (Toff >= 25 ms) instead of being sent back to back");
runtime.trigger(std::chrono::milliseconds(30));
check(stack.request(small_request()) == Result::accepted,
"The same frame is accepted again once the gate-keeper's interval has elapsed");
}
}
@@ -0,0 +1,126 @@
// Release-2 DCC_NET wiring (TS 103 836-4-2 V2.1.1 clauses 5, 6.3.3; SYS-DCC-003). The
// underlying algorithms (CbrAggregator's five-step CBR_G, DccMcoField's Table 3 bit layout,
// DccInformationSharing's periodic trigger) are upstream and already correct (verified against
// the standard text directly, not just the corrupted .txt cache -- see the thesis workbench
// notes); this file verifies that vanetza_idf::Stack actually wires them in: outgoing SHB
// packets carry a real DCC-MCO field instead of the default NullDccFieldGenerator's zero, and
// Stack::global_channel_busy_ratio() reflects DccInformationSharing's own state.
#include "check.hpp"
#include <vanetza_idf/stack.hpp>
#include <vanetza_idf/mn_sap.hpp>
#include <vanetza/geonet/basic_header.hpp>
#include <vanetza/geonet/common_header.hpp>
#include <vanetza/geonet/serialization_buffer.hpp>
#include <vanetza/geonet/shb_header.hpp>
#include <vanetza/units/angle.hpp>
#include <vanetza/units/velocity.hpp>
#include <boost/iostreams/stream_buffer.hpp>
#include <chrono>
#include <cmath>
#include <memory>
using namespace vanetza;
using namespace vanetza_idf;
using vidf_test::check;
namespace {
bool near(double a, double b, double eps = 1e-9) { return std::fabs(a - b) < eps; }
struct Radio : Access {
std::vector<AlDataRequest> packets;
Result request(AlDataRequest packet) override { packets.push_back(std::move(packet)); return Result::accepted; }
};
// The GNPDU capture starts at the Basic Header; ShbHeader's own serialization begins right
// after Basic + Common (TS 103 836-4-1 V2.2.1 clauses 9.6-9.7), regardless of ShbHeader's
// internal byte layout -- so this needs no knowledge of DCC-MCO's exact offset within it.
geonet::ShbHeader deserialize_shb(const ByteBuffer& gnpdu) {
using namespace geonet;
const auto skip = BasicHeader::length_bytes + CommonHeader::length_bytes;
check(gnpdu.size() > skip, "Captured GNPDU is at least Basic + Common + SHB headers long");
byte_buffer_source source(gnpdu.begin() + skip, gnpdu.end());
boost::iostreams::stream_buffer<byte_buffer_source> stream(source);
InputArchive ar(stream);
ShbHeader shb;
deserialize(shb, ar);
return shb;
}
// Stack has no move constructor (a deleted copy constructor plus a user-declared destructor
// suppress it), so a helper builds it on the heap rather than returning it by value.
std::unique_ptr<Stack> build_ready_stack(ManualRuntime& runtime, Access& access) {
StackConfig cfg;
cfg.mib.itsGnSecurity = false;
cfg.mib.vanetzaDisableBeaconing = true;
cfg.mib.itsGnLocalAddrConfMethod = geonet::AddrConfMethod::Managed;
cfg.mib.itsGnLocalGnAddr.mid({2, 0, 0, 0, 0, 1});
auto stack = std::make_unique<Stack>(cfg, runtime, access);
// Must be checked here: applying time/position below jumps the runtime far forward in
// one step, which fires DccInformationSharing's self-rescheduling trigger many times
// over on the way -- checking after that point would no longer observe the "before the
// first trigger" state at all.
check(!stack->global_channel_busy_ratio().has_value(),
"CBR_G is unavailable before DccInformationSharing's first 100 ms trigger");
MN_SAP::CORE_MMT_response ready;
ready.time = Clock::time_point(std::chrono::seconds(716292005));
PositionFix fix;
fix.timestamp = *ready.time;
fix.latitude = 48.1 * units::degree; fix.longitude = 11.6 * units::degree;
fix.speed = 1.5 * units::si::meters_per_second; fix.course = 90.0 * units::true_north_degrees;
ready.local_position_vector = fix;
check(MN_SAP::CORE_MMT_response_apply(*stack, ready) == Result::accepted, "test station has time and position");
return stack;
}
BtpRequest shb_request() {
BtpRequest req;
req.destination_port = 2018;
req.destination_port_info = 0;
req.data = {1, 2, 3};
return req;
}
} // namespace
void test_dcc_net() {
vidf_test::section("test_dcc_net");
// -- DCC-MCO round trip through a real ShbHeader (TS 103 836-4-2 Table 3 quantisation:
// floor(cbr * 255), so 0.2 and 0.4 are chosen as exact multiples of 1/255).
{
geonet::ShbHeader shb1;
geonet::DccMcoField mco;
mco.local_cbr(dcc::ChannelLoad(0.2));
mco.neighbour_cbr(dcc::ChannelLoad(0.4));
mco.output_power(17);
shb1.dcc = mco;
ByteBuffer buffer;
geonet::serialize_into_buffer(shb1, buffer);
check(buffer.size() == geonet::ShbHeader::length_bytes, "ShbHeader serializes to its declared length");
geonet::ShbHeader shb2;
geonet::deserialize_from_buffer(shb2, buffer);
auto mco2 = geonet::get_dcc_mco(shb2.dcc);
check(static_cast<bool>(mco2), "DCC-MCO variant survives the round trip, not the legacy uint32_t reserved field");
check(near(mco2->local_cbr().value(), 0.2) && near(mco2->neighbour_cbr().value(), 0.4) && mco2->output_power() == 17,
"DCC-MCO field content survives serialize/deserialize exactly");
}
// -- Stack wiring: global CBR_G is unavailable before any DCC_NET aggregation cycle,
// and outgoing SHB packets carry the fed local CBR/TX power, not a null/zero field.
{
ManualRuntime runtime;
Radio radio;
auto stack = build_ready_stack(runtime, radio);
stack->report_local_channel_load(dcc::ChannelLoad(51.0 / 255.0)); // exact 1/255 multiple
stack->report_tx_power(10);
// report_local_channel_load only feeds DccInformationSharing's next scheduled 100 ms
// trigger (clause 5.3's own cadence, independent of when this is called); it does not
// synchronously update the aggregator generate_dcc_field() reads from.
runtime.trigger(std::chrono::milliseconds(100));
check(stack->request(shb_request()) == Result::accepted, "SHB transmission with DCC_NET wired in still succeeds");
const auto shb = deserialize_shb(radio.packets.back().data);
const auto mco = geonet::get_dcc_mco(shb.dcc);
check(static_cast<bool>(mco), "Outgoing SHB carries a real DCC-MCO field, not NullDccFieldGenerator's reserved zero");
check(near(mco->local_cbr().value(), 51.0 / 255.0) && mco->output_power() == 10,
"Outgoing DCC-MCO reflects the fed local CBR and TX power");
}
}
@@ -0,0 +1,64 @@
#include "check.hpp"
#include <vanetza_idf/its_time.hpp>
#include <vanetza/common/clock.hpp>
#include <chrono>
using vidf_test::check;
using namespace vanetza_idf::its_time;
namespace {
// Unix seconds of a UTC calendar instant (days since 1970-01-01, proleptic Gregorian).
constexpr std::int64_t unix_utc(int y, int m, int d, int hh = 0, int mm = 0, int ss = 0) {
// Howard Hinnant's days_from_civil
y -= m <= 2;
const std::int64_t era = (y >= 0 ? y : y - 399) / 400;
const std::int64_t yoe = y - era * 400;
const std::int64_t doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
const std::int64_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
const std::int64_t days = era * 146097 + doe - 719468;
return days * 86400 + hh * 3600 + mm * 60 + ss;
}
std::chrono::system_clock::time_point at(std::int64_t unix_seconds, std::int64_t micro = 0) {
return std::chrono::system_clock::time_point(std::chrono::seconds(unix_seconds) + std::chrono::microseconds(micro));
}
}
void test_its_time() {
vidf_test::section("test_its_time");
static_assert(unix_utc(2004, 1, 1) == epoch_unix_seconds, "ITS epoch");
static_assert(unix_utc(2006, 1, 1) == leap_second_unix_seconds[0], "leap 2005-12-31");
static_assert(unix_utc(2009, 1, 1) == leap_second_unix_seconds[1], "leap 2008-12-31");
static_assert(unix_utc(2012, 7, 1) == leap_second_unix_seconds[2], "leap 2012-06-30");
static_assert(unix_utc(2015, 7, 1) == leap_second_unix_seconds[3], "leap 2015-06-30");
static_assert(unix_utc(2017, 1, 1) == leap_second_unix_seconds[4], "leap 2016-12-31");
check(microseconds_since_epoch(epoch_unix_seconds) == 0, "the ITS epoch is zero");
// TS 102 894-2 V2.4.1 DE_TimestampIts example: 2007-01-01T00:00:00.000Z is 94 694 401 000 ms
// (one leap second inserted since the epoch).
check(timestamp_its(at(unix_utc(2007, 1, 1))) == 94694401000ULL, "CDD example: 2007-01-01 = 94 694 401 000 ms");
// TS 102 894-2: "As of 1 January, 2022, TimestampIts is 5 seconds ahead of UTC".
check(timestamp_its(at(unix_utc(2022, 1, 1))) == (unix_utc(2022, 1, 1) - epoch_unix_seconds + 5) * 1000ULL,
"CDD statement: 5 s ahead of UTC since 2017");
check(time32(at(unix_utc(2022, 1, 1))) == unix_utc(2022, 1, 1) - epoch_unix_seconds + 5, "Time32 uses the same scale in seconds");
// Around the last insertion: UTC 2016-12-31T23:59:59 and 2017-01-01T00:00:00 are two TAI seconds apart.
const auto before = since_epoch(at(unix_utc(2016, 12, 31, 23, 59, 59)));
const auto after = since_epoch(at(unix_utc(2017, 1, 1)));
check(after - before == std::chrono::seconds(2), "the inserted second (23:59:60) lies between the two UTC seconds");
// Sub-second parts pass through.
check(since_epoch(at(unix_utc(2022, 1, 1), 123456)).count() % 1000000 == 123456, "microseconds are kept");
// Round trips, including instants on either side of every leap second.
for (auto leap : leap_second_unix_seconds) {
for (std::int64_t delta : {-2, -1, 0, 1, 2}) {
const auto expected = (leap + delta) * 1000000 + 500000;
check(unix_microseconds(microseconds_since_epoch(leap + delta, 500000)) == expected, "Unix round trip around a leap second");
}
}
// The inserted second itself maps onto the following UTC second.
const auto inserted = microseconds_since_epoch(leap_second_unix_seconds[4]) - 1000000;
check(unix_microseconds(inserted) == leap_second_unix_seconds[4] * 1000000, "23:59:60 maps onto 00:00:00");
// vanetza::Clock::time_point carries these microseconds directly; upstream Clock::at() lacks
// the leap seconds (documented in its_time.hpp), so the helper is the conversion to use.
const vanetza::Clock::time_point clock {since_epoch(at(unix_utc(2022, 1, 1)))};
const auto posix = vanetza::Clock::at("2022-01-01 00:00:00");
check(clock - posix == std::chrono::seconds(5), "upstream Clock::at() is 5 s behind TAI since 2017");
}
@@ -0,0 +1,305 @@
#include <vanetza_idf/access.hpp>
#include <vanetza_idf/its_g5_frame.hpp>
#if VIDF_NETWORK
#include <vanetza_idf/stack.hpp>
#include <vanetza_idf/nf_sap.hpp>
#include <vanetza/security/sha.hpp>
#endif
#if VIDF_CAM || VIDF_DENM || VIDF_VAM
#include <vanetza_idf/facilities.hpp>
#endif
#if VIDF_HIL
#include <vanetza_idf/hil.hpp>
#endif
#include "check.hpp"
#include <cstdio>
#include <stdexcept>
#include <vector>
#include <limits>
void test_its_time();
#if VIDF_NETWORK
void test_management();
void test_dcc();
void test_dcc_net();
#endif
#if VIDF_SECURITY
void test_crypto_backend_known_answers();
#if VIDF_BACKEND_OPENSSL
void test_crypto_backends(); // host only: OpenSSL is the oracle
#endif
void test_security_entity();
void test_credentials();
#if VIDF_PKI
void test_pki();
#endif
#endif
using namespace vanetza_idf;
using vidf_test::check;
using vidf_test::checks;
namespace {
struct Capture : Access {
std::vector<AlDataRequest> packets;
Result next = Result::accepted;
Result request(AlDataRequest packet) override { packets.push_back(std::move(packet)); return next; }
};
void test_access() {
Capture radio;
AccessStack stack(radio, 32);
AlDataRequest req;
req.data = {1,2,3}; req.source = {2,0,0,0,0,1}; req.destination = {2,0,0,0,0,2};
req.priority = 7; req.mcs = OfdmMcs::qam16_1_2; req.transmit_power_dbm = 12.5;
req.channel_number = 176; req.bandwidth_mhz = 10; req.transceiver_id = 3;
req.transceiver_mode = 0; req.datastream_id = 42;
check(stack.request(req) == Result::accepted, "IN request accepted");
auto& out = radio.packets.back();
check(out.data == req.data && out.source == req.source && out.destination == req.destination,
"IN addresses and payload preserved");
check(out.priority == 7 && out.mcs == OfdmMcs::qam16_1_2 && out.transmit_power_dbm == 12.5 &&
out.channel_number == 176 && out.transceiver_id == 3 && out.datastream_id == 42,
"IN radio controls preserved");
req.priority = 8;
check(stack.request(req) == Result::invalid_argument, "Invalid priority rejected");
req.priority = 1; req.data.resize(33);
check(stack.request(req) == Result::invalid_argument, "Access MTU enforced");
req.data.clear();
check(stack.request(req) == Result::invalid_argument, "Empty access packet rejected");
req.data = {1}; radio.next = Result::unsupported;
check(stack.request(req) == Result::unsupported, "Backend failure preserved");
}
void test_its_g5_frame() {
AlDataRequest request;
request.source = {2,0,0,0,0,1}; request.destination = {255,255,255,255,255,255};
request.priority = 6; request.data = {0x11, 0, 0, 1, 0x42};
vanetza::ByteBuffer wire;
check(its_g5::encode_frame(request, 0x123, wire) == Result::accepted, "QoS MPDU built");
check(wire.size() == 39 && wire[0] == 0x88 && wire[24] == 6 &&
wire[22] == 0x30 && wire[23] == 0x12 && wire[32] == 0x89 && wire[33] == 0x47,
"QoS TID, sequence and LLC EtherType");
AlDataIndication indication;
check(its_g5::decode_frame(wire.data(), wire.size(), false, indication) == Result::accepted &&
indication.data == request.data && indication.source == request.source &&
indication.destination == request.destination, "MPDU round trip preserves AL_DATA");
for (std::size_t length = 0; length < 34; ++length)
check(its_g5::decode_frame(wire.data(), length, false, indication) == Result::invalid_argument,
"Truncated MPDU rejected before header access");
auto malformed = wire; malformed[1] = 1;
check(its_g5::decode_frame(malformed.data(), malformed.size(), false, indication) == Result::unsupported,
"To-DS frame rejected by OCB binding");
malformed = wire; malformed[24] |= 0x80;
check(its_g5::decode_frame(malformed.data(), malformed.size(), false, indication) == Result::unsupported,
"A-MSDU cannot be interpreted as plain LLC");
malformed = wire; malformed[33] = 0;
check(its_g5::decode_frame(malformed.data(), malformed.size(), false, indication) == Result::unsupported,
"Non-GeoNetworking EtherType rejected");
wire.insert(wire.end(), 4, 0);
check(its_g5::decode_frame(wire.data(), wire.size(), true, indication) == Result::accepted &&
indication.data == request.data, "Explicit FCS removal");
request.data.resize(its_g5::maximum_gnpdu + 1);
check(its_g5::encode_frame(request, 0, wire) == Result::invalid_argument, "MAC MSDU limit enforced");
}
#if VIDF_NETWORK
void test_digests() {
// FIPS 180-4 algorithms, standard "abc" known-answer vectors.
const std::uint8_t input[] = {'a', 'b', 'c'};
using vidf_test::hex;
check(hex(vanetza::security::calculate_sha256_digest(input, sizeof(input))) ==
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
"SHA-256 known answer");
check(hex(vanetza::security::calculate_sha384_digest(input, sizeof(input))) ==
"cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7",
"SHA-384 known answer");
}
void test_network() {
using namespace vanetza;
ManualRuntime rt;
Capture radio;
StackConfig cfg;
cfg.mib.itsGnSecurity = false; // explicitly unsecured laboratory profile
cfg.mib.vanetzaDisableBeaconing = true;
cfg.mib.itsGnLocalGnAddr.mid({2,0,0,0,0,1});
Stack stack(cfg, rt, radio);
PositionFix fix {};
fix.latitude = 52.0 * units::degree; fix.longitude = 13.0 * units::degree;
fix.speed = 0.0 * units::si::meters_per_second;
fix.course = 0.0 * units::true_north_degrees;
check(stack.update_position(fix) == Result::accepted, "Position accepted");
BtpRequest req;
req.destination_port = 2009; req.destination_port_info = 0x1234;
req.data = {0x12,0x34,0x56};
check(stack.request(req) == Result::accepted, "BTP-B SHB sent through router");
check(radio.packets.size() == 1, "One SHB packet emitted");
const auto wire = radio.packets.back();
check(wire.data.size() == req.data.size() + 44, "GN SHB plus BTP header length");
check(wire.data[40] == 0x07 && wire.data[41] == 0xd9 && wire.data[42] == 0x12 && wire.data[43] == 0x34,
"BTP-B port and info use network byte order");
// Different GN address avoids receiving own packet in the router.
StackConfig receiver_cfg = cfg; receiver_cfg.mib.itsGnLocalGnAddr.mid({2,0,0,0,0,2});
Capture receiver_radio; ManualRuntime receiver_rt;
Stack receiver(receiver_cfg, receiver_rt, receiver_radio);
receiver.update_position(fix);
unsigned received = 0;
receiver.on_receive([&](BtpIndication ind) {
++received;
check(ind.destination_port == 2009 && ind.destination_port_info == 0x1234 &&
!ind.source_port && ind.data == req.data, "BTP-B receive semantics");
});
AlDataIndication incoming;
incoming.source = wire.source; incoming.destination = wire.destination; incoming.data = wire.data;
check(receiver.indicate(incoming) == Result::accepted && received == 1, "SHB traverses both stacks");
req.type = BtpType::a; req.source_port = 0xbeef; req.destination_port_info.reset();
check(stack.request(req) == Result::accepted, "BTP-A SHB accepted");
check(radio.packets.back().data[42] == 0xbe && radio.packets.back().data[43] == 0xef, "BTP-A source port preserved");
req.source_port.reset();
check(stack.request(req) == Result::invalid_argument, "BTP-A requires source port");
req.type = BtpType::b; req.transport = geonet::TransportType::GUC;
check(stack.request(req) == Result::unsupported, "Unsupported transport cannot silently become SHB");
req.transport = geonet::TransportType::SHB; req.maximum_hop_limit = 0;
check(stack.request(req) == Result::invalid_argument, "Hop limit boundary");
req.maximum_hop_limit.reset(); req.data.resize(cfg.mib.itsGnMaxSduSize);
check(stack.request(req) == Result::resource_limit, "BTP MTU includes header");
check(stack.advance(Clock::time_point(Clock::duration(-1))) == Result::time_regression, "Time cannot move backwards");
NF_SAP::BTP_DATA_request primitive;
primitive.fl_sdu = {0x12, 0x34};
primitive.destination_port = 2018;
check(NF_SAP::BTP_DATA_request_submit(stack, primitive) == Result::invalid_argument,
"NF-SAP length mismatch rejected");
primitive.length = primitive.fl_sdu.size();
primitive.gn_security_profile = NF_SAP::SecurityProfile::SECURED;
check(NF_SAP::BTP_DATA_request_submit(stack, primitive) == Result::unsupported,
"NF-SAP cannot silently downgrade secured request");
primitive.gn_security_profile = NF_SAP::SecurityProfile::UNSECURED;
check(NF_SAP::BTP_DATA_request_submit(stack, primitive) == Result::accepted,
"Named NF-SAP request enters real router");
check(radio.packets.back().data[40] == 0x07 && radio.packets.back().data[41] == 0xe2,
"Named NF-SAP destination port preserved on wire");
cfg.mib.itsGnSecurity = true; Capture secured_radio; Stack secured(cfg, rt, secured_radio);
secured.update_position(fix); req.data = {1};
check(secured.request(req) == Result::security_unavailable && secured_radio.packets.empty(), "Missing security fails closed");
}
#endif
#if VIDF_HIL
void test_hil() {
using namespace hil;
Frame f {Channel::upper, 0x12345678, {0,1,2,3}};
auto wire = encode(f, 32);
Decoder decoder(32);
unsigned seen = 0;
auto receive = [&](Frame got) {
++seen;
check(got.sequence == f.sequence && got.payload == f.payload && got.channel == f.channel, "HIL frame integrity");
};
for (auto byte : wire) decoder.feed(&byte, 1, receive);
check(seen == 1, "Bytewise HIL fragmentation");
auto corrupt = wire; corrupt.back() ^= 1;
decoder.feed(corrupt.data(), corrupt.size(), receive);
check(seen == 1, "CRC failure never reaches tester");
decoder.feed(wire.data(), wire.size(), receive);
check(seen == 2, "Decoder resynchronizes");
std::vector<std::uint8_t> junk(10000, 0x44);
decoder.feed(junk.data(), junk.size(), receive);
check(decoder.buffered() <= 3, "Decoder bounds garbage memory");
}
#endif
#if VIDF_CAM || VIDF_DENM || VIDF_VAM
void test_codecs() {
using namespace facilities;
for (auto k : {Kind::cam, Kind::denm, Kind::vam}) {
check(validate_pdu(k, {}) == Result::invalid_argument, "Empty UPER rejected");
check(validate_pdu(k, {0xff}) != Result::accepted, "Truncated UPER rejected");
}
#if VIDF_CAM
Cam cam;
cam->header.protocolVersion = 2; cam->header.messageId = 2; cam->header.stationId = 42;
auto& cp = cam->cam.camParameters;
cp.basicContainer.stationType = 15;
cp.highFrequencyContainer.present = Vanetza_ITS2_HighFrequencyContainer_PR_rsuContainerHighFrequency;
auto& cam_pos = cp.basicContainer.referencePosition;
cam_pos.latitude = 900000001; cam_pos.longitude = 1800000001;
cam_pos.positionConfidenceEllipse.semiMajorAxisLength = 4095;
cam_pos.positionConfidenceEllipse.semiMinorAxisLength = 4095;
cam_pos.positionConfidenceEllipse.semiMajorAxisOrientation = 3601;
cam_pos.altitude.altitudeValue = 800001; cam_pos.altitude.altitudeConfidence = 15;
check(cam.validate(), "CAM constraints");
check(validate_pdu(Kind::cam, cam.encode()) == Result::accepted, "CAM round trip");
cam->header.protocolVersion = 1;
check(validate_pdu(Kind::cam, cam.encode()) == Result::invalid_argument, "CAM protocol version enforced");
#endif
#if VIDF_DENM
Denm denm;
denm->header.protocolVersion = 2; denm->header.messageId = 1; denm->header.stationId = 42;
auto& dm = denm->denm.management;
dm.actionId.originatingStationId = 42;
check(asn_long2INTEGER(&dm.detectionTime, 0) == 0 && asn_long2INTEGER(&dm.referenceTime, 0) == 0,
"DENM timestamp allocation");
dm.eventPosition.latitude = 900000001; dm.eventPosition.longitude = 1800000001;
dm.eventPosition.positionConfidenceEllipse.semiMajorConfidence = 4095;
dm.eventPosition.positionConfidenceEllipse.semiMinorConfidence = 4095;
dm.eventPosition.positionConfidenceEllipse.semiMajorOrientation = 3601;
dm.eventPosition.altitude.altitudeValue = 800001; dm.eventPosition.altitude.altitudeConfidence = 15;
check(denm.validate(), "DENM constraints");
check(validate_pdu(Kind::denm, denm.encode()) == Result::accepted, "DENM round trip");
#endif
#if VIDF_VAM
Vam vam;
check(descriptor(Kind::vam).port == 2018, "TS 103 248 Table 1 VAM destination port");
vam->header.protocolVersion = 3; vam->header.messageId = 16; vam->header.stationId = 42;
auto& hf = vam->vam.vamParameters.vruHighFrequencyContainer;
hf.heading.value = 3601; hf.heading.confidence = 127;
hf.speed.speedValue = 16383; hf.speed.speedConfidence = 127;
hf.longitudinalAcceleration.longitudinalAccelerationValue = 161;
hf.longitudinalAcceleration.longitudinalAccelerationConfidence = 102;
auto& pos = vam->vam.vamParameters.basicContainer.referencePosition;
pos.latitude = 900000001; pos.longitude = 1800000001;
pos.positionConfidenceEllipse.semiMajorAxisLength = 4095;
pos.positionConfidenceEllipse.semiMinorAxisLength = 4095;
pos.positionConfidenceEllipse.semiMajorAxisOrientation = 3601;
pos.altitude.altitudeValue = 800001; pos.altitude.altitudeConfidence = 15;
check(vam.validate(), "VAM constraints");
auto bytes = vam.encode();
check(validate_pdu(Kind::vam, bytes) == Result::accepted, "VAM round trip");
bytes.push_back(0);
check(validate_pdu(Kind::vam, bytes) == Result::invalid_argument, "Trailing UPER bytes rejected");
#endif
}
#endif
}
int main() {
try {
test_access();
test_its_g5_frame();
#if VIDF_NETWORK
test_digests();
test_network();
#endif
#if VIDF_HIL
test_hil();
#endif
#if VIDF_CAM || VIDF_DENM || VIDF_VAM
test_codecs();
#endif
#if VIDF_NETWORK
test_management();
test_dcc();
test_dcc_net();
#endif
test_its_time();
#if VIDF_SECURITY
test_crypto_backend_known_answers();
#if VIDF_BACKEND_OPENSSL
test_crypto_backends();
#endif
test_security_entity();
test_credentials();
#if VIDF_PKI
test_pki();
#endif
#endif
std::printf("PASS: %u checks (host/component tests, not ETSI ATS verdicts)\n", checks);
return 0;
} catch (const std::exception& e) {
std::fprintf(stderr, "FAIL: %s (in %s)\n", e.what(), vidf_test::section_name);
return 1;
}
}
@@ -0,0 +1,158 @@
// Management-plane bindings: MN-SAP CORE_MMT feed and DCC parameter GET/SET
// over MN/MF/MI with an application-supplied provider, nothing invented.
#include "check.hpp"
#include <vanetza_idf/mf_sap.hpp>
#include <vanetza_idf/mi_sap.hpp>
#include <vanetza_idf/mn_sap.hpp>
#include <vanetza_idf/stack.hpp>
#include <map>
#include <vector>
using namespace vanetza;
using namespace vanetza_idf;
using vidf_test::check;
namespace {
struct Radio : Access {
std::vector<AlDataRequest> packets;
Result request(AlDataRequest packet) override { packets.push_back(std::move(packet)); return Result::accepted; }
};
// Access/DCC adapter stand-in that only knows the channel number and TX power limit.
struct DccAdapter : MN_SAP::NetworkParameterProvider, MI_SAP::AccessParameterProvider {
std::map<int, std::uint32_t> values {{1, 5}, {6, 23}, {52, 5}, {57, 23}};
MN_SAP::ErrStatus get(MN_SAP::N_Param_No no, std::uint32_t& value) override {
auto it = values.find(static_cast<int>(no));
if (it == values.end()) return MN_SAP::ErrStatus::UNSUPPORTED;
value = it->second;
return MN_SAP::ErrStatus::SUCCESS;
}
MN_SAP::ErrStatus set(MN_SAP::N_Param_No no, std::uint32_t value) override {
if (!values.count(static_cast<int>(no))) return MN_SAP::ErrStatus::UNSUPPORTED;
values[static_cast<int>(no)] = value;
return MN_SAP::ErrStatus::SUCCESS;
}
MN_SAP::ErrStatus get(MI_SAP::I_Param_No no, std::uint32_t& value) override {
auto it = values.find(static_cast<int>(no));
if (it == values.end()) return MN_SAP::ErrStatus::UNSUPPORTED;
value = it->second;
return MN_SAP::ErrStatus::SUCCESS;
}
MN_SAP::ErrStatus set(MI_SAP::I_Param_No no, std::uint32_t value) override {
if (!values.count(static_cast<int>(no))) return MN_SAP::ErrStatus::UNSUPPORTED;
values[static_cast<int>(no)] = value;
return MN_SAP::ErrStatus::SUCCESS;
}
};
struct Facilities : MF_SAP::FacilitiesParameterSink {
std::vector<MF_SAP::F_Param> received;
MN_SAP::ErrStatus set(MF_SAP::F_Param_No no, std::uint32_t value) override {
received.push_back({no, value});
return MN_SAP::ErrStatus::SUCCESS;
}
};
void test_core_mmt() {
vidf_test::section("test_core_mmt");
ManualRuntime runtime;
Radio radio;
StackConfig cfg;
cfg.mib.itsGnSecurity = false;
cfg.mib.vanetzaDisableBeaconing = true;
cfg.mib.itsGnLocalAddrConfMethod = geonet::AddrConfMethod::Managed;
cfg.mib.itsGnLocalGnAddr.mid({2, 0, 0, 0, 0, 1});
Stack stack(cfg, runtime, radio);
MN_SAP::CORE_MMT_response empty;
check(MN_SAP::CORE_MMT_response_apply(stack, empty) == Result::invalid_argument, "CORE_MMT.response needs a parameter");
BtpRequest req;
req.destination_port = 2018; req.destination_port_info = 0; req.data = {1, 2, 3};
check(stack.request(req) == Result::rejected, "no position before the management entity supplied one");
// Time and position vector through the management plane (Annex K.3, EN 302 890-2 clause 5.5.2 data set).
MN_SAP::CORE_MMT_response response;
response.time = Clock::time_point(std::chrono::seconds(716292005));
PositionFix fix;
fix.timestamp = *response.time;
fix.latitude = 48.1 * units::degree; fix.longitude = 11.6 * units::degree;
fix.speed = 1.5 * units::si::meters_per_second; fix.course = 90.0 * units::true_north_degrees;
response.local_position_vector = fix;
check(MN_SAP::CORE_MMT_response_apply(stack, response) == Result::accepted, "time and position applied");
check(runtime.now() == *response.time, "time reached the runtime");
check(stack.request(req) == Result::accepted && radio.packets.size() == 1, "position enables transmission");
// Managed address configuration (clause 10.2.1.3.3): the SO PV of the next packet carries the new MID.
MN_SAP::CORE_MMT_response address;
geonet::Address gn_addr = cfg.mib.itsGnLocalGnAddr;
gn_addr.mid({2, 0, 0, 0, 0, 9});
address.geonetworking_address = gn_addr;
check(MN_SAP::CORE_MMT_response_apply(stack, address) == Result::accepted, "managed address update applied");
stack.request(req);
const auto& wire = radio.packets.back();
check(wire.source == MacAddress {2, 0, 0, 0, 0, 9}, "link-layer source follows the CORE_MMT address");
// SO PV in an unsecured SHB: Basic (4) + Common (8) headers, then GN_ADDR (2 octets + MID).
check(wire.data.size() > 18 && std::equal(wire.source.octets.begin(), wire.source.octets.end(), wire.data.begin() + 14),
"SO PV MID follows the CORE_MMT address");
check(stack.address().mid() == MacAddress {2, 0, 0, 0, 0, 9}, "Stack::address reports the update");
MN_SAP::CORE_MMT_response mapping;
mapping.tc_mapping = std::vector<std::uint8_t> {0};
check(MN_SAP::CORE_MMT_response_apply(stack, mapping) == Result::unsupported, "TC mapping is not silently accepted");
MN_SAP::CORE_MMT_response regress;
regress.time = *response.time - std::chrono::seconds(1);
check(MN_SAP::CORE_MMT_response_apply(stack, regress) == Result::time_regression, "time regression rejected");
// Auto configuration refuses an address from the management plane (clause 10.2.1.2).
StackConfig auto_cfg = cfg;
auto_cfg.mib.itsGnLocalAddrConfMethod = geonet::AddrConfMethod::Auto;
Stack auto_stack(auto_cfg, runtime, radio);
check(MN_SAP::CORE_MMT_response_apply(auto_stack, address) == Result::unsupported, "auto configuration keeps its address");
}
void test_parameter_saps() {
vidf_test::section("test_parameter_saps");
DccAdapter adapter;
// MN-GET: known parameters answered, unknown ones reported, no values invented.
MN_SAP::MN_GET_request get {7, 1, {MN_SAP::N_Param_No::CHANNEL_NUMBER, MN_SAP::N_Param_No::GLOBAL_CBR,
MN_SAP::N_Param_No::TX_POWER_LEVEL_LIMIT}};
auto got = MN_SAP::MN_GET_request_submit(&adapter, get);
check(got.nt_id == 7 && got.command_ref == 1, "MN-GET.confirm echoes NT-ID and CommandRef");
check(got.n_param.size() == 2 && got.n_param[0].no == MN_SAP::N_Param_No::CHANNEL_NUMBER && got.n_param[0].value == 5 &&
got.n_param[1].value == 23, "provider values returned");
check(got.errors.size() == 1 && got.errors[0].n_param_no == MN_SAP::N_Param_No::GLOBAL_CBR &&
got.errors[0].err_status == MN_SAP::ErrStatus::UNSUPPORTED, "unmeasured global CBR is an error, not zero");
auto none = MN_SAP::MN_GET_request_submit(nullptr, get);
check(none.n_param.empty() && none.errors.size() == 3, "without a provider every parameter is unsupported");
// MN-SET: read-only and format violations are refused before the provider.
MN_SAP::MN_SET_request set {7, 2, {{MN_SAP::N_Param_No::TX_POWER_LEVEL_LIMIT, 20},
{MN_SAP::N_Param_No::LOCAL_CBR, 10},
{MN_SAP::N_Param_No::CHANNEL_NUMBER, 9}}};
auto set_confirm = MN_SAP::MN_SET_request_submit(&adapter, set);
check(adapter.values[6] == 20, "writable parameter reached the adapter");
check(set_confirm.errors.size() == 2 && set_confirm.errors[0].err_status == MN_SAP::ErrStatus::READ_ONLY &&
set_confirm.errors[1].err_status == MN_SAP::ErrStatus::INVALID_VALUE, "read-only and out-of-format writes reported");
check(adapter.values[1] == 5, "invalid channel number never reached the adapter");
// MF-SET: F-Params delivered to the facilities sink; a null sink is unsupported.
Facilities facilities;
MF_SAP::MF_SET_request mf {3, 4, {{MF_SAP::F_Param_No::CHANNEL_NUMBER, 5}, {MF_SAP::F_Param_No::AVAILABLE_RESOURCE, 70},
{MF_SAP::F_Param_No::CHANNEL_NUMBER, 0}}};
auto mf_confirm = MF_SAP::MF_SET_request_submit(&facilities, mf);
check(facilities.received.size() == 2 && facilities.received[1].value == 70, "MF-SET delivered the resource to the facilities sink");
check(mf_confirm.fac_id == 3 && mf_confirm.errors.size() == 1 && mf_confirm.errors[0].err_status == MN_SAP::ErrStatus::INVALID_VALUE,
"channel 0 is outside Table 13");
check(MF_SAP::MF_SET_request_submit(nullptr, mf).errors.size() == 3, "no sink: every F-Param unsupported");
MF_SAP::MF_COMMAND_request command {3, 5, 42, {}};
check(MF_SAP::MF_COMMAND_request_submit(&facilities, command).err_status == MN_SAP::ErrStatus::INVALID_COMMAND_REQUEST_NUMBER,
"unknown MF-COMMAND acknowledged with ErrStatus 5");
// MI-GET/SET over the access adapter.
MI_SAP::MI_GET_request mi_get {1, 6, {MI_SAP::I_Param_No::CHANNEL_NUMBER, MI_SAP::I_Param_No::LOCAL_CBR}};
auto mi = MI_SAP::MI_GET_request_submit(&adapter, mi_get);
check(mi.i_param.size() == 1 && mi.i_param[0].value == 5 && mi.errors.size() == 1, "MI-GET: measured value and unmeasured CBR");
MI_SAP::MI_SET_request mi_set {1, 7, {{MI_SAP::I_Param_No::TX_POWER_LEVEL_LIMIT, 40}, {MI_SAP::I_Param_No::MESSAGE_LENGTH, 1}}};
auto mi_confirm = MI_SAP::MI_SET_request_submit(&adapter, mi_set);
check(mi_confirm.errors.size() == 2 && mi_confirm.errors[0].err_status == MN_SAP::ErrStatus::INVALID_VALUE &&
mi_confirm.errors[1].err_status == MN_SAP::ErrStatus::READ_ONLY && adapter.values[57] == 23,
"MI-SET refuses 40 dBm and a read-only parameter");
}
} // namespace
void test_management() {
test_core_mmt();
test_parameter_saps();
}
@@ -0,0 +1,311 @@
// TS 102 941 V2.2.1 enrolment and authorization round trips against the test
// trust domain: the EA/AA side uses the shared authority-side tooling of
// pki_authority.hpp (the same tooling vidf_issue's ea-respond/aa-respond commands
// use against real, separately-run requests), the station side is the library.
// Nothing is accepted on decoding alone.
#include "check.hpp"
#include "pki_authority.hpp"
#include "test_backend.hpp"
#include "test_trust_domain.hpp"
#include <vanetza_idf/pki.hpp>
#include <vanetza_idf/security.hpp>
#if VIDF_BACKEND_OPENSSL
#include <vanetza_idf/ecies_openssl.hpp>
#endif
#if VIDF_BACKEND_MBEDTLS
#include <vanetza_idf/ecies_mbedtls.hpp>
#endif
#include <algorithm>
#include <chrono>
using namespace vanetza;
using namespace vanetza::security;
using namespace vanetza_idf;
using vidf_test::check;
namespace {
#if VIDF_BACKEND_OPENSSL
using TestEcies = pki::EciesOpenSsl;
#else
using TestEcies = pki::EciesMbedTls;
#endif
const Clock::time_point t0 = Clock::time_point(std::chrono::seconds(716292005));
void test_ecies(Backend& backend, pki::EciesBackend& ecies) {
vidf_test::section("test_ecies");
for (auto type : {KeyType::NistP256, KeyType::BrainpoolP256r1}) {
const auto recipient = ecies.generate_key(type);
const ByteBuffer p1 = backend.calculate_hash(HashAlgorithm::SHA256, {1, 2, 3});
std::array<std::uint8_t, 16> aes {};
const auto rnd = ecies.random(16);
std::copy(rnd.begin(), rnd.end(), aes.begin());
auto wrapped = pki::ecies_encrypt_key(backend, ecies, recipient.pub, p1, aes);
check(wrapped.has_value() && wrapped->v.type == type, "ECIES wraps the AES key with an ephemeral point");
auto opened = pki::ecies_decrypt_key(backend, ecies, recipient.priv, p1, *wrapped);
check(opened && *opened == aes, "recipient recovers the AES key");
// compressed ephemeral point on the wire is accepted as well
pki::EncryptedKey compressed = *wrapped;
compressed.v.compression = (wrapped->v.y.back() & 1) ? KeyCompression::Y1 : KeyCompression::Y0;
compressed.v.y.clear();
opened = pki::ecies_decrypt_key(backend, ecies, recipient.priv, p1, compressed);
check(opened && *opened == aes, "compressed ephemeral point decompressed for ECDH");
auto tampered = *wrapped; tampered.t[0] ^= 1;
check(!pki::ecies_decrypt_key(backend, ecies, recipient.priv, p1, tampered), "bad authentication tag refused");
auto other = ecies.generate_key(type);
check(!pki::ecies_decrypt_key(backend, ecies, other.priv, p1, *wrapped), "another private key refused");
check(!pki::ecies_decrypt_key(backend, ecies, recipient.priv, ByteBuffer {9}, *wrapped), "other P1 refused");
}
// KDF2: 32-octet blocks, counter from 1, output truncated to the requested length
const ByteBuffer z = {0x01, 0x02};
const auto k48 = pki::kdf2_sha256(backend, z, {}, 48);
const auto k32 = pki::kdf2_sha256(backend, z, {}, 32);
check(k48.size() == 48 && std::equal(k32.begin(), k32.end(), k48.begin()), "KDF2 prefix property");
ByteBuffer block1 = z; block1.insert(block1.end(), {0, 0, 0, 1});
check(backend.calculate_hash(HashAlgorithm::SHA256, block1) == k32, "KDF2 first block = H(Z || 00000001 || P1)");
// AES-CCM symmetric round trip with PSK recipient info
std::array<std::uint8_t, 16> psk {};
const auto rnd = ecies.random(16);
std::copy(rnd.begin(), rnd.end(), psk.begin());
const ByteBuffer secret_text = {0x10, 0x20, 0x30, 0x40, 0x50};
auto enc = pki::encrypt_with_psk(ecies, psk, secret_text);
check(enc.has_value(), "psk encryption");
auto dec = pki::decrypt_with_psk(ecies, psk, *enc);
check(dec && *dec == secret_text, "psk decryption round trip");
std::array<std::uint8_t, 16> wrong = psk; wrong[3] ^= 0x40;
check(!pki::decrypt_with_psk(ecies, wrong, *enc), "pskRecipInfo mismatch refused before decryption");
ByteBuffer corrupt = *enc; corrupt[corrupt.size() - 3] ^= 0x01;
check(!pki::decrypt_with_psk(ecies, psk, corrupt), "CCM tag failure refused");
}
bool same_point(const PublicKey& a, const PublicKey& b) {
return a.type == b.type && a.x == b.x;
}
void test_enrolment_and_authorization(Backend& backend, pki::EciesBackend& ecies) {
vidf_test::section("test_enrolment_and_authorization");
vidf_test::TrustDomain domain(backend, t0);
vidf_test::Authority authority {backend, ecies};
// ---- Enrolment (clause 6.2.3.2): initial request with the canonical key ----
const pki::KeyPair canonical = ecies.generate_key(KeyType::NistP256);
pki::EnrolmentRequestParameters ec_params;
ec_params.its_id = ByteBuffer {'v', 'i', 'd', 'f', '-', 's', 't', 'a', 't', 'i', 'o', 'n'};
ec_params.verification_key = ecies.generate_key(KeyType::NistP256);
ec_params.app_permissions = {{aid::SCR, {0x01, 0xc0}}};
ec_params.outer_signer_key = canonical.priv;
ByteBuffer request;
pki::RequestContext ec_context;
check(pki::build_enrolment_request(backend, ecies, t0, ec_params, domain.ea.certificate, request, ec_context) == Result::accepted,
"EnrolmentRequest built");
check(pki::build_enrolment_request(backend, ecies, t0, ec_params, domain.root.certificate, request, ec_context) == Result::invalid_argument,
"recipient without encryption key refused");
// EA: decrypt, verify the outer signature and the proof of possession, decode
std::array<std::uint8_t, 16> ea_seen_key {};
auto parsed = vidf_test::parse_enrolment_request(backend, ecies, domain.ea, domain.ea_encryption_key, request,
&canonical.pub, nullptr, ea_seen_key);
check(parsed.has_value() && ea_seen_key == ec_context.aes_key, "EA decrypts the request and recovers the AES key");
check(parsed && same_point(parsed->verification_key, ec_params.verification_key.pub),
"InnerEcRequest carries the requested verification key");
check(parsed && parsed->its_id == ec_params.its_id, "InnerEcRequest carries itsId");
check(parsed && !parsed->re_enrolment, "initial enrolment, not a re-enrolment");
check(parsed && parsed->app_permissions.size() == 1, "requested attributes: appPermissions only");
{
std::array<std::uint8_t, 16> discard {};
check(!vidf_test::parse_authorization_request(backend, ecies, domain.aa, domain.aa_encryption_key, request, discard).has_value(),
"AA cannot decrypt a request addressed to the EA");
check(!vidf_test::parse_enrolment_request(backend, ecies, domain.ea, domain.ea_encryption_key, request,
&ec_params.verification_key.pub, nullptr, discard).has_value(),
"outer signature is not by the new key");
}
// EA issues the EC and answers
const auto ec = domain.issue_credential_for(parsed->verification_key, "vidf-station EC", t0 - std::chrono::hours(1), 24 * 365);
const ByteBuffer response = authority.enrolment_response(t0, ec_context.aes_key, request, 0, &ec, domain.ea);
pki::EnrolmentResponse ec_response;
check(pki::parse_enrolment_response(backend, ecies, ec_context, domain.ea.certificate, response, ec_response) == Result::accepted,
"EnrolmentResponse accepted");
check(ec_response.response_code == 0 && ec_response.certificate && ec_response.certificate->encode() == ec.encode(),
"EC returned unchanged");
check(pki::parse_enrolment_response(backend, ecies, ec_context, domain.aa.certificate, response, ec_response) == Result::rejected,
"response signed by the EA is rejected when the AA is expected");
pki::RequestContext other_context = ec_context; other_context.request_hash[0] ^= 1;
check(pki::parse_enrolment_response(backend, ecies, other_context, domain.ea.certificate, response, ec_response) == Result::rejected,
"requestHash mismatch rejected");
other_context = ec_context; other_context.aes_key[5] ^= 1;
check(pki::parse_enrolment_response(backend, ecies, other_context, domain.ea.certificate, response, ec_response) == Result::rejected,
"response encrypted for another request rejected");
const ByteBuffer forged = authority.enrolment_response(t0, ec_context.aes_key, request, 0, &ec, domain.aa);
check(pki::parse_enrolment_response(backend, ecies, ec_context, domain.ea.certificate, forged, ec_response) == Result::rejected,
"response signed by a different authority rejected");
const ByteBuffer denied = authority.enrolment_response(t0, ec_context.aes_key, request, 3, nullptr, domain.ea);
check(pki::parse_enrolment_response(backend, ecies, ec_context, domain.ea.certificate, denied, ec_response) == Result::accepted &&
ec_response.response_code == 3 && !ec_response.certificate, "negative response without certificate is reported");
const ByteBuffer dishonest = authority.enrolment_response(t0, ec_context.aes_key, request, 0, nullptr, domain.ea);
check(pki::parse_enrolment_response(backend, ecies, ec_context, domain.ea.certificate, dishonest, ec_response) == Result::rejected,
"positive response without certificate rejected");
// ---- Re-enrolment: outer signer = digest of the current EC ----
pki::EnrolmentRequestParameters renew = ec_params;
const auto ec_id = *ec.calculate_digest();
renew.its_id.assign(ec_id.begin(), ec_id.end());
renew.verification_key = ecies.generate_key(KeyType::NistP256);
renew.outer_signer_key = ec_params.verification_key.priv;
renew.current_ec = &ec;
pki::RequestContext renew_context;
ByteBuffer renew_request;
check(pki::build_enrolment_request(backend, ecies, t0, renew, domain.ea.certificate, renew_request, renew_context) == Result::accepted,
"re-enrolment request built");
std::array<std::uint8_t, 16> renew_seen_key {};
auto reparsed = vidf_test::parse_enrolment_request(backend, ecies, domain.ea, domain.ea_encryption_key, renew_request,
nullptr, &ec, renew_seen_key);
check(reparsed.has_value() && reparsed->re_enrolment && reparsed->its_id == renew.its_id,
"re-enrolment outer signature by the EC digest");
// ---- Authorization (clause 6.2.3.3) with privacy and POP ----
pki::AuthorizationRequestParameters at_params;
at_params.verification_key = ecies.generate_key(KeyType::NistP256);
at_params.app_permissions = {{aid::VRU, {0x01}}, {aid::CA, {0x01, 0xff, 0xfc}}};
at_params.validity_period = std::make_pair(t0 - std::chrono::hours(1), std::uint16_t(24));
at_params.ec = &ec;
at_params.ec_key = ec_params.verification_key.priv;
ByteBuffer at_request;
pki::RequestContext at_context;
check(pki::build_authorization_request(backend, ecies, t0, at_params, domain.ea.certificate, domain.aa.certificate, at_request, at_context) == Result::accepted,
"AuthorizationRequest built");
// AA: decrypt, verify the proof of possession, decode
std::array<std::uint8_t, 16> aa_seen_key {};
auto at_parsed = vidf_test::parse_authorization_request(backend, ecies, domain.aa, domain.aa_encryption_key, at_request, aa_seen_key);
check(at_parsed.has_value(), "AA decrypts, verifies and decodes the request");
check(at_parsed && same_point(at_parsed->verification_key, at_params.verification_key.pub),
"InnerAtRequest carries the AT verification key");
check(at_parsed && at_parsed->app_permissions.size() == 2, "requested permissions present");
const auto ea_id = *domain.ea.certificate.calculate_digest();
check(at_parsed && std::equal(ea_id.begin(), ea_id.end(), at_parsed->ea_id.begin()), "SharedAtRequest names the EA");
check(at_parsed && at_parsed->ec_signature_encrypted, "EC signature encrypted for the EA (privacy)");
// EA (authorization validation, the diagram's "AA <-> EA"): decrypt the EC signature
// and check it against the SharedAtRequest -- the entitlement check itself
check(at_parsed && vidf_test::validate_entitlement(backend, ecies, domain.ea, domain.ea_encryption_key, *at_parsed, ec),
"EA validates the EC's entitlement");
const auto impostor = domain.issue_credential_for(ecies.generate_key(KeyType::NistP256).pub, "impostor",
t0 - std::chrono::hours(1), 24);
check(at_parsed && !vidf_test::validate_entitlement(backend, ecies, domain.ea, domain.ea_encryption_key, *at_parsed, impostor),
"entitlement check rejects a certificate that did not sign this request");
// AA issues the AT and answers
const auto at = domain.issue_ticket_for(at_parsed->verification_key, at_parsed->app_permissions, t0 - std::chrono::hours(1), 24);
const ByteBuffer at_response = authority.authorization_response(t0, at_context.aes_key, at_request, 0, &at, domain.aa);
pki::AuthorizationResponse at_response_parsed;
check(pki::parse_authorization_response(backend, ecies, at_context, domain.aa.certificate, at_response, at_response_parsed) == Result::accepted &&
at_response_parsed.response_code == 0 && at_response_parsed.certificate, "AuthorizationResponse accepted");
check(pki::parse_authorization_response(backend, ecies, at_context, domain.ea.certificate, at_response, at_response_parsed) == Result::rejected,
"AT response signed by the AA is rejected when the EA is expected");
// the new AT with the station's own key enters the pool and signs
vanetza_idf::security::CertificatePool pool(backend);
check(pool.add(at_response_parsed.certificate->encode(), at_params.verification_key.priv) == Result::accepted,
"AT from the response and the generated key form a usable ticket");
check(pool.add(at_response_parsed.certificate->encode(), canonical.priv) == Result::invalid_argument, "another key does not match the AT");
// ---- No privacy / no POP variant ----
at_params.privacy = false;
at_params.include_pop = false;
ByteBuffer plain_request;
pki::RequestContext plain_context;
check(pki::build_authorization_request(backend, ecies, t0, at_params, domain.ea.certificate, domain.aa.certificate, plain_request, plain_context) == Result::accepted,
"AuthorizationRequest without POP built");
std::array<std::uint8_t, 16> plain_seen_key {};
auto plain_parsed = vidf_test::parse_authorization_request(backend, ecies, domain.aa, domain.aa_encryption_key, plain_request, plain_seen_key);
check(plain_parsed.has_value() && !plain_parsed->ec_signature_encrypted, "EC signature in clear without privacy, decoded without POP");
at_params.hash = HashAlgorithm::SHA384;
check(pki::build_authorization_request(backend, ecies, t0, at_params, domain.ea.certificate, domain.aa.certificate, plain_request, plain_context) == Result::unsupported,
"SHA-384 external hash is refused, not silently downgraded");
}
} // namespace
// TS 102 941 V2.2.1 clause 6.3: the RCA's CTL and CRL as built for a distribution centre
// and as an ITS-S reads them (clause 6.3.6: signed by its RCA, otherwise nothing is taken).
void test_trust_lists(Backend& backend) {
vidf_test::section("test_trust_lists");
const Clock::time_point now = Clock::time_point(std::chrono::seconds(716292005));
vidf_test::TrustDomain domain {backend, now};
vidf_test::TrustDomain other {backend, now};
const auto root_id = *domain.root.certificate.calculate_digest();
const auto aa_id = *domain.aa.certificate.calculate_digest();
pki::TrustListEntries entries;
entries.ea.push_back({domain.ea.certificate.encode(), "http://ea.example.test/"});
entries.aa.push_back({domain.aa.certificate.encode(), "http://aa.example.test/"});
entries.dc.push_back({"http://dc.example.test/", {root_id}});
const pki::Time32 next_update = 716292005 + 7 * 86400;
auto ctl = pki::build_rca_ctl(backend, now, domain.root.certificate, domain.root.key, entries, next_update, 3);
check(ctl.has_value() && !ctl->empty(), "RCA CTL built (FullCtl, sequence 3)");
// 1. It reads back, signed by the RCA, with every entry.
auto list = pki::parse_rca_ctl(backend, *ctl, domain.root.certificate);
check(list.has_value(), "CTL verifies against the RCA that signed it");
if (list) {
check(list->sequence == 3 && list->next_update == next_update && list->full, "CTL sequence, nextUpdate and isFullCtl");
check(list->ea.size() == 1 && list->aa.size() == 1 && *list->aa[0].calculate_digest() == aa_id, "EA and AA entries decoded");
check(list->dc.size() == 1 && list->dc[0].url == "http://dc.example.test/" && list->dc[0].certificates == std::vector<HashedId8> {root_id},
"DC entry with the RCA digest");
}
// 2. Not by another root, not tampered, not with a foreign AA.
check(!pki::parse_rca_ctl(backend, *ctl, other.root.certificate), "a CTL of another RCA is refused");
{ ByteBuffer bad = *ctl; bad[bad.size() / 2] ^= 0x01; check(!pki::parse_rca_ctl(backend, bad, domain.root.certificate), "a tampered CTL is refused"); }
{
pki::TrustListEntries foreign;
foreign.aa.push_back({other.aa.certificate.encode(), ""});
auto bad = pki::build_rca_ctl(backend, now, domain.root.certificate, domain.root.key, foreign, next_update, 4);
check(bad && !pki::parse_rca_ctl(backend, *bad, domain.root.certificate), "an AA not issued by the RCA is refused even inside its signed CTL");
}
{ // an RCA without the CTL permission cannot sign one (TS 103 097 clause 7.2.3)
auto ticket = domain.issue_ticket({{aid::VRU, {0x01}}}, now - std::chrono::hours(1), 24);
check(!pki::build_rca_ctl(backend, now, ticket.certificate, ticket.key, entries, next_update, 1), "a certificate without psid 624 signs no CTL");
}
// 3. Applied: the authorities become issuers.
{
vanetza_idf::security::TrustConfiguration trust;
check(trust.add_root(domain.root.certificate.encode()) == Result::accepted, "root provisioned");
check(pki::apply(*list, trust) == 2 && trust.authorities().size() == 3, "CTL adds EA and AA as issuers");
check(pki::apply(*list, trust) == 0, "applying the same CTL again adds nothing");
}
// 4. CRL: revoking the AA.
auto crl = pki::build_crl(backend, now, domain.root.certificate, domain.root.key, {aa_id}, 716292005, next_update);
check(crl.has_value(), "CRL built");
auto revoked = pki::parse_crl(backend, *crl, domain.root.certificate);
check(revoked && revoked->revoked == std::vector<HashedId8> {aa_id} && revoked->this_update == 716292005 && revoked->next_update == next_update,
"CRL verifies and lists the AA");
check(!pki::parse_crl(backend, *crl, other.root.certificate), "a CRL of another RCA is refused");
check(!pki::parse_rca_ctl(backend, *crl, domain.root.certificate) && !pki::parse_crl(backend, *ctl, domain.root.certificate),
"a CRL is no CTL and a CTL no CRL (psid)");
{
vanetza_idf::security::TrustConfiguration trust;
trust.add_root(domain.root.certificate.encode());
check(pki::apply(*revoked, domain.root.certificate, trust) == 1 && trust.revocations().is_revoked(root_id, aa_id) &&
!trust.revocations().is_revoked(root_id, root_id), "CRL entries become revocations by the RCA");
auto empty = pki::build_crl(backend, now, domain.root.certificate, domain.root.key, {}, 716292005, next_update);
auto none = pki::parse_crl(backend, *empty, domain.root.certificate);
check(none && pki::apply(*none, domain.root.certificate, trust) == 0 && !trust.revocations().is_revoked(root_id, aa_id),
"a newer empty CRL replaces the earlier list");
}
}
void test_pki() {
vidf_test::TestBackend backend;
TestEcies ecies;
test_ecies(backend, ecies);
test_enrolment_and_authorization(backend, ecies);
test_trust_lists(backend);
#if VIDF_BACKEND_OPENSSL && VIDF_BACKEND_MBEDTLS
// Cross-check: the PSA primitives interoperate with the OpenSSL ones.
pki::EciesMbedTls psa;
const auto recipient = psa.generate_key(KeyType::NistP256);
const ByteBuffer p1 = backend.calculate_hash(HashAlgorithm::SHA256, {7});
std::array<std::uint8_t, 16> aes {};
auto wrapped = pki::ecies_encrypt_key(backend, ecies, recipient.pub, p1, aes);
auto opened = pki::ecies_decrypt_key(backend, psa, recipient.priv, p1, *wrapped);
check(opened && *opened == aes, "OpenSSL-wrapped key opened by PSA ECDH/HMAC");
wrapped = pki::ecies_encrypt_key(backend, psa, recipient.pub, p1, aes);
opened = pki::ecies_decrypt_key(backend, ecies, recipient.priv, p1, *wrapped);
check(opened && *opened == aes, "PSA-wrapped key opened by OpenSSL");
std::array<std::uint8_t, 12> nonce {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
ByteBuffer ct, pt;
check(psa.aes_ccm_encrypt(aes, nonce, {1, 2, 3}, ct) && ecies.aes_ccm_decrypt(aes, nonce, ct, pt) && pt == ByteBuffer {1, 2, 3},
"PSA AES-CCM ciphertext accepted by OpenSSL");
check(ecies.aes_ccm_encrypt(aes, nonce, {4, 5}, ct) && psa.aes_ccm_decrypt(aes, nonce, ct, pt) && pt == ByteBuffer {4, 5},
"OpenSSL AES-CCM ciphertext accepted by PSA");
check(psa.hmac_sha256({1}, {2}) == ecies.hmac_sha256({1}, {2}), "HMAC-SHA256 agrees across backends");
test_enrolment_and_authorization(backend, psa);
#endif
}
@@ -0,0 +1,185 @@
// Security entity regression: crypto backends against OpenSSL as oracle.
#include "check.hpp"
#include <vanetza_idf/ecc.hpp>
#include <vanetza/security/backend_openssl.hpp>
#include <vanetza/security/openssl_wrapper.hpp>
#include <vanetza/security/key_type.hpp>
#if VIDF_BACKEND_MBEDTLS
#include <vanetza_idf/backend_mbedtls.hpp>
#endif
#include <openssl/bn.h>
#include <openssl/ec.h>
#include <openssl/obj_mac.h>
#include <algorithm>
#include <memory>
#include <string>
using namespace vanetza;
using namespace vanetza::security;
using vidf_test::check;
using vidf_test::hex;
namespace {
const struct { KeyType type; int nid; const char* name; } curves[] = {
{KeyType::NistP256, NID_X9_62_prime256v1, "NIST P-256"},
{KeyType::BrainpoolP256r1, NID_brainpoolP256r1, "brainpoolP256r1"},
{KeyType::BrainpoolP384r1, NID_brainpoolP384r1, "brainpoolP384r1"},
};
std::string upper_hex(const BIGNUM* n, std::size_t octets) {
ByteBuffer bytes(octets);
BN_bn2binpad(n, bytes.data(), bytes.size());
auto s = hex(bytes);
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::toupper(c); });
return s;
}
struct OpenSslKey {
PrivateKey priv;
PublicKey pub;
};
// Fresh key on the given curve, straight from OpenSSL: the reference the port is measured against.
OpenSslKey generate(KeyType type, int nid) {
openssl::Key key(nid);
openssl::check(EC_KEY_generate_key(key));
const std::size_t n = key_length(type);
OpenSslKey out;
out.priv.type = type;
out.priv.key.resize(n);
BN_bn2binpad(EC_KEY_get0_private_key(key), out.priv.key.data(), n);
openssl::BigNumber x, y;
openssl::BigNumberContext ctx;
EC_POINT_get_affine_coordinates(EC_KEY_get0_group(key), EC_KEY_get0_public_key(key), x, y, ctx);
out.pub.type = type;
out.pub.compression = KeyCompression::NoCompression;
out.pub.x.resize(n); out.pub.y.resize(n);
BN_bn2binpad(x, out.pub.x.data(), n);
BN_bn2binpad(y, out.pub.y.data(), n);
return out;
}
PublicKey compressed(const PublicKey& key) {
PublicKey c = key;
c.compression = (key.y.back() & 1) ? KeyCompression::Y1 : KeyCompression::Y0;
c.y.clear();
return c;
}
void test_curve_constants() {
for (const auto& curve : curves) {
openssl::Group group(curve.nid);
openssl::BigNumber p, a, b;
openssl::BigNumberContext ctx;
openssl::check(EC_GROUP_get_curve(group, p, a, b, ctx));
const auto* params = vanetza_idf::ecc::curve(curve.type);
check(params != nullptr, "curve parameters known");
check(upper_hex(p, params->octets) == params->p, "field prime matches OpenSSL");
check(upper_hex(a, params->octets) == params->a, "coefficient a matches OpenSSL");
check(upper_hex(b, params->octets) == params->b, "coefficient b matches OpenSSL");
check(BN_mod_word(p, 4) == 3, "p = 3 (mod 4) so the square root formula applies");
}
check(vanetza_idf::ecc::curve(KeyType::Unspecified) == nullptr, "unspecified key type has no curve");
}
void test_decompression() {
for (const auto& curve : curves) {
for (int round = 0; round < 4; ++round) {
const auto key = generate(curve.type, curve.nid);
const bool odd = key.pub.y.back() & 1;
auto point = vanetza_idf::ecc::decompress(curve.type, key.pub.x, odd);
check(point && point->x == key.pub.x && point->y == key.pub.y, "y recovered from x and parity");
auto other = vanetza_idf::ecc::decompress(curve.type, key.pub.x, !odd);
check(other && other->y != key.pub.y, "opposite parity gives the negated point");
// p - y must also lie on the curve according to OpenSSL.
openssl::Group group(curve.nid);
openssl::Point p(group);
openssl::BigNumberContext ctx;
check(EC_POINT_set_affine_coordinates(group, p, openssl::BigNumber(other->x), openssl::BigNumber(other->y), ctx) == 1 &&
EC_POINT_is_on_curve(group, p, ctx) == 1, "negated point is on the curve");
}
ByteBuffer short_x(key_length(curve.type) - 1, 0x01);
check(!vanetza_idf::ecc::decompress(curve.type, short_x, false), "wrong coordinate length rejected");
ByteBuffer beyond(key_length(curve.type), 0xff); // >= p for every curve here
check(!vanetza_idf::ecc::decompress(curve.type, beyond, false), "x outside the field rejected");
}
// Any decompression result must satisfy the curve equation; a non-residue x must be refused.
unsigned refused = 0;
for (std::uint8_t seed = 0; seed < 64; ++seed) {
ByteBuffer x(32, seed);
auto point = vanetza_idf::ecc::decompress(KeyType::NistP256, x, false);
if (!point) { ++refused; continue; }
openssl::Group group(NID_X9_62_prime256v1);
openssl::Point p(group);
openssl::BigNumberContext ctx;
check(EC_POINT_set_affine_coordinates(group, p, openssl::BigNumber(point->x), openssl::BigNumber(point->y), ctx) == 1 &&
EC_POINT_is_on_curve(group, p, ctx) == 1, "decompressed point lies on P-256");
}
check(refused > 0 && refused < 64, "quadratic non-residues are refused, residues accepted");
}
void round_trip(Backend& signer, Backend& verifier, const char* label) {
for (const auto& curve : curves) {
const auto key = generate(curve.type, curve.nid);
const auto algo = curve.type == KeyType::BrainpoolP384r1 ? HashAlgorithm::SHA384 : HashAlgorithm::SHA256;
const ByteBuffer message = {'v', 'a', 'n', 'e', 't', 'z', 'a', '-', 'i', 'd', 'f'};
const auto digest = signer.calculate_hash(algo, message);
check(digest == verifier.calculate_hash(algo, message), "both backends hash identically");
check(digest.size() == (algo == HashAlgorithm::SHA384 ? 48u : 32u), "digest length per algorithm");
const auto signature = signer.sign_digest(key.priv, digest);
check(signature.type == curve.type && signature.r.size() == key_length(curve.type) &&
signature.s.size() == key_length(curve.type), "raw r||s signature sizes");
check(verifier.verify_digest(key.pub, digest, signature), label);
check(verifier.verify_digest(compressed(key.pub), digest, signature), "verification with compressed public key");
auto tampered = digest; tampered[0] ^= 0x80;
check(!verifier.verify_digest(key.pub, tampered, signature), "tampered digest rejected");
auto bad = signature; bad.s[5] ^= 0x01;
check(!verifier.verify_digest(key.pub, digest, bad), "tampered signature rejected");
const auto other = generate(curve.type, curve.nid);
check(!verifier.verify_digest(other.pub, digest, signature), "wrong public key rejected");
}
// Legacy P-256 data signing (v2 profile helpers), cross-verified.
const auto pair = signer.generate_key_pair();
const ByteBuffer data(100, 0x5a);
const auto legacy = signer.sign_data(pair.private_key, data);
check(verifier.verify_data(pair.public_key, data, legacy), "generated key pair signs and verifies across backends");
auto data2 = data; data2[7] ^= 1;
check(!verifier.verify_data(pair.public_key, data2, legacy), "legacy signature bound to data");
// Compressed point handling of the backend itself against OpenSSL.
Compressed_Lsb_Y_0 y0; y0.x.assign(pair.public_key.x.begin(), pair.public_key.x.end());
Compressed_Lsb_Y_1 y1; y1.x = y0.x;
const EccPoint point = (pair.public_key.y.back() & 1) ? EccPoint(y1) : EccPoint(y0);
auto expanded = signer.decompress_point(point);
check(expanded && std::equal(expanded->y.begin(), expanded->y.end(), pair.public_key.y.begin()),
"backend decompress_point recovers the generated public key");
}
} // namespace
void test_crypto_backends() {
test_curve_constants();
test_decompression();
BackendOpenSsl openssl_backend;
round_trip(openssl_backend, openssl_backend, "OpenSSL sign/verify per curve");
#if VIDF_BACKEND_MBEDTLS
vanetza_idf::BackendMbedTls psa;
round_trip(psa, openssl_backend, "PSA signature verified by OpenSSL");
round_trip(openssl_backend, psa, "OpenSSL signature verified by PSA");
round_trip(psa, psa, "PSA sign/verify per curve");
// Key cache: a second signature with the same key must reuse the imported key, a third key evicts.
psa.set_key_cache_size(2);
const auto k1 = generate(KeyType::NistP256, NID_X9_62_prime256v1);
const auto k2 = generate(KeyType::BrainpoolP256r1, NID_brainpoolP256r1);
const auto k3 = generate(KeyType::BrainpoolP384r1, NID_brainpoolP384r1);
const auto d256 = psa.calculate_hash(HashAlgorithm::SHA256, {1, 2, 3});
const auto d384 = psa.calculate_hash(HashAlgorithm::SHA384, {1, 2, 3});
for (int i = 0; i < 3; ++i) {
check(openssl_backend.verify_digest(k1.pub, d256, psa.sign_digest(k1.priv, d256)), "cached key still signs");
check(openssl_backend.verify_digest(k2.pub, d256, psa.sign_digest(k2.priv, d256)), "second cached key signs");
check(openssl_backend.verify_digest(k3.pub, d384, psa.sign_digest(k3.priv, d384)), "evicting key signs");
}
PrivateKey wrong = k1.priv; wrong.key.pop_back();
bool threw = false;
try { psa.sign_digest(wrong, d256); } catch (const std::runtime_error&) { threw = true; }
check(threw, "malformed private key is refused, not signed");
#endif
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,395 @@
#include "test_trust_domain.hpp"
#include <vanetza/asn1/asn1c_wrapper.hpp>
#include <vanetza/security/ecc_point.hpp>
#include <vanetza/security/v2/basic_elements.hpp>
#include <vanetza/security/v3/asn1_types.hpp>
#include <chrono>
#include <string>
namespace vidf_test {
using namespace vanetza;
using namespace vanetza::security;
using vanetza::security::v3::Certificate;
namespace {
struct assign_compressed_point : boost::static_visitor<> {
Vanetza_Security_EccP256CurvePoint* point;
explicit assign_compressed_point(Vanetza_Security_EccP256CurvePoint* p) : point(p) {}
void operator()(const Compressed_Lsb_Y_0& p) const {
point->present = Vanetza_Security_EccP256CurvePoint_PR_compressed_y_0;
OCTET_STRING_fromBuf(&point->choice.compressed_y_0, reinterpret_cast<const char*>(p.x.data()), p.x.size());
}
void operator()(const Compressed_Lsb_Y_1& p) const {
point->present = Vanetza_Security_EccP256CurvePoint_PR_compressed_y_1;
OCTET_STRING_fromBuf(&point->choice.compressed_y_1, reinterpret_cast<const char*>(p.x.data()), p.x.size());
}
template<class T> void operator()(const T&) const { point->present = Vanetza_Security_EccP256CurvePoint_PR_NOTHING; }
};
// TS 103 097 clause 6 common fields; canonical form: compressed verification key.
void common_fields(Certificate& cert, const PublicKey& verification, Clock::time_point start, unsigned hours,
Vanetza_Security_Duration_PR unit) {
cert->version = 3;
cert->type = Vanetza_Security_CertificateType_explicit;
static const char craca[3] = {0, 0, 0};
OCTET_STRING_fromBuf(&cert->toBeSigned.cracaId, craca, sizeof(craca));
cert->toBeSigned.crlSeries = 0;
cert->toBeSigned.validityPeriod.start = v2::convert_time32(start);
cert->toBeSigned.validityPeriod.duration.present = unit;
cert->toBeSigned.validityPeriod.duration.choice.hours = hours; // same storage for every unit
ecdsa256::PublicKey legacy;
std::copy(verification.x.begin(), verification.x.end(), legacy.x.begin());
std::copy(verification.y.begin(), verification.y.end(), legacy.y.begin());
auto& indicator = cert->toBeSigned.verifyKeyIndicator;
indicator.present = Vanetza_Security_VerificationKeyIndicator_PR_verificationKey;
indicator.choice.verificationKey.present = Vanetza_Security_PublicVerificationKey_PR_ecdsaNistP256;
assign_compressed_point visitor(&indicator.choice.verificationKey.choice.ecdsaNistP256);
boost::apply_visitor(visitor, compress_public_key(legacy));
}
// PsidSspRange without sspRange: the CA may issue any SSP for that PSID (IEEE Std 1609.2
// 6.4.29 SspRange: omitting it means "all", the only range consistent with a ticket
// whose ssp is omitted, as GN-MGMT tickets are).
void add_psid_all_permission(v3::asn1::PsidGroupPermissions* group, ItsAid aid) {
auto* range = vanetza::asn1::allocate<v3::asn1::PsidSspRange>();
range->psid = aid;
ASN_SEQUENCE_ADD(&group->subjectPermissions.choice.Explicit, range);
}
// certIssuePermissions (clauses 7.2.3/7.2.4) shaped like the EU CCMS CPOC Protocol
// Release 3.0 root profile: explicit PSID/SSP ranges the CA may issue, the SSP ranges
// with the IEEE Std 1609.2 6.4.30 bitmask rule (a 1 bit fixes the subordinate's bit).
// * The application group (CA, DEN, VRU, GN-MGMT and the end-entity part of the
// Secured Certificate Request service, TS 102 941 V2.2.1 Table B.6: EC signs
// enrolment and authorization requests, 01C0/FF3F). In a root this group carries
// minChainLength 2 (IEEE Std 1609.2 6.4.28: the chain below the root runs through
// the AA/EA down to the ticket/credential, so its length is 2) and eeType app+enrol;
// a subordinate CA keeps the defaults (1, app) because it issues end entities only.
// * In a root, a second group for the CA side of the Secured Certificate Request
// service (013E/FFC1: the SSP bits an EA/AA may hold to sign responses, Table B.6),
// chain length 1 as in the CPOC profile.
void issue_permissions(Certificate& ca, bool root) {
auto* group = v3::asn1::allocate<v3::asn1::PsidGroupPermissions>();
group->subjectPermissions.present = Vanetza_Security_SubjectPermissions_PR_explicit;
v3::add_psid_group_permission(group, aid::CA, {0x01, 0xff, 0xfc}, {0xff, 0x00, 0x03});
v3::add_psid_group_permission(group, aid::DEN, {0x01, 0xff, 0xff, 0xff}, {0xff, 0x00, 0x00, 0x00});
v3::add_psid_group_permission(group, aid::VRU, {0x01}, {0xff});
add_psid_all_permission(group, aid::GN_MGMT);
v3::add_psid_group_permission(group, aid::SCR, {0x01, 0xc0}, {0xff, 0x3f});
if (root) {
group->minChainLength = vanetza::asn1::allocate<long>();
*group->minChainLength = 2;
group->eeType = vanetza::asn1::allocate<Vanetza_Security_EndEntityType_t>();
// EndEntityType BIT STRING (SIZE (8)) with app(0) and enrol(1) set; the pinned asn1c
// schema defaults an absent eeType to 00H, IEEE Std 1609.2-2022 to {app}: encode it.
group->eeType->buf = static_cast<std::uint8_t*>(vanetza::asn1::allocate(1));
group->eeType->buf[0] = 0xc0;
group->eeType->size = 1;
group->eeType->bits_unused = 0;
}
ca.add_cert_issue_permission(group);
if (root) {
auto* authorities = v3::asn1::allocate<v3::asn1::PsidGroupPermissions>();
authorities->subjectPermissions.present = Vanetza_Security_SubjectPermissions_PR_explicit;
v3::add_psid_group_permission(authorities, aid::SCR, {0x01, 0x3e}, {0xff, 0xc1});
ca.add_cert_issue_permission(authorities);
}
}
// Clause 7.2.3: the root's appPermissions are the CRL and CTL services (TS 102 941 V2.2.1
// Table B.3: a root CTL lists EA, AA and DC entries, SSP 0138; clause B.3: CRL SSP 01).
void root_fields(Certificate& root, const std::string& name) {
root->toBeSigned.id.present = Vanetza_Security_CertificateId_PR_name;
OCTET_STRING_fromBuf(&root->toBeSigned.id.choice.name, name.data(), name.size());
root.add_app_permission(aid::CRL, {0x01});
root.add_app_permission(aid::CTL, {0x01, 0x38});
issue_permissions(root, true);
}
// Clause 7.2.4: a subordinate CA carries an encryption key for the ECIES of TS 102 941 and
// appPermissions to sign certificate responses (SCR): an AA signs authorization validation
// requests and authorization responses (TS 102 941 V2.2.1 Table B.6, bits 2 and 3: 0130), an
// EA signs authorization validation responses, enrolment responses and CA certificate
// requests (bits 4 to 6: 010E).
enum class AuthorityKind { aa, ea };
void authority_fields(Certificate& ca, const std::string& name, const PublicKey& encryption, AuthorityKind kind) {
ca->toBeSigned.id.present = Vanetza_Security_CertificateId_PR_name;
OCTET_STRING_fromBuf(&ca->toBeSigned.id.choice.name, name.data(), name.size());
issue_permissions(ca, false);
ca.add_app_permission(aid::SCR, kind == AuthorityKind::aa ? ByteBuffer {0x01, 0x30} : ByteBuffer {0x01, 0x0e});
auto* key = v3::asn1::allocate<v3::asn1::PublicEncryptionKey>();
key->supportedSymmAlg = Vanetza_Security_SymmAlgorithm_aes128Ccm;
key->publicKey.present = Vanetza_Security_BasePublicEncryptionKey_PR_eciesNistP256;
ecdsa256::PublicKey legacy;
std::copy(encryption.x.begin(), encryption.x.end(), legacy.x.begin());
std::copy(encryption.y.begin(), encryption.y.end(), legacy.y.begin());
assign_compressed_point visitor(&key->publicKey.choice.eciesNistP256);
boost::apply_visitor(visitor, compress_public_key(legacy));
ca->toBeSigned.encryptionKey = key;
}
} // namespace
TrustDomain::KeyMaterial TrustDomain::fresh_key() const {
const auto pair = backend_.generate_key_pair();
KeyMaterial material;
material.priv.type = KeyType::NistP256;
material.priv.key.assign(pair.private_key.key.begin(), pair.private_key.key.end());
material.pub.type = KeyType::NistP256;
material.pub.compression = KeyCompression::NoCompression;
material.pub.x.assign(pair.public_key.x.begin(), pair.public_key.x.end());
material.pub.y.assign(pair.public_key.y.begin(), pair.public_key.y.end());
return material;
}
void TrustDomain::sign(Certificate& subject, const Certificate* issuer, const PrivateKey& issuer_key) const {
if (issuer) {
subject->issuer.present = Vanetza_Security_IssuerIdentifier_PR_sha256AndDigest;
const auto digest = issuer->calculate_digest();
OCTET_STRING_fromBuf(&subject->issuer.choice.sha256AndDigest,
reinterpret_cast<const char*>(digest->data()), digest->size());
} else {
subject->issuer.present = Vanetza_Security_IssuerIdentifier_PR_self;
subject->issuer.choice.self = Vanetza_Security_HashAlgorithm_sha256;
}
// IEEE 1609.2 clause 5.3.1: Hash(Hash(COER(toBeSigned)) || Hash(COER(issuer certificate) or ""))
const ByteBuffer tbs = vanetza::asn1::encode_oer(asn_DEF_Vanetza_Security_ToBeSignedCertificate, &subject->toBeSigned);
ByteBuffer input = backend_.calculate_hash(HashAlgorithm::SHA256, tbs);
const ByteBuffer issuer_hash = backend_.calculate_hash(HashAlgorithm::SHA256, issuer ? issuer->encode() : ByteBuffer {});
input.insert(input.end(), issuer_hash.begin(), issuer_hash.end());
const auto signature = backend_.sign_digest(issuer_key, backend_.calculate_hash(HashAlgorithm::SHA256, input));
EcdsaSignature ecdsa;
ecdsa.R = X_Coordinate_Only {signature.r}; // canonical x-only r
ecdsa.s = signature.s;
subject.set_signature(ecdsa);
}
bool TrustDomain::verify_chain_signature(const Certificate& subject, const Certificate& issuer) const {
const auto signature = v3::get_signature(*subject.content());
const auto public_key = v3::get_public_key(*issuer.content());
if (!signature || !public_key) return false;
const ByteBuffer tbs = vanetza::asn1::encode_oer(asn_DEF_Vanetza_Security_ToBeSignedCertificate, &subject->toBeSigned);
ByteBuffer input = backend_.calculate_hash(HashAlgorithm::SHA256, tbs);
const ByteBuffer issuer_hash = backend_.calculate_hash(HashAlgorithm::SHA256,
subject.issuer_is_self() ? ByteBuffer {} : issuer.encode());
input.insert(input.end(), issuer_hash.begin(), issuer_hash.end());
return backend_.verify_digest(*public_key, backend_.calculate_hash(HashAlgorithm::SHA256, input), *signature);
}
TrustDomain::TrustDomain(Backend& backend, Clock::time_point now) : backend_(backend) {
const auto start = now - std::chrono::hours(1);
// Root CA (clause 7.2.3): self-signed, name, issuing permissions.
auto root_key = fresh_key();
root.key = root_key.priv;
common_fields(root.certificate, root_key.pub, start, 4, Vanetza_Security_Duration_PR_years);
root_fields(root.certificate, "vanetza-idf test root");
sign(root.certificate, nullptr, root.key);
// Authorization authority (clause 7.2.4): issued by the root, with an encryption key.
auto aa_key = fresh_key();
auto aa_enc = fresh_key();
aa.key = aa_key.priv;
aa_encryption_key = aa_enc.priv;
common_fields(aa.certificate, aa_key.pub, start, 3, Vanetza_Security_Duration_PR_years);
authority_fields(aa.certificate, "vanetza-idf test AA", aa_enc.pub, AuthorityKind::aa);
sign(aa.certificate, &root.certificate, root.key);
// Enrolment authority (clause 7.2.4): issued by the root, with an encryption key.
auto ea_key = fresh_key();
auto ea_enc = fresh_key();
ea.key = ea_key.priv;
ea_encryption_key = ea_enc.priv;
common_fields(ea.certificate, ea_key.pub, start, 3, Vanetza_Security_Duration_PR_years);
authority_fields(ea.certificate, "vanetza-idf test EA", ea_enc.pub, AuthorityKind::ea);
sign(ea.certificate, &root.certificate, root.key);
}
Credential TrustDomain::issue_ticket(const Credential& authority, const Permissions& permissions,
Clock::time_point start, unsigned hours) const {
Credential ticket;
auto ticket_key = fresh_key();
ticket.key = ticket_key.priv;
common_fields(ticket.certificate, ticket_key.pub, start, hours, Vanetza_Security_Duration_PR_hours);
ticket.certificate->toBeSigned.id.present = Vanetza_Security_CertificateId_PR_none;
for (const auto& permission : permissions) ticket.certificate.add_app_permission(permission.first, permission.second);
sign(ticket.certificate, &authority.certificate, authority.key);
return ticket;
}
Credential TrustDomain::issue_ticket(const Credential& authority, const Permissions& permissions,
Clock::time_point start, unsigned hours, const CircularRegion& region) const {
Credential ticket;
auto ticket_key = fresh_key();
ticket.key = ticket_key.priv;
common_fields(ticket.certificate, ticket_key.pub, start, hours, Vanetza_Security_Duration_PR_hours);
ticket.certificate->toBeSigned.id.present = Vanetza_Security_CertificateId_PR_none;
for (const auto& permission : permissions) ticket.certificate.add_app_permission(permission.first, permission.second);
auto* geographic = vanetza::asn1::allocate<Vanetza_Security_GeographicRegion_t>();
geographic->present = Vanetza_Security_GeographicRegion_PR_circularRegion;
geographic->choice.circularRegion.center.latitude = region.latitude;
geographic->choice.circularRegion.center.longitude = region.longitude;
geographic->choice.circularRegion.radius = region.radius_m;
ticket.certificate->toBeSigned.region = geographic;
sign(ticket.certificate, &authority.certificate, authority.key);
return ticket;
}
Credential TrustDomain::issue_authority(const std::string& name, Clock::time_point start) const {
Credential authority;
auto key = fresh_key();
auto enc = fresh_key();
authority.key = key.priv;
common_fields(authority.certificate, key.pub, start, 3, Vanetza_Security_Duration_PR_years);
authority_fields(authority.certificate, name, enc.pub, AuthorityKind::aa);
sign(authority.certificate, &root.certificate, root.key);
return authority;
}
// A subordinate CA may issue what its issuer allows through it: every group of the issuer
// whose chain-length window reaches two certificates down (IEEE Std 1609.2 6.4.28) and
// whose eeType admits authorization certificates is copied as a group with the defaults
// (chain length 1, app); the issuer's region, if any, is inherited (6.4.17: no part of
// the subordinate's region may lie outside the issuer's; the same region is within).
void derive_from_issuer(Certificate& ca, const Certificate& issuer) {
if (const auto* groups = issuer->toBeSigned.certIssuePermissions) {
for (int i = 0; i < groups->list.count; ++i) {
const auto* group = groups->list.array[i];
if (!group) continue;
const long min = group->minChainLength ? *group->minChainLength : 1;
const long range = group->chainLengthRange;
const bool reaches = min <= 2 && (range < 0 || min + range >= 2);
const bool app = !group->eeType || group->eeType->size == 0 || (group->eeType->buf[0] & 0x80);
if (!reaches || !app) continue;
auto* derived = v3::asn1::allocate<v3::asn1::PsidGroupPermissions>();
if (group->subjectPermissions.present == Vanetza_Security_SubjectPermissions_PR_all) {
derived->subjectPermissions.present = Vanetza_Security_SubjectPermissions_PR_all;
} else if (group->subjectPermissions.present == Vanetza_Security_SubjectPermissions_PR_explicit) {
derived->subjectPermissions.present = Vanetza_Security_SubjectPermissions_PR_explicit;
const auto& ranges = group->subjectPermissions.choice.Explicit.list;
for (int k = 0; k < ranges.count; ++k) {
if (!ranges.array[k]) continue;
auto* range = static_cast<v3::asn1::PsidSspRange*>(vanetza::asn1::copy(asn_DEF_Vanetza_Security_PsidSspRange, ranges.array[k]));
ASN_SEQUENCE_ADD(&derived->subjectPermissions.choice.Explicit, range);
}
} else {
vanetza::asn1::free(asn_DEF_Vanetza_Security_PsidGroupPermissions, derived);
continue;
}
ca.add_cert_issue_permission(derived);
}
}
if (const auto* region = issuer->toBeSigned.region) {
ca->toBeSigned.region = static_cast<Vanetza_Security_GeographicRegion_t*>(vanetza::asn1::copy(asn_DEF_Vanetza_Security_GeographicRegion, region));
}
}
Credential TrustDomain::issue_authority(const Credential& issuer, const std::string& name, Clock::time_point start,
unsigned years, PrivateKey* encryption_key) const {
Credential authority;
auto key = fresh_key();
auto enc = fresh_key();
authority.key = key.priv;
if (encryption_key) *encryption_key = enc.priv;
common_fields(authority.certificate, key.pub, start, years, Vanetza_Security_Duration_PR_years);
// clause 7.2.4 fields as for the lab AA, but the issuing permissions and the region come
// from the issuer rather than from the lab profile
authority.certificate->toBeSigned.id.present = Vanetza_Security_CertificateId_PR_name;
OCTET_STRING_fromBuf(&authority.certificate->toBeSigned.id.choice.name, name.data(), name.size());
derive_from_issuer(authority.certificate, issuer.certificate);
authority.certificate.add_app_permission(aid::SCR, {0x01, 0x30});
auto* enc_key = v3::asn1::allocate<v3::asn1::PublicEncryptionKey>();
enc_key->supportedSymmAlg = Vanetza_Security_SymmAlgorithm_aes128Ccm;
enc_key->publicKey.present = Vanetza_Security_BasePublicEncryptionKey_PR_eciesNistP256;
ecdsa256::PublicKey legacy;
std::copy(enc.pub.x.begin(), enc.pub.x.end(), legacy.x.begin());
std::copy(enc.pub.y.begin(), enc.pub.y.end(), legacy.y.begin());
assign_compressed_point visitor(&enc_key->publicKey.choice.eciesNistP256);
boost::apply_visitor(visitor, compress_public_key(legacy));
authority.certificate->toBeSigned.encryptionKey = enc_key;
sign(authority.certificate, &issuer.certificate, issuer.key);
return authority;
}
Credential TrustDomain::issue_ticket(const Credential& authority, const Permissions& permissions, Clock::time_point start,
unsigned hours, const Vanetza_Security_GeographicRegion_t* region) const {
Credential ticket;
auto ticket_key = fresh_key();
ticket.key = ticket_key.priv;
common_fields(ticket.certificate, ticket_key.pub, start, hours, Vanetza_Security_Duration_PR_hours);
ticket.certificate->toBeSigned.id.present = Vanetza_Security_CertificateId_PR_none;
for (const auto& permission : permissions) ticket.certificate.add_app_permission(permission.first, permission.second);
if (region) ticket.certificate->toBeSigned.region = static_cast<Vanetza_Security_GeographicRegion_t*>(vanetza::asn1::copy(asn_DEF_Vanetza_Security_GeographicRegion, region));
sign(ticket.certificate, &authority.certificate, authority.key);
return ticket;
}
Certificate TrustDomain::issue_root(const PrivateKey& key, const PublicKey& verification, const std::string& name,
Clock::time_point start, unsigned years) const {
Certificate certificate;
common_fields(certificate, verification, start, years, Vanetza_Security_Duration_PR_years);
root_fields(certificate, name);
sign(certificate, nullptr, key);
return certificate;
}
Certificate TrustDomain::issue_root_like(const PrivateKey& key, const PublicKey& verification, const std::string& name,
Clock::time_point start, unsigned years, const Certificate& profile) const {
Certificate certificate;
common_fields(certificate, verification, start, years, Vanetza_Security_Duration_PR_years);
certificate->toBeSigned.id.present = Vanetza_Security_CertificateId_PR_name;
OCTET_STRING_fromBuf(&certificate->toBeSigned.id.choice.name, name.data(), name.size());
// the profile's permissions and region, deep-copied; key, name and validity are ours
if (profile->toBeSigned.appPermissions)
certificate->toBeSigned.appPermissions = static_cast<v3::asn1::SequenceOfPsidSsp*>(
vanetza::asn1::copy(asn_DEF_Vanetza_Security_SequenceOfPsidSsp, profile->toBeSigned.appPermissions));
if (profile->toBeSigned.certIssuePermissions)
certificate->toBeSigned.certIssuePermissions = static_cast<v3::asn1::SequenceOfPsidGroupPermissions*>(
vanetza::asn1::copy(asn_DEF_Vanetza_Security_SequenceOfPsidGroupPermissions, profile->toBeSigned.certIssuePermissions));
if (profile->toBeSigned.region)
certificate->toBeSigned.region = static_cast<Vanetza_Security_GeographicRegion_t*>(
vanetza::asn1::copy(asn_DEF_Vanetza_Security_GeographicRegion, profile->toBeSigned.region));
sign(certificate, nullptr, key);
return certificate;
}
Certificate TrustDomain::issue_ticket_for(const PublicKey& verification, const Permissions& permissions,
Clock::time_point start, unsigned hours) const {
return issue_ticket_for(aa, verification, permissions, start, hours);
}
Certificate TrustDomain::issue_ticket_for(const Credential& authority, const PublicKey& verification, const Permissions& permissions,
Clock::time_point start, unsigned hours) const {
Certificate ticket;
common_fields(ticket, verification, start, hours, Vanetza_Security_Duration_PR_hours);
ticket->toBeSigned.id.present = Vanetza_Security_CertificateId_PR_none; // clause 7.2.1
for (const auto& permission : permissions) ticket.add_app_permission(permission.first, permission.second);
sign(ticket, &authority.certificate, authority.key);
return ticket;
}
Certificate TrustDomain::issue_credential_for(const PublicKey& verification, const std::string& name,
Clock::time_point start, unsigned hours) const {
return issue_credential_for(ea, verification, name, start, hours);
}
Certificate TrustDomain::issue_credential_for(const Credential& ea_issuer, const PublicKey& verification, const std::string& name,
Clock::time_point start, unsigned hours) const {
Certificate credential;
common_fields(credential, verification, start, hours, Vanetza_Security_Duration_PR_hours);
credential->toBeSigned.id.present = Vanetza_Security_CertificateId_PR_name; // clause 7.2.2
OCTET_STRING_fromBuf(&credential->toBeSigned.id.choice.name, name.data(), name.size());
credential.add_app_permission(aid::SCR, {0x01, 0xc0});
sign(credential, &ea_issuer.certificate, ea_issuer.key);
return credential;
}
Credential TrustDomain::issue_ticket(const Permissions& permissions, Clock::time_point start, unsigned hours) const {
Credential ticket;
auto ticket_key = fresh_key();
ticket.key = ticket_key.priv;
common_fields(ticket.certificate, ticket_key.pub, start, hours, Vanetza_Security_Duration_PR_hours);
ticket.certificate->toBeSigned.id.present = Vanetza_Security_CertificateId_PR_none; // clause 7.2.1
for (const auto& permission : permissions) ticket.certificate.add_app_permission(permission.first, permission.second);
sign(ticket.certificate, &aa.certificate, aa.key);
return ticket;
}
} // namespace vidf_test
@@ -0,0 +1,110 @@
#pragma once
// Isolated TEST trust domain: a root CA, an authorization authority and
// authorization tickets generated on the fly with the host OpenSSL backend.
// Certificates follow TS 103 097 V2.2.1 clause 6 and clauses 7.2.1/7.2.3/7.2.4
// and carry IEEE Std 1609.2 clause 5.3.1 signatures (Hash(Hash(toBeSigned) ||
// Hash(issuer certificate)), empty string for self-signed). The CA permissions
// are shaped like the EU CCMS CPOC Protocol Release 3.0 root profile (chain
// length 2 with eeType app+enrol at the root, IEEE Std 1609.2 clause 6.4.28)
// with the Security Management SSPs of TS 102 941 V2.2.1 Tables B.3/B.6. Keys
// and certificates never leave the test process; nothing here is a production
// PKI (vidf_issue reuses the building blocks for a lab chain under a real root).
#include <vanetza/common/clock.hpp>
#include <vanetza/common/its_aid.hpp>
#include <vanetza/security/backend.hpp>
#include <vanetza/security/private_key.hpp>
#include <vanetza/security/v3/certificate.hpp>
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
namespace vidf_test {
struct Credential {
vanetza::security::v3::Certificate certificate;
vanetza::security::PrivateKey key;
};
class TrustDomain {
public:
using Permissions = std::vector<std::pair<vanetza::ItsAid, vanetza::ByteBuffer>>;
/// root and AA valid from now - 1 h; both may issue CA, DEN, VRU and GN-MGMT
TrustDomain(vanetza::security::Backend&, vanetza::Clock::time_point now);
/// authorization ticket (clause 7.2.1) issued by the AA
Credential issue_ticket(const Permissions&, vanetza::Clock::time_point start, unsigned hours) const;
/// the same, issued by another authority of this domain (see issue_authority)
Credential issue_ticket(const Credential& authority, const Permissions&, vanetza::Clock::time_point start,
unsigned hours) const;
/// IEEE Std 1609.2 CircularRegion (6.4.19): centre in 1/10 microdegrees, radius in metres
struct CircularRegion { std::int32_t latitude; std::int32_t longitude; std::uint16_t radius_m; };
/// the same ticket restricted to a circular region (TS 103 097 clause 7.2.1 allows region)
Credential issue_ticket(const Credential& authority, const Permissions&, vanetza::Clock::time_point start,
unsigned hours, const CircularRegion& region) const;
/// a further subordinate CA (clause 7.2.4) under the root, e.g. the test-system side authority
Credential issue_authority(const std::string& name, vanetza::Clock::time_point start) const;
/// a subordinate CA (clause 7.2.4) under any issuer, e.g. an AA under an external root (vidf_issue):
/// its certIssuePermissions are the issuer's groups that reach two certificates down (IEEE Std 1609.2
/// 6.4.28) with the defaults, its region the issuer's (6.4.17). A fresh ECIES encryption key pair is
/// generated and its public half embedded in the certificate (clause 7.2.4); when encryption_key is
/// given, the private half is returned through it so a caller can actually decrypt requests addressed
/// to this authority (TS 102 941 clause 6.2.3) instead of the key being generated and discarded.
Credential issue_authority(const Credential& issuer, const std::string& name, vanetza::Clock::time_point start,
unsigned years, vanetza::security::PrivateKey* encryption_key = nullptr) const;
/// the ticket with an explicit GeographicRegion copied in (nullptr: none), e.g. the issuer's own region
Credential issue_ticket(const Credential& authority, const Permissions&, vanetza::Clock::time_point start,
unsigned hours, const Vanetza_Security_GeographicRegion_t* region) const;
/// a self-signed root (clause 7.2.3) from an existing key, e.g. a project root certificate (vidf_issue)
vanetza::security::v3::Certificate issue_root(const vanetza::security::PrivateKey& key,
const vanetza::security::PublicKey& verification,
const std::string& name, vanetza::Clock::time_point start,
unsigned years) const;
/// a self-signed root with the appPermissions, certIssuePermissions and region of another root
/// certificate (a lab twin of a real root for rehearsals, vidf_issue root --like)
vanetza::security::v3::Certificate issue_root_like(const vanetza::security::PrivateKey& key,
const vanetza::security::PublicKey& verification,
const std::string& name, vanetza::Clock::time_point start,
unsigned years,
const vanetza::security::v3::Certificate& profile) const;
/// AT for a verification key the station generated itself (TS 102 941 authorization); no private key.
/// Signed by this domain's own AA fixture.
vanetza::security::v3::Certificate issue_ticket_for(const vanetza::security::PublicKey& verification,
const Permissions&, vanetza::Clock::time_point start,
unsigned hours) const;
/// the same, signed by an explicit AA (e.g. one loaded from files, vidf_issue aa-respond)
vanetza::security::v3::Certificate issue_ticket_for(const Credential& authority, const vanetza::security::PublicKey& verification,
const Permissions&, vanetza::Clock::time_point start,
unsigned hours) const;
/// enrolment credential (clause 7.2.2) issued by the EA for a station verification key.
/// Signed by this domain's own EA fixture.
vanetza::security::v3::Certificate issue_credential_for(const vanetza::security::PublicKey& verification,
const std::string& name, vanetza::Clock::time_point start,
unsigned hours) const;
/// the same, signed by an explicit EA (e.g. one loaded from files, vidf_issue ea-respond)
vanetza::security::v3::Certificate issue_credential_for(const Credential& ea, const vanetza::security::PublicKey& verification,
const std::string& name, vanetza::Clock::time_point start,
unsigned hours) const;
/// IEEE 1609.2 clause 5.3.1 check of a certificate signature against its issuer (or itself)
bool verify_chain_signature(const vanetza::security::v3::Certificate& subject,
const vanetza::security::v3::Certificate& issuer) const;
Credential root;
Credential aa;
Credential ea; // enrolment authority (clause 7.2.4), issued by the root
vanetza::security::PrivateKey aa_encryption_key; // private part of the AA encryptionKey
vanetza::security::PrivateKey ea_encryption_key; // private part of the EA encryptionKey
// Building blocks, public so tests can construct deliberately wrong material (forged signatures).
struct KeyMaterial { vanetza::security::PrivateKey priv; vanetza::security::PublicKey pub; };
KeyMaterial fresh_key() const; // NIST P-256 from Backend::generate_key_pair
/// IEEE 1609.2 clause 5.3.1 certificate signature with the given key (issuer nullptr: self-signed)
void sign(vanetza::security::v3::Certificate& subject, const vanetza::security::v3::Certificate* issuer,
const vanetza::security::PrivateKey& issuer_key) const;
private:
vanetza::security::Backend& backend_;
};
} // namespace vidf_test
@@ -0,0 +1,89 @@
// Writes an ISOLATED TEST trust domain in the file layout the ETSI ITS test
// framework's certificate loader reads (ccsrc/Protocols/Security/
// certificates_loader.cc): <name>.oer (COER certificate), <name>.vkey (raw
// private signing key), <name>.ekey (raw private encryption key, authorities
// only) and index.lst ("<HashedId8 hex> <name>.oer" per line).
//
// vidf_test_pool <directory> [<validity hours for the tickets, default 24>]
//
// Names follow the ATS defaults (LibItsSecurity_TypesAndValues.ttcn /
// LibItsSecurity_Pixits.ttcn): the IUT chain CERT_IUT_A_RCA -> CERT_IUT_A_AA
// -> CERT_IUT_A_AT and the test-system chain CERT_TS_A_AA -> CERT_TS_A_AT, CERT_TS_B_AT (region)
// under the same root. Tickets carry CA, DEN, GN-MGMT and VRU permissions
// and start one minute before generation (TC_SEC_ITSS_SND_GENMSG_05_BV expects
// the start within five minutes of the current time). This is not a PKI.
#include "test_backend.hpp"
#include "test_trust_domain.hpp"
#include <vanetza_idf/its_time.hpp>
#include <vanetza/common/clock.hpp>
#include <vanetza/security/v3/certificate.hpp>
#include <chrono>
#include <cstdio>
#include <ctime>
#include <fstream>
#include <string>
#include <vector>
namespace {
using namespace vanetza;
using vanetza::security::v3::Certificate;
// ITS time (TAI since 2004-01-01T00:00:00Z) from the library's tested conversion.
Clock::time_point its_now() {
return Clock::time_point(vanetza_idf::its_time::since_epoch(std::chrono::system_clock::now()));
}
bool write(const std::string& path, const ByteBuffer& bytes) {
std::ofstream out(path, std::ios::binary);
out.write(reinterpret_cast<const char*>(bytes.data()), bytes.size());
return static_cast<bool>(out);
}
std::string hex(const vanetza::security::HashedId8& id) {
static const char* digits = "0123456789ABCDEF";
std::string s;
for (auto b : id) { s += digits[b >> 4]; s += digits[b & 15]; }
return s;
}
}
int main(int argc, char** argv) {
if (argc < 2) { std::fprintf(stderr, "usage: vidf_test_pool <directory> [ticket hours]\n"); return 2; }
const std::string dir = argv[1];
const unsigned hours = argc > 2 ? static_cast<unsigned>(std::atoi(argv[2])) : 24;
const auto now = its_now();
vidf_test::TestBackend backend;
vidf_test::TrustDomain domain(backend, now);
const vidf_test::TrustDomain::Permissions permissions {
{aid::CA, {0x01, 0xff, 0xfc}}, {aid::DEN, {0x01, 0xff, 0xff, 0xff}}, {aid::GN_MGMT, {}}, {aid::VRU, {0x01}}};
const auto start = now - std::chrono::minutes(1);
const auto iut_at = domain.issue_ticket(permissions, start, hours);
const auto ts_aa = domain.issue_authority("vanetza-idf test-system AA", now - std::chrono::hours(1));
const auto ts_at = domain.issue_ticket(ts_aa, permissions, start, hours);
// PX_AT_CERTIFICATE of the receiving-side cases (DENM_02_BV_XX: "certificate containing region
// restriction"): a 5 km circle around the SUT position hil_sut.cpp uses (52.0 N, 13.0 E).
const auto ts_b_at = domain.issue_ticket(ts_aa, permissions, start, hours,
vidf_test::TrustDomain::CircularRegion {520000000, 130000000, 5000});
struct Entry { const char* name; const Certificate* certificate; const vanetza::security::PrivateKey* key; };
const std::vector<Entry> entries {
{"CERT_IUT_A_RCA", &domain.root.certificate, &domain.root.key},
{"CERT_IUT_A_AA", &domain.aa.certificate, &domain.aa.key},
{"CERT_IUT_A_AT", &iut_at.certificate, &iut_at.key},
{"CERT_TS_A_AA", &ts_aa.certificate, &ts_aa.key},
{"CERT_TS_A_AT", &ts_at.certificate, &ts_at.key},
{"CERT_TS_B_AT", &ts_b_at.certificate, &ts_b_at.key},
};
std::string index;
for (const auto& entry : entries) {
const auto digest = entry.certificate->calculate_digest();
if (!digest || !write(dir + "/" + entry.name + ".oer", entry.certificate->encode()) ||
!write(dir + "/" + entry.name + ".vkey", entry.key->key)) {
std::fprintf(stderr, "cannot write %s\n", entry.name);
return 1;
}
index += hex(*digest) + " " + entry.name + ".oer\n";
}
std::ofstream(dir + "/index.lst") << index;
std::printf("%s", index.c_str());
return 0;
}
@@ -0,0 +1,96 @@
* text=auto !eol svneol=native#text/plain
*.gitattributes text svneol=native#text/plain
# Scriptish formats
*.bat text svneol=native#text/plain
*.bsh text svneol=native#text/x-beanshell
*.cgi text svneol=native#text/plain
*.cmd text svneol=native#text/plain
*.js text svneol=native#text/javascript
*.php text svneol=native#text/x-php
*.pl text svneol=native#text/x-perl
*.pm text svneol=native#text/x-perl
*.py text svneol=native#text/x-python
*.sh eol=lf svneol=LF#text/x-sh
configure eol=lf svneol=LF#text/x-sh
# Image formats
*.bmp binary svneol=unset#image/bmp
*.gif binary svneol=unset#image/gif
*.ico binary svneol=unset#image/ico
*.jpeg binary svneol=unset#image/jpeg
*.jpg binary svneol=unset#image/jpeg
*.png binary svneol=unset#image/png
*.tif binary svneol=unset#image/tiff
*.tiff binary svneol=unset#image/tiff
*.svg text svneol=native#image/svg%2Bxml
# Data formats
*.pdf binary svneol=unset#application/pdf
*.avi binary svneol=unset#video/avi
*.doc binary svneol=unset#application/msword
*.dsp text svneol=crlf#text/plain
*.dsw text svneol=crlf#text/plain
*.eps binary svneol=unset#application/postscript
*.gz binary svneol=unset#application/gzip
*.mov binary svneol=unset#video/quicktime
*.mp3 binary svneol=unset#audio/mpeg
*.ppt binary svneol=unset#application/vnd.ms-powerpoint
*.ps binary svneol=unset#application/postscript
*.psd binary svneol=unset#application/photoshop
*.rdf binary svneol=unset#text/rdf
*.rss text svneol=unset#text/xml
*.rtf binary svneol=unset#text/rtf
*.sln text svneol=native#text/plain
*.swf binary svneol=unset#application/x-shockwave-flash
*.tgz binary svneol=unset#application/gzip
*.vcproj text svneol=native#text/xml
*.vcxproj text svneol=native#text/xml
*.vsprops text svneol=native#text/xml
*.wav binary svneol=unset#audio/wav
*.xls binary svneol=unset#application/vnd.ms-excel
*.zip binary svneol=unset#application/zip
# Text formats
.htaccess text svneol=native#text/plain
*.bbk text svneol=native#text/xml
*.cmake text svneol=native#text/plain
*.css text svneol=native#text/css
*.dtd text svneol=native#text/xml
*.htm text svneol=native#text/html
*.html text svneol=native#text/html
*.ini text svneol=native#text/plain
*.log text svneol=native#text/plain
*.mak text svneol=native#text/plain
*.qbk text svneol=native#text/plain
*.rst text svneol=native#text/plain
*.sql text svneol=native#text/x-sql
*.txt text svneol=native#text/plain
*.xhtml text svneol=native#text/xhtml%2Bxml
*.xml text svneol=native#text/xml
*.xsd text svneol=native#text/xml
*.xsl text svneol=native#text/xml
*.xslt text svneol=native#text/xml
*.xul text svneol=native#text/xul
*.yml text svneol=native#text/plain
boost-no-inspect text svneol=native#text/plain
CHANGES text svneol=native#text/plain
COPYING text svneol=native#text/plain
INSTALL text svneol=native#text/plain
Jamfile text svneol=native#text/plain
Jamroot text svneol=native#text/plain
Jamfile.v2 text svneol=native#text/plain
Jamrules text svneol=native#text/plain
Makefile* text svneol=native#text/plain
README text svneol=native#text/plain
TODO text svneol=native#text/plain
# Code formats
*.c text svneol=native#text/plain
*.cpp text svneol=native#text/plain
*.h text svneol=native#text/plain
*.hpp text svneol=native#text/plain
*.ipp text svneol=native#text/plain
*.tpp text svneol=native#text/plain
*.jam text svneol=native#text/plain
*.java text svneol=native#text/plain
@@ -0,0 +1,26 @@
# Copyright 2018 Glen Joseph Fernandes
# (glenjofe@gmail.com)
#
# Distributed under the Boost Software License, Version 1.0.
# (http://www.boost.org/LICENSE_1_0.txt)
cmake_minimum_required(VERSION 3.5...3.20)
project(boost_circular_buffer VERSION "${BOOST_SUPERPROJECT_VERSION}" LANGUAGES CXX)
add_library(boost_circular_buffer INTERFACE)
add_library(Boost::circular_buffer ALIAS boost_circular_buffer)
target_include_directories(boost_circular_buffer INTERFACE include)
target_link_libraries(boost_circular_buffer INTERFACE
Boost::assert
Boost::concept_check
Boost::config
Boost::core
Boost::move
Boost::static_assert
Boost::throw_exception
Boost::type_traits
)
@@ -0,0 +1,48 @@
# Copyright 2018 Peter Dimov
# Distributed under the Boost Software License, Version 1.0.
version: 1.0.{build}-{branch}
shallow_clone: true
branches:
only:
- master
- develop
environment:
matrix:
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
TOOLSET: msvc-9.0
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
TOOLSET: msvc-10.0
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
TOOLSET: msvc-11.0
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
TOOLSET: msvc-12.0
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
TOOLSET: msvc-14.0
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
TOOLSET: msvc-14.1
CXXSTD: 14,17
install:
- set BOOST_BRANCH=develop
- if "%APPVEYOR_REPO_BRANCH%" == "master" set BOOST_BRANCH=master
- cd ..
- git clone -b %BOOST_BRANCH% https://github.com/boostorg/boost.git boost
- cd boost
- git submodule update --init tools/build
- git submodule update --init libs/config
- git submodule update --init tools/boostdep
- xcopy /s /e /q %APPVEYOR_BUILD_FOLDER% libs\circular_buffer\
- python tools/boostdep/depinst/depinst.py circular_buffer
- cmd /c bootstrap
- b2 headers
build: off
test_script:
- PATH=%ADDPATH%%PATH%
- if not "%CXXSTD%" == "" set CXXSTD=cxxstd=%CXXSTD%
- b2 -j 3 libs/circular_buffer/test toolset=%TOOLSET% %CXXSTD%
@@ -0,0 +1,22 @@
# circular_buffer.idx index script file
# for Boost.circular_buffer Quickbook Doxygen documentation Auto-indexing forcircular_buffer library.
# Copyright (c) 2011 Paul A. Bristow
# Copyright (c) 2003 - 2008 Jan Gaspar
# boost-no-inspect
# Use, modification and distribution is subject to the Boost Software License, Version 1.0.
# (See accompanying file LICENSE_1_0.txt
# or copy at http://www.boost.org/LICENSE_1_0.txt)
# All header files, recursing down to include sub-folders.
!scan-path "boost/circular_buffer" ".*\.hpp" true
# All example source files, assuming no sub-folders.
!scan-path "libs/circular_buffer/example" ".*\.cpp"
@@ -0,0 +1,626 @@
[article Boost.Circular Buffer
[quickbook 1.6]
[id circular_buffer]
[copyright 2003-2013 Jan Gaspar]
[license
Distributed under the Boost Software License, Version 1.0.
(See accompanying file LICENSE_1_0.txt or copy at
[@http://www.boost.org/LICENSE_1_0.txt])
]
[authors [Gaspar, Jan]]
[source-mode c++]
]
[/ Links - by (most common) convention, prefixed with double underscore so not confused with other names.]
[def __alert [$./images/alert.png]] [/ Examples of your own images (in doc/html/images/ .]
[def __tip [$./images/tip.png]]
[/ If you provide a file type like .png, you will probably find that the file is missing in the pdf version.]
[/ This is because the default file type specified is .png in html, but .svg for pdf version.]
[/ Some links to external sources.]
[/ You often want to link more than once, so using a def ensures you always refer to the same location.]
[def __boost [@http://www.boost.org/ Boost]] [/Boost.org]
[def __boostroot [@boost: Boost root]] [/ Your boost root]
[/Note the custom boost root url schema for linking to files within the Boost distribution.]
[/Note It can't be used for images, nor for pdf, so not so useful.]
[/def __boostlicense [@http://www.boost.org/LICENSE_1_0.txt Boost License]]
[/ Or refer to your most recent version of Boost.]
[def __boostlicense [@boost:/LICENSE_1_0.txt Boost License]]
[def __boostbook [@http://www.boost.org/doc/html/boostbook.html BoostBook]]
[def __boostbook_docs [@http://www.boost.org/doc/libs/1_53_0/doc/html/boostbook.html BoostBook documentation]]
[def __quickbook [@http://www.boost.org/doc/tools/quickbook/index.html Quickbook]]
[def __quickbook_syntax [@http://www.boost.org/doc/libs/1_53_0/doc/html/quickbook/ref.html Quickbook Syntax Compendium]]
[def __docbook [@http://www.docbook.org/ DocBook]]
[def __doxygen [@http://www.doxygen.org/ Doxygen]]
[def __autoindex [@boost:/tools/auto_index/doc/html/index.html AutoIndex]]
[def __pdf [@http://www.adobe.com/products/acrobat/adobepdf.html PDF]]
[def __textpad [@http://www.textpad.com Textpad]]
[def __emacs [@http://www.gnu.org/software/emacs/ GNU emacs]]
[def __css [@http://en.wikipedia.org/wiki/Cascading_Style_Sheets Cascading Style Sheet]]
[def __intro [link circular_buffer.intro Introduction]] [/Link to a Quickbook section (see below).]
[def __docbook_params [@http://docbook.sourceforge.net/release/xsl/current/doc/ Docbook xsl:param format options]]
[def __cb [classref boost::circular_buffer circular_buffer]]
[def __cbso [classref boost::circular_buffer_space_optimized circular_buffer_space_optimized]]
[def __min_capacity [memberref boost::circular_buffer_space_optimized::min_capacity() min_capacity]]
[def __capacity_control [memberref boost::circular_buffer_space_optimized::capacity_control () capacity_control ]]
[def __debug_support [link circular_buffer.implementation.debug_support debug support]]
[include ../../../tools/auto_index/include/auto_index_helpers.qbk]
[/ Must be first included file!]
[note A printer-friendly PDF version of this manual is also available.]
[section:intro Introduction]
A Circular Buffer.
[h2 Description]
The term [@http://en.wikipedia.org/wiki/Circular_buffer circular buffer]
(also called a ['ring] or ['cyclic buffer])
refers to an area in memory which is used to store incoming data.
When the buffer is filled,
new data is written starting at the beginning of the buffer and overwriting the old.
[classref boost::circular_buffer] is a STL compliant container.
It is a kind of sequence similar to [@https://www.boost.org/sgi/stl/List.html std::list]
or [@https://www.boost.org/sgi/stl/Deque.html std::deque].
It supports random access iterators, constant time insert and erase operations
at the beginning or the end of the buffer and interoperability with std algorithms.
The __cb is especially designed to provide [*fixed capacity] storage.
When its capacity is exhausted, newly inserted elements will cause elements
to be overwritten, either at the beginning or end of the buffer
(depending on what insert operation is used).
The __cb only allocates memory when created,
when the capacity is adjusted explicitly,
or as necessary to accommodate resizing or assign operations.
[$../../libs/circular_buffer/doc/images/circular_buffer.png]
There is also a __cbso version available.
[$../../libs/circular_buffer/doc/images/space_optimized.png]
__cbso is an adaptation of the __cb
which [*does not allocate memory all at once when created],
instead it allocates memory as needed.
The predictive memory allocation is similar to typical `std::vector` implementation.
Memory is automatically freed as the size of the container decreases.
The memory allocation process of the space-optimized circular buffer.
The __min_capacity of the capacity controller represents
the minimal guaranteed amount of allocated memory.
The allocated memory will never drop under this value.
The default value of the `min_capacity` is set to 0.
The `min_capacity` can be set using the constructor parameter __capacity_control
or the function `set_capacity`.
The space-optimized version is, of course, a little slower.
[endsect] [/section:intro Introduction]
[section:example Circular_buffer example]
Here is a simple example to introduce the class __cb.
[import ../example/circular_buffer_example.cpp]
[circular_buffer_example_1]
This example shows construction, inserting elements, overwriting and popping.
[circular_buffer_example_2]
[/circular_buffer_example_output - there is no output for this example]
You can see the full example code at [@boost:libs/circular_buffer/example/circular_buffer_example.cpp circular_buffer_example.cpp].
The full annotated description is in the C++ Reference section.
[endsect] [/section:example circular_buffer example]
[section:rationale Rationale]
The basic motivation behind the __cb was to create a container which would [*work seamlessly with STL].
Additionally, the design of the __cb was guided by the following principles:
* Maximum ['efficiency] for envisaged applications.
* Suitable for ['general purpose use].
* The behaviour of the buffer as ['intuitive] as possible.
* Suitable for ['specialization] by means of adaptors. (The __cbso is such an example of the adaptor.)
* Easy to ['debug]. (See Debug Support for details.)
In order to achieve maximum efficiency, the __cb and __cbso store their elements in a
[*contiguous region of memory], which then enables:
* Use of fixed memory and no implicit or unexpected memory allocation.
* Fast constant-time insertion and removal of elements from the front and back.
* Fast constant-time random access of elements.
* Suitability for real-time and performance critical applications.
Possible applications of the circular buffer include:
* Storage of the ['most recently received samples], overwriting the oldest as new samples arrive.
* As an underlying container for a ['bounded buffer]
(see the Bounded Buffer example, code at [@boost:libs/circular_buffer/example/circular_buffer_bound_example.cpp circular_buffer_bound_example.cpp]).
* A kind of ['cache] storing a specified number of last inserted elements.
* Efficient fixed capacity ['FIFO (First In, First Out)],
* Efficient fixed capacity ['LIFO (Last In, First Out)] queue which removes the oldest (inserted as first) elements when full.
[endsect] [/section:rationale Rationale]
[section:implementation Implementation ]
The following paragraphs describe issues that had to be considered during the implementation of the circular_buffer:
[h3 Thread-Safety]
The thread-safety of the __cb is the same as the thread-safety of containers in most STL implementations.
This means the __cb is not fully thread-safe.
The thread-safety is guaranteed only in the sense that simultaneous accesses
to distinct instances of the __cb are safe,
and simultaneous read accesses to a shared __cb are safe.
If multiple threads access a single __cb,
and at least one of the threads may potentially write,
then the user is responsible for ensuring mutual exclusion between the threads during the container accesses.
The mutual exclusion between the threads can be achieved by wrapping
operations of the underlying __cb with a lock acquisition and release.
(See the Bounded Buffer example code at [@boost:libs/circular_buffer/example/circular_buffer_bound_example.cpp circular_buffer_bound_example.cpp])
[h3 Overwrite Operation]
Overwrite operation occurs when an element is inserted into a full __cb -
the old element is being overwritten by the new one.
There was a discussion what exactly "overwriting of an element" means during the formal review.
It may be either a destruction of the original element and
a consequent inplace construction of a new element
or it may be an assignment of a new element into an old one.
The __cb implements assignment because it is more effective.
From the point of business logic of a stored element,
the destruction/construction operation and assignment usually mean the same.
However, in very rare cases (if in any) they may differ.
If there is a requirement for elements to be destructed/constructed instead of being assigned,
consider implementing a wrapper of the element which would implement the assign operator,
and store the wrappers instead.
It is necessary to note that storing such wrappers has a drawback.
The destruction/construction will be invoked on every assignment of the wrapper -
not only when a wrapper is being overwritten (when the buffer is full)
but also when the stored wrappers are being shifted
(e.g. as a result of insertion into the middle of container).
[h3 Writing to a Full Buffer]
There are several options how to cope if a data source produces more data than can fit in the fixed-sized buffer:
* Inform the data source to wait until there is room in the buffer (e.g. by throwing an overflow exception).
* If the oldest data is the most important, ignore new data from the source until there is room in the buffer again.
* If the latest data is the most important, write over the oldest data.
* Let the producer to be responsible for checking the size of the buffer prior writing into it.
It is apparent that the __cb implements the third option.
But it may be less apparent it does not implement any other option -
especially the first two.
One can get an impression that the __cb should implement first three options
and offer a mechanism of choosing among them. This impression is wrong.
The __cb was designed and optimized to be circular
(which means overwriting the oldest data when full).
If such a controlling mechanism had been enabled,
it would just complicate the matters
and the usage of the __cb would be probably less straightforward.
Moreover, the first two options (and the fourth option as well)
do not require the buffer to be circular at all.
If there is a need for the first or second option, consider implementing an adaptor of e.g. std::vector.
In this case the __cb is not suitable for adapting, because,
contrary to std::vector, it bears an overhead for its circular behaviour.
[h3 Reading/Removing from an Empty Buffer]
When reading or removing an element from an empty buffer,
the buffer should be able to notify the data consumer
(e.g. by throwing underflow exception) that there are no elements stored in it.
The __cb does not implement such a behaviour for two reasons:
* It would introduce a performance overhead.
* No other std container implements it this way.
It is considered to be a bug to read or remove an element
(e.g. by calling [memberref boost::circular_buffer::front() front()]
or [memberref boost::circular_buffer::pop_back() pop_back()])
from an empty std container and from an empty __cb as well.
The data consumer has to test if the container is not empty before reading/removing from it by testing
[memberref boost::circular_buffer::empty empty()].
However, when reading from the __cb,
there is an option to rely on the [memberref boost::circular_buffer::at() at()]
method which throws an exception when the index is out of range.
[h3 Iterator Invalidation]
An iterator is usually considered to be invalidated if an element,
the iterator pointed to, had been removed or overwritten by an another element.
This definition is enforced by the Debug Support and is documented for every method.
However, some applications utilizing __cb may require less strict definition:
an iterator is invalid only if it points to an uninitialized memory.
Consider following example:
[import ../example/circular_buffer_iter_example.cpp]
[circular_buffer_iter_example_1]
The iterator does not point to the original element any more
(and is considered to be invalid from the "strict" point of view)
but it still points to the same valid place in the memory.
This "soft" definition of iterator invalidation is supported by the __cb
but should be considered as an implementation detail rather than a full-fledged feature.
The rules when the iterator is still valid can be inferred from the code in
[@boost:libs/circular_buffer/test/soft_iterator_invalidation.cpp soft_iterator_invalidation.cpp].
[h3 Move emulation and rvalues]
Since Boost 1.54.0 support for move semantics was implemented using
the [@boost:libs/move/index.html Boost.Move] library.
If rvalue references are available __cb will use them, but if not it uses a close,
but imperfect emulation. On such compilers:
* Non-copyable objects can be stored in the containers.
They can be constructed in place using `emplace`, or if they support
Boost.Move, moved into place.
* The containers themselves are not movable.
* Argument forwarding is not perfect.
__cb will use rvalues and move emulations for value types only if move constructor and move assignment operator of the value type do not throw;
or if the value type has no copy constructor.
Some methods won't use move constructor for the value type at all, if the constructor throws. This is
required for data consistency and avoidance of situations, when aftrer an exception __cb
contains moved away objects along with the good ones.
See documentation for [@boost:libs/type_traits/doc/html/boost_typetraits/reference/is_copy_constructible.html `is_copy_constructible`], [@boost:libs/type_traits/doc/html/boost_typetraits/reference/is_nothrow_move_assignable.html `is_nothrow_move_assignable`] and [@boost:libs/type_traits/doc/html/boost_typetraits/reference/is_nothrow_move_constructible.html `is_nothrow_move_constructible`] type triats.
There you'll find information about how to make constructor of class noexcept and how to make a non-copyable
class in C++03 and C++98.
Performance of __cb will *greatly improve* if value type has noexcept move constructor and noexcept move assignment.
[h3 Exceptions of move_if_noexcept(T&)]
Reference documentation of the __cb contains notes like "Throws: See Exceptions of `move_if_noexcept(T&)`".
That note means the following: `move_if_noexcept(T& value)` does not throws exceptions at all, but it returns
`value` as rvalue reference only if class `T` have noexcept move constructor and noexcept move assignment operator;
or if it has no copy constructor. Otherwise `move_if_noexcept(T& value)` returns `value` as const reference.
This leads us to the following situation:
* If `value` has a noexcept move constructor and noexcept move assignment operator, then no exceptions will be thrown at all.
* If `value` has a throwing move constructor and some copy constructor, then method may throw exceptions of copy constructor.
* If `value` has no copy constructor, then method may throw exceptions of move constructor.
`move_if_noexcept(T&)` uses [@boost:libs/move/index.html Boost.Move], [@boost:libs/type_traits/doc/html/boost_typetraits/reference/is_copy_constructible.html `is_copy_constructible`], [@boost:libs/type_traits/doc/html/boost_typetraits/reference/is_nothrow_move_assignable.html `is_nothrow_move_assignable`] and [@boost:libs/type_traits/doc/html/boost_typetraits/reference/is_nothrow_move_constructible.html `is_nothrow_move_constructible`] type triats.
[h3 Caveats]
The __cb should not be used for storing pointers to dynamically allocated objects.
When a circular buffer becomes full, further insertion will overwrite the stored pointers
- resulting in a [*memory leak]. One recommend alternative is the use of smart pointers, for example
[@http://www.boost.org/doc/libs/1_53_0/libs/smart_ptr/smart_ptr.htm Boost Smart pointers].
[@http://en.wikipedia.org/wiki/Std::auto_ptr std::auto_ptr]
[caution Any container of `std::auto_ptr` is considered particularly hazardous.]
[tip Never create a circular buffer of `std::auto_ptr`.
Refer to Scott Meyers' excellent book Effective STL for a detailed discussion.
(Meyers S., Effective STL: 50 Specific Ways to Improve Your Use of the Standard Template Library.
Addison-Wesley, 2001.)
]
While internals of a __cb are circular, [*iterators are not].
Iterators of a __cb are only valid for the range `\[begin(), end()\]`,
so for example: iterators `(begin() - 1)` and `(end() + 1)` are both invalid.
[h3 Debug Support]
In order to help a programmer to avoid and find common bugs,
the __cb can be enabled to provide a kind of debug support.
When the debugging functionality is enabled, the __cb maintains a list of valid iterators.
As soon as any element gets destroyed all iterators pointing to this element
are removed from this list and explicitly invalidated (an invalidation flag is set).
The debug support also consists of many assertions (`BOOST_ASSERT` macros)
which ensure the __cb and its iterators are used in the correct manner at runtime.
In case an invalid iterator is used, the assertion will report an error.
The connection of explicit iterator invalidation and assertions
makes a very robust debug technique which catches most of the errors.
Moreover, the uninitialized memory allocated by __cb is filled with the value `0xcc` in the debug mode.
When debugging the code, this can help the programmer to recognize the initialized memory from the uninitialized.
For details refer the source code [@boost:boost/circular_buffer/debug.hpp circular_buffer/debug.hpp].
[caution Since the debugging code makes __cb and its iterators more interconnected, thread safety guarantees of __cb
are different when debug support is enabled. In addition to the container itself, all iterators tracked by the container
(including any copies thereof) must be protected from concurrent access. In particular, this includes copying, destroying or
obtaining iterators from the container, even if for read-only access.]
The debug support is disabled by default. To enable it, one has to define `BOOST_CB_ENABLE_DEBUG` macro with the value of 1
while compiling the code using __cb.
[h3 Compatibility with Interprocess library]
The __cb is compatible with the [@boost:libs/interprocess/index.html Boost.Interprocess]
[/ This should be in @boost:libs/interprocess/doc/index.html ]
library used for interprocess communication.
Considering that the circular_buffer's debug support relies on 'raw' pointers
(which is not permitted by the Interprocess library)
the code has to compiled with debug support disabled (i.e. with `BOOST_CB_ENABLE_DEBUG` macro not defined or defined to 0).
Not doing that will cause the compilation to fail.
[endsect] [/section:implementation Implementation ]
[section:examples More Examples]
[h3 Summing all the values in a circular buffer]
[import ../example/circular_buffer_sum_example.cpp]
[circular_buffer_sum_example_1]
[/circular_buffer_example_output - there is no output for this example]
The __cb has a capacity of three `int`.
Therefore, the size of the buffer will never exceed three.
The `std::accumulate` algorithm evaluates the sum of the stored elements.
The semantics of the __cb can be inferred from the assertions.
You can see the full example code at [@boost:libs/circular_buffer/example/circular_buffer_sum_example.cpp circular_buffer_sum_example.cpp].
[h3 Bounded Buffer Example]
The bounded buffer is normally used in a producer-consumer mode:
producer threads produce items and store them in the container
and consumer threads remove these items and process them.
The bounded buffer has to guarantee that
* producers do not insert items into the container when the container is full,
* consumers do not try to remove items when the container is empty,
* each produced item is consumed by exactly one consumer.
[import ../example/circular_buffer_bound_example.cpp]
[circular_buffer_bound_example_1]
[/ there is no output for this example]
The bounded_buffer relies on [@boost:/doc/html/thread.html Boost.Thread]
and [@boost:libs/bind/index.html Boost.Bind] libraries
and [@boost:libs/utility/call_traits.htm Boost.call_traits utility].
The [memberref boost::circular_buffer::push_front() push_front()]
method is called by the producer thread in order to insert a new item into the buffer.
The method locks the mutex and waits until there is a space for the new item.
(The mutex is unlocked during the waiting stage and has to be regained when the condition is met.)
If there is a space in the buffer available,
the execution continues and the method inserts the item at the end of the __cb.
Then it increments the number of unread items and unlocks the mutex
(in case an exception is thrown before the mutex is unlocked,
the mutex is unlocked automatically by the destructor of the scoped_lock).
At last the method notifies one of the consumer threads
waiting for a new item to be inserted into the buffer.
The [memberref boost::circular_buffer::pop_back() pop_back()]
method is called by the consumer thread in order to read the next item from the buffer.
The method locks the mutex and waits until there is an unread item in the buffer.
If there is at least one unread item,
the method decrements the number of unread items and reads the next item from the __cb.
Then it unlocks the mutex and notifies one of the producer threads
waiting for the buffer to free a space for the next item.
The `bounded buffer::pop_back()`
method [*does not remove the item] but the item is left
in the circular_buffer which then [*replaces it with a new one]
(inserted by a producer) when the circular_buffer is full.
This technique is more effective than removing the item
explicitly by calling the [memberref boost::circular_buffer::pop_back() circular_buffer::pop_back()]
method of the __cb.
This claim is based on the assumption that an assignment (replacement)
of a new item into an old one is more effective than a destruction
(removal) of an old item and a consequent inplace construction (insertion) of a new item.
For comparison of bounded buffers based on different containers compile and
run [@boost:libs/circular_buffer/test/bounded_buffer_comparison.cpp bounded_buffer_comparison.cpp].
The test should reveal the bounded buffer based on the __cb is most effective
closely followed by the `std::deque` based bounded buffer.
(In reality, the result may differ sometimes because the test
is always affected by external factors such as immediate CPU load.)
[import ../test/bounded_buffer_comparison.cpp]
You can see the full test code at [@boost:libs/circular_buffer/test/bounded_buffer_comparison.cpp bounded_buffer_comparison.cpp],
and an example of output is [bounded_buffer_comparison_output].
[endsect] [/section:examples More examples]
[section:headers Header Files]
The circular buffer library is defined in the file [@boost:boost/circular_buffer.hpp circular_buffer.hpp].
#include <boost/circular_buffer.hpp>
(There is also a forward declaration for the __cb
in the header file [@boost:boost/circular_buffer_fwd.hpp circular_buffer_fwd.hpp]).
The __cb is defined in the file [@boost:boost/circular_buffer/base.hpp base.hpp].
The __cbso is defined in the file [@boost:boost/circular_buffer/space_optimized.hpp space_optimized.hpp].
[endsect] [/section:headers Header Files]
[section:concepts Modelled Concepts]
[@https://www.boost.org/sgi/stl/RandomAccessContainer.html Random Access Container],
[@https://www.boost.org/sgi/stl/FrontInsertionSequence.html Front Insertion Sequence], and
[@https://www.boost.org/sgi/stl/BackInsertionSequence.html Back Insertion sequence]
[endsect] [/section:concepts Modelled Concepts]
[section:template_params Template Parameters]
[table:templ Template parameter requirements
[[parameter] [Requirements]]
[[T] [The type of the elements stored in the circular_buffer.
The T has to be [@boost:libs/utility/Assignable.html Assignable]
and [@boost:libs/utility/CopyConstructible.html CopyConstructible].
Moreover T has to be [@https://www.boost.org/sgi/stl/DefaultConstructible.html DefaultConstructible]
if supplied as a default parameter when invoking some of the circular_buffer's methods,
e.g. `insert(iterator pos, const value_type& item = value_type())`.
And [@https://www.boost.org/sgi/stl/EqualityComparable.html EqualityComparable]
and/or [@boost:libs/utility/LessThanComparable.html LessThanComparable]
if the circular_buffer will be compared with another container.]]
[[Alloc] [The allocator type used for all internal memory management.
The Alloc has to meet the allocator requirements imposed by STL.]]
]
[endsect] [/section:template_params Template Parameters]
[section:tickets Trac Tickets]
Report and view bugs and features by adding a ticket at [@https://svn.boost.org/trac/boost Boost.Trac].
Existing open tickets for this library alone can be viewed
[@https://svn.boost.org/trac/boost/query?status=assigned&status=new&status=reopened&component=circular_buffer&col=id&col=summary&col=status&col=owner&col=type&col=milestone&order=priority here].
Existing tickets for this library - including closed ones - can be viewed
[@https://svn.boost.org/trac/boost/query?status=assigned&status=closed&status=new&status=reopened&component=circular_buffer&col=id&col=summary&col=status&col=owner&col=type&col=milestone&order=priority here].
Type: Bugs
[@https://svn.boost.org/trac/boost/ticket/4100 #4100] Some boost classes have sizeof that depends on NDEBUG.
[@https://svn.boost.org/trac/boost/ticket/5362 #5362] circular_buffer does not compile with BOOST_NO_EXCEPTIONS.
[@https://svn.boost.org/trac/boost/ticket/6277 #6277] Checked iterators are not threadsafe.
[@https://svn.boost.org/trac/boost/ticket/6747 #6747] Circular_Buffer / Bounded_Buffer inside Template class problem.
[@https://svn.boost.org/trac/boost/ticket/7025 #7025] circular buffer reports warning: " type qualifiers ignored on function return type" while compile.
[@https://svn.boost.org/trac/boost/ticket/7950 #7950] Eliminate W4-warnings under VS2005.
[@https://svn.boost.org/trac/boost/ticket/8012 #8012] Inconsistency in `linearize()`.
[@https://svn.boost.org/trac/boost/ticket/8438 #8438] `vector` & __cb storage misbehave when using compiler optimizations.
Type: Feature Requests
[@https://svn.boost.org/trac/boost/ticket/5511 #5511] Documentation needs some improvement.
[@https://svn.boost.org/trac/boost/ticket/7888 #7888] circular_buffer should support move semantics.
Type: Patches
[@https://svn.boost.org/trac/boost/ticket/8032 #8032] Warning fixes in circular_buffer.
[endsect] [/section:tickets Trac Tickets]
[section:release Release Notes]
[h4 Boost 1.56]
* C++11 allocator model support implemented by Glen Fernandes.
[h4 Boost 1.55]
* Documentation refactored by Paul A. Bristow using Quickbook, Doxygen and Autoindexing.
* Rvalue references emulation added by Antony Polukhin using Boost.Move.
[h4 Boost 1.42]
* Added methods erase_begin(size_type) and erase_end(size_type) with constant complexity for such types of stored elements which do not need an explicit destruction e.g. int or double.
* Similarly changed implementation of the clear() method and the destructor so their complexity is now constant for such types of stored elements which do not require an explicit destruction (the complexity for other types remains linear).
[h4 Boost 1.37]
*Added new methods is_linearized() and rotate(const_iterator).
* Fixed bugs:
[@https://svn.boost.org/trac/boost/ticket/1987 #1987] Patch to make circular_buffer.hpp #includes absolute.
[@https://svn.boost.org/trac/boost/ticket/1852 #1852] Copy constructor does not copy capacity.
[h4 Boost 1.36]
* Changed behaviour of the circular_buffer(const allocator_type&) constructor.
Since this version the constructor does not allocate any memory and both capacity and size are set to zero.
* Fixed bug:
[@https://svn.boost.org/trac/boost/ticket/191 #1919] Default constructed circular buffer throws std::bad_alloc.
[h4 Boost 1.35]
* Initial release.
[endsect] [/section:release Release Notes]
[section:acknowledgements Acknowledgements]
Thomas Witt in 2002 produced a prototype called cyclic buffer.
The circular_buffer has a short history. Its first version was a std::deque adaptor.
This container was not very effective because of many reallocations when inserting/removing an element.
Thomas Wenish did a review of this version and
motivated me to create a circular buffer which allocates memory at once when created.
The second version adapted `std::vector` but it has been abandoned soon
because of limited control over iterator invalidation.
The current version is a full-fledged STL compliant container.
Pavel Vozenilek did a thorough review of this version and came with many good ideas and improvements.
The idea of the space optimized circular buffer has been introduced by Pavel Vozenilek.
Also, I would like to thank Howard Hinnant, Nigel Stewart and everyone
who participated at the formal review for valuable comments and ideas.
Paul A. Bristow refactored the documentation in 2013 to use the full power of Quickbook, Doxygen and Autoindexing.
[endsect] [/section:acknowledgements Acknowledgements]
[section:version_id Documentation Version Info]
Last edit to Quickbook file __FILENAME__ was at __TIME__ on __DATE__.
[tip This should appear on the pdf version
(but may be redundant on a html version where the last edit date is on the first (home) page).]
[warning Home page "Last revised" is GMT, not local time. Last edit date is local time.]
[/See also Adobe Reader pdf File Properties for creation date, and PDF producer, version and page count.]
[endsect] [/section:version_id Version Info]
[xinclude autodoc.xml] [/ Using Doxygen reference documentation.]
[/ The position of this in the Quickbook determines the location of the Doxygen references section.]
[/ Index(es) should be invoked in the main module, not within a section.]
'''
<index/>
'''
[/ circular_buffer.qbk
Copyright 2013 Paul A. Bristow.
Copyright 2003-2008 Jan Gaspar.
Distributed under the Boost Software License, Version 1.0.
(See accompanying file LICENSE_1_0.txt or copy at
http://www.boost.org/LICENSE_1_0.txt).
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,217 @@
# Boost.circular_buffer library documentation Jamfile.v2
#
# Copyright Paul A. Bristow 2013.
# Copyright Jan Gaspar 2003-2008.
# Use, modification and distribution is subject to
# the Boost Software License, Version 1.0.
# (See accompanying file LICENSE_1_0.txt
# or copy at http://www.boost.org/LICENSE_1_0.txt)
path-constant nav_images : html/images/ ; # png and svg images for home, next, note, tip...
path-constant images_location : html/images ; # location of my SVG and PNG images referenced by Quickbook.
path-constant pdf_images_location : .. ; # location of SVG and PNG images referenced by pdf.
path-constant here : . ; # location of /doc folder.
# echo "nav_images = " $(nav_images) ; # "nav_images = I:\boost-trunk\libs\circular_buffer\doc\html\images
# echo "images_location = " $(images_location) ; # images_location = I:\boost-trunk\libs\circular_buffer\doc\html\images
# echo "pdf_images_location = " $(pdf_images_location) #
import modules ;
using auto-index ;
using doxygen ; # Required if you want to use Doxygen.
using quickbook ;
doxygen autodoc
:
# List all the files individually (RECURSIVE=NO ).
[ glob ../../../boost/circular_buffer.hpp ]
[ glob ../../../boost/circular_buffer/base.hpp ]
[ glob ../../../boost/circular_buffer/space_optimized.hpp ]
:
# Pass some setting parameters to Doxygen.
<doxygen:param>WARNINGS=YES # Default NO, but useful to see warnings, especially in a logfile.
# It is also wise to to set a warnings logfile like this:
<doxygen:param>WARN_LOGFILE=AutoDoxywarnings.log # This may not be empty (usually not a good sign!), depending on options chosen.
# Much better to send message to a logfile than the default stderr.
# and make sure that there are no Doxygen errors or significant warnings in the log file.
<doxygen:param>RECURSIVE=NO # Search recursively down .hpp and .cpp subdirectories.
<doxygen:param>EXTRACT_ALL=NO
<doxygen:param>EXTRACT_PRIVATE=NO # NO means do not extract info about private member functions and data.
<doxygen:param>HIDE_UNDOC_MEMBERS=YES # Only show members that have some documentation like \param, \return ...
<doxygen:param>MACRO_EXPANSION=YES # YES will expand all macro names in the source code (default = NO).
<doxygen:param>EXPAND_ONLY_PREDEF=YES # If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES
# then the macro expansion is limited to the macros specified with the PREDEFINED and EXPAND_AS_DEFINED tags.
# If EXPAND_ONLY_PREDEF tag can be used to specify a list of macro names that should be expanded (as defined).
# The PREDEFINED tag can be used to specify one or more macro names that are defined
# before the preprocessor is started (similar to the -D option of gcc).
# The argument of the tag is a list of macros of the form:
# name or name=definition (no spaces).
# If the definition and the "=" are omitted, "=1" is assumed.
# To prevent a macro definition from being undefined via #undef or
# recursively expanded use the := operator instead of the = operator.
# See http://www.stack.nl/~dimitri/doxygen/config.html#cfg_predefined.
# static char *malloc BOOST_PREVENT_MACRO_SUBSTITUTION(const size_type bytes);
# will not produce a helpful Doxygen output, so
# replace some with more helpful text, or none, for example:
<doxygen:param>"PREDEFINED= \\
\"BOOST_PREVENT_MACRO_SUBSTITUTION\" \\
\"BOOST_STATIC_CONSTANT(T,V)=static x const y\" \\
\"BOOST_UNITS_AUTO_STATIC_CONSTANT(a,b)=static const auto a = b\" \\
\"BOOST_DEDUCED_TYPENAME=typename\" \\
\"BOOST_CONSTEXPR=constexpr\" \\
\"BOOST_CONTAINER_NOEXCEPT=noexcept\" \\
\"BOOST_CONTAINER_NOEXCEPT_IF(T)=noexcept(T)\" \\
\"BOOST_UNITS_TYPEOF(a)=typeof(a)\" \\
\"BOOST_UNITS_HAS_TYPEOF=1\" \\
\"BOOST_MPL_ASSERT(expr)=\" \\
\"BOOST_ASSERT(expr)=\" \\
\"BOOST_RV_REF(T)=T &&\" \\
\"ASSERT(x)=assert(x)\" \\
\"__cplusplus \""
# BOOST_PREVENT_MACRO_SUBSTITUTION, will not be replaced by ,
# BOOST_STATIC_CONSTANT will be replaced by "static x const y",
# BOOST_DEDUCED_TYPENAME will be replaced by "typename",
# BOOST_CONSTEXPR will be replaced by "constexpr".
<doxygen:param>EXCLUDE_SYMBOLS=*_throws
# <doxygen:param>IMAGE_PATH="../images" # for circular_buffer.png
# See autodoxywarnings.log to check this is correct.
# The syntax hoops to jump through are 'interesting' for more than one PREDEFINED,
# and to permit spaces within definitions (use double quotes).
# Don't forget that every double quote " needs a preceding \trip character!
# and that each trailing continuation \ needs a preceding \trip character too!
# And finally that if more than one item is included (as here) the whole is
# enclosed in "PREDEFINED=... ", but without a leading \. Go figure...
# A grep for PREDEFINED= in jamfiles will reveal even more complex examples.
# Boost Libraries with useful examples are: Accumulators, Interprocess, MPI, Random, Units, Expressive.
# Optionally, you can provide a Reference section name specific for your library, for example:
<xsl:param>"boost.doxygen.reftitle=Boost.Circular_buffer C++ Reference"
;
xml circular_buffer : circular_buffer.qbk ;
using boostbook ;
boostbook standalone
:
circular_buffer
:
# General settings
# =================
<format>html:<xsl:param>boost.root=../../../..
<format>html:<xsl:param>img.src.path=../../../../doc/html/
<format>docbook:<xsl:param>boost.root=boost:
# Options for html and pdf
# ========================
# No indent on body text:
<xsl:param>body.start.indent=0pt
# Margin size:
<xsl:param>page.margin.inner=0.5in
# Margin size:
<xsl:param>page.margin.outer=0.5in
# Yes, we want graphics for admonishments:
<xsl:param>admon.graphics=1
# HTML options:
# =============
# Use graphics icons not text for navigation:
<xsl:param>navig.graphics=1
# How far down we chunk nested sections, basically all of them:
<xsl:param>chunk.section.depth=2
# Don't put the first section on the same page as the TOC itself:
<xsl:param>chunk.first.sections=1
# How far down sections get TOC's
<xsl:param>toc.section.depth=4
# Max depth in each TOC:
<xsl:param>toc.max.depth=2
# How far down we go with TOC's
<xsl:param>generate.section.toc.level=10
# Horizontal ? spacing in table cells.
<format>html:<xsl:param>html.cellspacing=3 # pixels
# Vertical spacing in table cells.
<format>html:<xsl:param>html.cellpadding=5 # pixels
# Not sure if these are right way round?
<auto-index>on # Turns on index (or off).
# Turns on (or off) index-verbose for diagnostic info (using /bin auto-index-verbose folders).
<auto-index-verbose>on
<format>pdf:<auto-index-internal>off # on (or off) to use internally generated indexes.
<format>html:<xsl:param>index.on.type=1 # = 1 For the native stylesheets to generate multiple different indexes.
<auto-index-script>circular_buffer.idx # Specifies the name of the script to load for circular_buffer.
<auto-index-prefix>../../.. # Will get you back up to /circular_buffer, so !scan-path "boost/circular_buffer/" is where *.hpp will be,
# and /libs/circular_buffer for other files.
# Without this would need !scan-path "../../../boost/circular_buffer"
# Used by Quickbook to invoke indexing.
# Required by boost-trunk/doc/ see jamfile.v2 to use auto-index.
# Choose indexing method for html:
<format>html:<auto-index-internal>on
<format>docbook:<auto-index-internal>on
# PDF Options:
# ============
# TOC Generation: this is needed for FOP-0.9 and later:
<format>pdf:<xsl:param>fop1.extensions=0
# Or enable this if you're using XEP:
<format>pdf:<xsl:param>xep.extensions=1
# TOC generation: this is needed for FOP 0.2, but must not be set to zero for FOP-0.9!
<format>pdf:<xsl:param>fop.extensions=0
# No indent on body text:
<xsl:param>body.start.indent=0pt
# Margin size:
<xsl:param>page.margin.inner=0.5in
# Margin size:
<xsl:param>page.margin.outer=0.5in
# Yes, we want graphics for admonishments:
<xsl:param>admon.graphics=1
# Set these one for PDF generation *only*:
# default png graphics are awful in PDF form,
# better use SVG instead:
<format>pdf:<xsl:param>admon.graphics.extension=".svg"
#<format>pdf:<xsl:param>admon.graphics.extension=".png" # Only png images are available.
# Don't need this, default path works OK:
#<format>pdf:<xsl:param>admon.graphics.path=$(nav_images)/ # next, prev, note, tip ... for pdf.
<format>pdf:<xsl:param>use.role.for.mediaobject=1
<format>pdf:<xsl:param>preferred.mediaobject.role=print
<format>pdf:<xsl:param>img.src.path=$(pdf_images_location)/ # graphics (diagrams) for pdf.
<format>pdf:<xsl:param>draft.mode="no"
<format>pdf:<xsl:param>boost.url.prefix=../../../..
<dependency>autodoc #
<dependency>png_install
;
# Install (copy) the 'master' copies of all icon images (both PNG and SVG)
# and the Boost logo from your current Boost-root
# to the local /doc/html/images folder so that html is complete and standalone.
install png_install : [ glob $(here)/*.png ] : <location>$(here)/../../../doc/html/images ;
# install pdf-install : standalone : <install-type>PDF <location>. ;
# Effectively copies the file from \bin folder to the \doc folder,
# but will not work as expected if doxygen and/or autoindex is used
# because a modified pdf file is created, so this command
# will rename the file to the expected filename, here circular_buffer.pdf.
install pdfinstall : standalone : <install-type>PDF <location>. <name>circular_buffer.pdf ;
###############################################################################
alias boostdoc
: standalone/<format>docbook
:
:
: ;
explicit boostdoc ;
alias boostrelease ;
explicit boostrelease ;
@@ -0,0 +1,316 @@
// Comparison of bounded buffers based on different containers.
// Copyright (c) 2003-2008 Jan Gaspar
// Copyright 2013 Paul A. Bristow. Added some Quickbook snippet markers.
// Use, modification, and distribution is subject to the Boost Software
// License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)
#include <boost/circular_buffer.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/condition.hpp>
#include <boost/thread/thread.hpp>
#include <boost/timer/timer.hpp>
#include <boost/call_traits.hpp>
#include <boost/bind.hpp>
#include <deque>
#include <list>
#include <string>
#include <iostream>
const unsigned long QUEUE_SIZE = 1000L;
const unsigned long TOTAL_ELEMENTS = QUEUE_SIZE * 1000L;
template <class T>
class bounded_buffer {
public:
typedef boost::circular_buffer<T> container_type;
typedef typename container_type::size_type size_type;
typedef typename container_type::value_type value_type;
typedef typename boost::call_traits<value_type>::param_type param_type;
explicit bounded_buffer(size_type capacity) : m_unread(0), m_container(capacity) {}
void push_front(param_type item) {
boost::mutex::scoped_lock lock(m_mutex);
m_not_full.wait(lock, boost::bind(&bounded_buffer<value_type>::is_not_full, this));
m_container.push_front(item);
++m_unread;
lock.unlock();
m_not_empty.notify_one();
}
void pop_back(value_type* pItem) {
boost::mutex::scoped_lock lock(m_mutex);
m_not_empty.wait(lock, boost::bind(&bounded_buffer<value_type>::is_not_empty, this));
*pItem = m_container[--m_unread];
lock.unlock();
m_not_full.notify_one();
}
private:
bounded_buffer(const bounded_buffer&); // Disabled copy constructor
bounded_buffer& operator = (const bounded_buffer&); // Disabled assign operator
bool is_not_empty() const { return m_unread > 0; }
bool is_not_full() const { return m_unread < m_container.capacity(); }
size_type m_unread;
container_type m_container;
boost::mutex m_mutex;
boost::condition m_not_empty;
boost::condition m_not_full;
};
template <class T>
class bounded_buffer_space_optimized {
public:
typedef boost::circular_buffer_space_optimized<T> container_type;
typedef typename container_type::size_type size_type;
typedef typename container_type::value_type value_type;
typedef typename boost::call_traits<value_type>::param_type param_type;
explicit bounded_buffer_space_optimized(size_type capacity) : m_container(capacity) {}
void push_front(param_type item) {
boost::mutex::scoped_lock lock(m_mutex);
m_not_full.wait(lock, boost::bind(&bounded_buffer_space_optimized<value_type>::is_not_full, this));
m_container.push_front(item);
lock.unlock();
m_not_empty.notify_one();
}
void pop_back(value_type* pItem) {
boost::mutex::scoped_lock lock(m_mutex);
m_not_empty.wait(lock, boost::bind(&bounded_buffer_space_optimized<value_type>::is_not_empty, this));
*pItem = m_container.back();
m_container.pop_back();
lock.unlock();
m_not_full.notify_one();
}
private:
bounded_buffer_space_optimized(const bounded_buffer_space_optimized&); // Disabled copy constructor
bounded_buffer_space_optimized& operator = (const bounded_buffer_space_optimized&); // Disabled assign operator
bool is_not_empty() const { return m_container.size() > 0; }
bool is_not_full() const { return m_container.size() < m_container.capacity(); }
container_type m_container;
boost::mutex m_mutex;
boost::condition m_not_empty;
boost::condition m_not_full;
};
template <class T>
class bounded_buffer_deque_based {
public:
typedef std::deque<T> container_type;
typedef typename container_type::size_type size_type;
typedef typename container_type::value_type value_type;
typedef typename boost::call_traits<value_type>::param_type param_type;
explicit bounded_buffer_deque_based(size_type capacity) : m_capacity(capacity) {}
void push_front(param_type item) {
boost::mutex::scoped_lock lock(m_mutex);
m_not_full.wait(lock, boost::bind(&bounded_buffer_deque_based<value_type>::is_not_full, this));
m_container.push_front(item);
lock.unlock();
m_not_empty.notify_one();
}
void pop_back(value_type* pItem) {
boost::mutex::scoped_lock lock(m_mutex);
m_not_empty.wait(lock, boost::bind(&bounded_buffer_deque_based<value_type>::is_not_empty, this));
*pItem = m_container.back();
m_container.pop_back();
lock.unlock();
m_not_full.notify_one();
}
private:
bounded_buffer_deque_based(const bounded_buffer_deque_based&); // Disabled copy constructor
bounded_buffer_deque_based& operator = (const bounded_buffer_deque_based&); // Disabled assign operator
bool is_not_empty() const { return m_container.size() > 0; }
bool is_not_full() const { return m_container.size() < m_capacity; }
const size_type m_capacity;
container_type m_container;
boost::mutex m_mutex;
boost::condition m_not_empty;
boost::condition m_not_full;
};
template <class T>
class bounded_buffer_list_based {
public:
typedef std::list<T> container_type;
typedef typename container_type::size_type size_type;
typedef typename container_type::value_type value_type;
typedef typename boost::call_traits<value_type>::param_type param_type;
explicit bounded_buffer_list_based(size_type capacity) : m_capacity(capacity) {}
void push_front(param_type item) {
boost::mutex::scoped_lock lock(m_mutex);
m_not_full.wait(lock, boost::bind(&bounded_buffer_list_based<value_type>::is_not_full, this));
m_container.push_front(item);
lock.unlock();
m_not_empty.notify_one();
}
void pop_back(value_type* pItem) {
boost::mutex::scoped_lock lock(m_mutex);
m_not_empty.wait(lock, boost::bind(&bounded_buffer_list_based<value_type>::is_not_empty, this));
*pItem = m_container.back();
m_container.pop_back();
lock.unlock();
m_not_full.notify_one();
}
private:
bounded_buffer_list_based(const bounded_buffer_list_based&); // Disabled copy constructor
bounded_buffer_list_based& operator = (const bounded_buffer_list_based&); // Disabled assign operator
bool is_not_empty() const { return m_container.size() > 0; }
bool is_not_full() const { return m_container.size() < m_capacity; }
const size_type m_capacity;
container_type m_container;
boost::mutex m_mutex;
boost::condition m_not_empty;
boost::condition m_not_full;
};
template<class Buffer>
class Consumer {
typedef typename Buffer::value_type value_type;
Buffer* m_container;
value_type m_item;
public:
Consumer(Buffer* buffer) : m_container(buffer) {}
void operator()() {
for (unsigned long i = 0L; i < TOTAL_ELEMENTS; ++i) {
m_container->pop_back(&m_item);
}
}
};
template<class Buffer>
class Producer {
typedef typename Buffer::value_type value_type;
Buffer* m_container;
public:
Producer(Buffer* buffer) : m_container(buffer) {}
void operator()() {
for (unsigned long i = 0L; i < TOTAL_ELEMENTS; ++i) {
m_container->push_front(value_type());
}
}
};
template<class Buffer>
void fifo_test(Buffer* buffer) {
// Start of measurement
boost::timer::auto_cpu_timer progress;
// Initialize the buffer with some values before launching producer and consumer threads.
for (unsigned long i = QUEUE_SIZE / 2L; i > 0; --i) {
#if BOOST_WORKAROUND(BOOST_BORLANDC, BOOST_TESTED_AT(0x581))
buffer->push_front(Buffer::value_type());
#else
buffer->push_front(BOOST_DEDUCED_TYPENAME Buffer::value_type());
#endif
}
Consumer<Buffer> consumer(buffer);
Producer<Buffer> producer(buffer);
// Start the threads.
boost::thread consume(consumer);
boost::thread produce(producer);
// Wait for completion.
consume.join();
produce.join();
// End of measurement
}
int main(int /*argc*/, char* /*argv*/[]) {
bounded_buffer<int> bb_int(QUEUE_SIZE);
std::cout << "bounded_buffer<int> ";
fifo_test(&bb_int);
bounded_buffer_space_optimized<int> bb_space_optimized_int(QUEUE_SIZE);
std::cout << "bounded_buffer_space_optimized<int> ";
fifo_test(&bb_space_optimized_int);
bounded_buffer_deque_based<int> bb_deque_based_int(QUEUE_SIZE);
std::cout << "bounded_buffer_deque_based<int> ";
fifo_test(&bb_deque_based_int);
bounded_buffer_list_based<int> bb_list_based_int(QUEUE_SIZE);
std::cout << "bounded_buffer_list_based<int> ";
fifo_test(&bb_list_based_int);
bounded_buffer<std::string> bb_string(QUEUE_SIZE);
std::cout << "bounded_buffer<std::string> ";
fifo_test(&bb_string);
bounded_buffer_space_optimized<std::string> bb_space_optimized_string(QUEUE_SIZE);
std::cout << "bounded_buffer_space_optimized<std::string> ";
fifo_test(&bb_space_optimized_string);
bounded_buffer_deque_based<std::string> bb_deque_based_string(QUEUE_SIZE);
std::cout << "bounded_buffer_deque_based<std::string> ";
fifo_test(&bb_deque_based_string);
bounded_buffer_list_based<std::string> bb_list_based_string(QUEUE_SIZE);
std::cout << "bounded_buffer_list_based<std::string> ";
fifo_test(&bb_list_based_string);
return 0;
}
/*
//[bounded_buffer_comparison_output
Description: Autorun "J:\Cpp\Misc\Debug\bounded_buffer_comparison.exe"
bounded_buffer<int> 5.15 s
bounded_buffer_space_optimized<int> 5.71 s
bounded_buffer_deque_based<int> 15.57 s
bounded_buffer_list_based<int> 17.33 s
bounded_buffer<std::string> 24.49 s
bounded_buffer_space_optimized<std::string> 28.33 s
bounded_buffer_deque_based<std::string> 29.45 s
bounded_buffer_list_based<std::string> 31.29 s
//] //[bounded_buffer_comparison_output]
*/
@@ -0,0 +1,192 @@
// Copyright 2003-2008 Jan Gaspar.
// Copyright 2013 Paul A. Bristow. Added some Quickbook snippet markers.
// Distributed under the Boost Software License, Version 1.0.
// (See the accompanying file LICENSE_1_0.txt
// or a copy at <http://www.boost.org/LICENSE_1_0.txt>.)
//[circular_buffer_bound_example_1
/*`
This example shows how the `circular_buffer` can be utilized
as an underlying container of the bounded buffer.
*/
#include <boost/circular_buffer.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/condition.hpp>
#include <boost/thread/thread.hpp>
#include <boost/call_traits.hpp>
#include <boost/bind.hpp>
#include <boost/timer/timer.hpp> // for auto_cpu_timer
#include <iostream>
template <class T>
class bounded_buffer
{
public:
typedef boost::circular_buffer<T> container_type;
typedef typename container_type::size_type size_type;
typedef typename container_type::value_type value_type;
typedef typename boost::call_traits<value_type>::param_type param_type;
explicit bounded_buffer(size_type capacity) : m_unread(0), m_container(capacity) {}
void push_front(typename boost::call_traits<value_type>::param_type item)
{ // `param_type` represents the "best" way to pass a parameter of type `value_type` to a method.
boost::mutex::scoped_lock lock(m_mutex);
m_not_full.wait(lock, boost::bind(&bounded_buffer<value_type>::is_not_full, this));
m_container.push_front(item);
++m_unread;
lock.unlock();
m_not_empty.notify_one();
}
void pop_back(value_type* pItem) {
boost::mutex::scoped_lock lock(m_mutex);
m_not_empty.wait(lock, boost::bind(&bounded_buffer<value_type>::is_not_empty, this));
*pItem = m_container[--m_unread];
lock.unlock();
m_not_full.notify_one();
}
private:
bounded_buffer(const bounded_buffer&); // Disabled copy constructor.
bounded_buffer& operator = (const bounded_buffer&); // Disabled assign operator.
bool is_not_empty() const { return m_unread > 0; }
bool is_not_full() const { return m_unread < m_container.capacity(); }
size_type m_unread;
container_type m_container;
boost::mutex m_mutex;
boost::condition m_not_empty;
boost::condition m_not_full;
}; //
//] [/circular_buffer_bound_example_1]
const unsigned long queue_size = 1000L;
const unsigned long total_elements = queue_size * 1000L;
//[circular_buffer_bound_example_2]
/*`To demonstrate, create two classes to exercise the buffer.
The producer class fills the buffer with elements.
The consumer class consumes the buffer contents.
*/
template<class Buffer>
class Producer
{
typedef typename Buffer::value_type value_type;
Buffer* m_container;
public:
Producer(Buffer* buffer) : m_container(buffer)
{}
void operator()()
{
for (unsigned long i = 0L; i < total_elements; ++i)
{
m_container->push_front(value_type());
}
}
};
template<class Buffer>
class Consumer
{
typedef typename Buffer::value_type value_type;
Buffer* m_container;
value_type m_item;
public:
Consumer(Buffer* buffer) : m_container(buffer)
{}
void operator()()
{
for (unsigned long i = 0L; i < total_elements; ++i)
{
m_container->pop_back(&m_item);
}
}
};
/*`Create a first-int first-out test of the bound_buffer.
Include a call to boost::progress_timer
[@http://www.boost.org/doc/libs/1_53_0/libs/timer/doc/cpu_timers.html CPU timer]
*/
template<class Buffer>
void fifo_test(Buffer* buffer)
{
// Start of timing.
boost::timer::auto_cpu_timer progress;
// Initialize the buffer with some values before launching producer and consumer threads.
for (unsigned long i = queue_size / 2L; i > 0; --i)
{
#if BOOST_WORKAROUND(BOOST_BORLANDC, BOOST_TESTED_AT(0x581))
buffer->push_front(Buffer::value_type());
#else
buffer->push_front(BOOST_DEDUCED_TYPENAME Buffer::value_type());
#endif
}
// Construct the threads.
Consumer<Buffer> consumer(buffer);
Producer<Buffer> producer(buffer);
// Start the threads.
boost::thread consume(consumer);
boost::thread produce(producer);
// Wait for completion.
consume.join();
produce.join();
// End of timing.
// destructor of boost::timer::auto_cpu_timer will output the time to std::cout.
}
//] [/circular_buffer_bound_example_2]
int main()
{
//[circular_buffer_bound_example_3]
//`Construct a bounded_buffer to hold the chosen type, here int.
bounded_buffer<int> bb_int(queue_size);
std::cout << "Testing bounded_buffer<int> ";
//`Start the fifo test.
fifo_test(&bb_int);
//` destructor of boost::timer::auto_cpu_timer will output the time to std::cout
//] [/circular_buffer_bound_example_3]
return 0;
} // int main()
/*
//[circular_buffer_bound_output
Description: Autorun "J:\Cpp\Misc\Debug\circular_buffer_bound_example.exe"
Testing bounded_buffer<int> 15.010692s wall, 9.188459s user + 7.207246s system = 16.395705s CPU (109.2%)
//] [/circular_buffer_bound_output]
*/
@@ -0,0 +1,63 @@
// Copyright 2003-2008 Jan Gaspar.
// Copyright 2013 Paul A. Bristow. Added some Quickbook snippet markers.
// Distributed under the Boost Software License, Version 1.0.
// (See the accompanying file LICENSE_1_0.txt
// or a copy at <http://www.boost.org/LICENSE_1_0.txt>.)
//[circular_buffer_example_1
/*`For all examples, we need this include:
*/
#include <boost/circular_buffer.hpp>
//] [/circular_buffer_example_1]
int main()
{
//[circular_buffer_example_2
// Create a circular buffer with a capacity for 3 integers.
boost::circular_buffer<int> cb(3);
// Insert three elements into the buffer.
cb.push_back(1);
cb.push_back(2);
cb.push_back(3);
int a = cb[0]; // a == 1
int b = cb[1]; // b == 2
int c = cb[2]; // c == 3
// The buffer is full now, so pushing subsequent
// elements will overwrite the front-most elements.
cb.push_back(4); // Overwrite 1 with 4.
cb.push_back(5); // Overwrite 2 with 5.
// The buffer now contains 3, 4 and 5.
a = cb[0]; // a == 3
b = cb[1]; // b == 4
c = cb[2]; // c == 5
// Elements can be popped from either the front or the back.
cb.pop_back(); // 5 is removed.
cb.pop_front(); // 3 is removed.
// Leaving only one element with value = 4.
int d = cb[0]; // d == 4
//] [/circular_buffer_example_2]
return 0;
}
/*
//[circular_buffer_example_output
There is no output from this example.
//] [/circular_buffer_example_output]
*/
@@ -0,0 +1,14 @@
echo off
rem quickbook doxygen auto-index docs template circular_buffer_html_index.bat
rem echo circular_buffer_html_index_%date%_%time:~0,2%_%time:~3,2%.log
rem The DOS time format is assumed 12:34 and the : separator is not used.
set t=%time% /T
set tim=%t:~0,2%%t:~3,2%
rem pick just hours and minutes.
rem time may include leading space, like " 915", so remove space.
set tim=%tim: =%
rem boost-no-inspect
rem cd \boost-trunk/circular_buffer\libs\circular_buffer\example
bjam -a > circular_buffer_examples_%date%_%tim%.log
if not ERRORLEVEL 0 (echo Errorlevel is %ERRORLEVEL%) else (echo OK)
pause
@@ -0,0 +1,40 @@
// Copyright 2003-2008 Jan Gaspar.
// Copyright 2013 Paul A. Bristow. Added some Quickbook snippet markers.
// Distributed under the Boost Software License, Version 1.0.
// (See the accompanying file LICENSE_1_0.txt
// or a copy at <http://www.boost.org/LICENSE_1_0.txt>.)
#undef BOOST_CB_ENABLE_DEBUG
//[circular_buffer_iter_example_1
/*`
*/
#define BOOST_CB_ENABLE_DEBUG 0 // The Debug Support has to be disabled, otherwise the code produces a runtime error.
#include <boost/circular_buffer.hpp>
#include <boost/assert.hpp>
#include <assert.h>
int main(int /*argc*/, char* /*argv*/[])
{
boost::circular_buffer<int> cb(3);
cb.push_back(1);
cb.push_back(2);
cb.push_back(3);
boost::circular_buffer<int>::iterator it = cb.begin();
assert(*it == 1);
cb.push_back(4);
assert(*it == 4); // The iterator still points to the initialized memory.
return 0;
}
//] [/circular_buffer_iter_example_1]
@@ -0,0 +1,64 @@
// Copyright 2003-2008 Jan Gaspar.
// Copyright 2013 Paul A. Bristow. Added some Quickbook snippet markers.
// Distributed under the Boost Software License, Version 1.0.
// (See the accompanying file LICENSE_1_0.txt
// or a copy at <http://www.boost.org/LICENSE_1_0.txt>.)
//[circular_buffer_sum_example_1
/*`This example shows several functions, including summing all valid values.
*/
#include <boost/circular_buffer.hpp>
#include <numeric>
#include <assert.h>
int main(int /*argc*/, char* /*argv*/[])
{
// Create a circular buffer of capacity 3.
boost::circular_buffer<int> cb(3);
assert(cb.capacity() == 3);
// Check is empty.
assert(cb.size() == 0);
assert(cb.empty());
// Insert some elements into the circular buffer.
cb.push_back(1);
cb.push_back(2);
// Assertions to check push_backs have expected effect.
assert(cb[0] == 1);
assert(cb[1] == 2);
assert(!cb.full());
assert(cb.size() == 2);
assert(cb.capacity() == 3);
// Insert some other elements.
cb.push_back(3);
cb.push_back(4);
// Evaluate the sum of all elements.
int sum = std::accumulate(cb.begin(), cb.end(), 0);
// Assertions to check state.
assert(sum == 9);
assert(cb[0] == 2);
assert(cb[1] == 3);
assert(cb[2] == 4);
assert(*cb.begin() == 2);
assert(cb.front() == 2);
assert(cb.back() == 4);
assert(cb.full());
assert(cb.size() == 3);
assert(cb.capacity() == 3);
return 0;
}
//] [/circular_buffer_sum_example_1]
/*
There is no output from this example.
*/
@@ -0,0 +1,42 @@
# Copyright Paul A. Bristow 2013
# Distributed under the Boost Software License, Version 1.0.
# (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
# jamfile.v2 to run all circular_buffer examples.
# bring in the rules for testing.
import testing ;
project
: requirements
<library>/boost/system//boost_system
<library>/boost/thread//boost_thread
#<define>BOOST_ALL_NO_LIB=1
<threading>multi
<toolset>gcc:<cxxflags>-Wno-missing-braces
<toolset>darwin:<cxxflags>-Wno-missing-braces
<toolset>acc:<cxxflags>+W2068,2461,2236,4070
<toolset>intel:<cxxflags>-Qwd264,239
<toolset>msvc:<warnings>all
<toolset>msvc:<asynch-exceptions>on
<toolset>msvc:<define>_CRT_SECURE_NO_DEPRECATE
<toolset>msvc:<define>_SCL_SECURE_NO_DEPRECATE
<toolset>msvc:<define>_SCL_SECURE_NO_WARNINGS
<toolset>msvc:<define>_CRT_SECURE_NO_WARNINGS
<toolset>msvc:<cxxflags>/wd4996
<toolset>msvc:<cxxflags>/wd4512
<toolset>msvc:<cxxflags>/wd4610
<toolset>msvc:<cxxflags>/wd4510
<toolset>msvc:<cxxflags>/wd4127
<toolset>msvc:<cxxflags>/wd4701
<toolset>msvc:<cxxflags>/wd4127
<toolset>msvc:<cxxflags>/wd4305
;
run bounded_buffer_comparison.cpp ../../timer/build//boost_timer ;
run circular_buffer_iter_example.cpp ;
run circular_buffer_sum_example.cpp ;
run circular_buffer_bound_example.cpp ../../thread/build//boost_thread ../../timer/build//boost_timer ;
@@ -0,0 +1,65 @@
// Circular buffer library header file.
// Copyright (c) 2003-2008 Jan Gaspar
// Use, modification, and distribution is subject to the Boost Software
// License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)
// See www.boost.org/libs/circular_buffer for documentation.
/*! @file
Includes <boost/circular_buffer/base.hpp>
*/
#if !defined(BOOST_CIRCULAR_BUFFER_HPP)
#define BOOST_CIRCULAR_BUFFER_HPP
#if defined(_MSC_VER)
#pragma once
#endif
#include <boost/circular_buffer_fwd.hpp>
#include <boost/config/workaround.hpp>
#include <boost/static_assert.hpp>
/*! Debug support control. */
#if !defined(BOOST_CB_ENABLE_DEBUG)
#define BOOST_CB_ENABLE_DEBUG 0
#endif
/*! INTERNAL ONLY */
#if BOOST_CB_ENABLE_DEBUG
#include <boost/assert.hpp>
#define BOOST_CB_ASSERT(Expr) BOOST_ASSERT(Expr)
#else
#define BOOST_CB_ASSERT(Expr) ((void)0)
#endif
/*! INTERNAL ONLY */
#if BOOST_WORKAROUND(BOOST_BORLANDC, <= 0x0550) || BOOST_WORKAROUND(__MWERKS__, <= 0x2407)
#define BOOST_CB_IS_CONVERTIBLE(Iterator, Type) ((void)0)
#else
#include <iterator>
#include <boost/type_traits/is_convertible.hpp>
#define BOOST_CB_IS_CONVERTIBLE(Iterator, Type) \
BOOST_STATIC_ASSERT((is_convertible<typename std::iterator_traits<Iterator>::value_type, Type>::value))
#endif
/*! INTERNAL ONLY */
#if defined(BOOST_NO_TEMPLATED_ITERATOR_CONSTRUCTORS)
#define BOOST_CB_ASSERT_TEMPLATED_ITERATOR_CONSTRUCTORS BOOST_STATIC_ASSERT(false);
#else
#define BOOST_CB_ASSERT_TEMPLATED_ITERATOR_CONSTRUCTORS ((void)0);
#endif
#include <boost/circular_buffer/debug.hpp>
#include <boost/circular_buffer/details.hpp>
#include <boost/circular_buffer/base.hpp>
#include <boost/circular_buffer/space_optimized.hpp>
#undef BOOST_CB_ASSERT_TEMPLATED_ITERATOR_CONSTRUCTORS
#undef BOOST_CB_IS_CONVERTIBLE
#undef BOOST_CB_ASSERT
#endif // #if !defined(BOOST_CIRCULAR_BUFFER_HPP)
@@ -0,0 +1,248 @@
// Debug support for the circular buffer library.
// Copyright (c) 2003-2008 Jan Gaspar
// Use, modification, and distribution is subject to the Boost Software
// License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)
#if !defined(BOOST_CIRCULAR_BUFFER_DEBUG_HPP)
#define BOOST_CIRCULAR_BUFFER_DEBUG_HPP
#if defined(_MSC_VER)
#pragma once
#endif
#if BOOST_CB_ENABLE_DEBUG
#include <cstring>
#if defined(BOOST_NO_STDC_NAMESPACE)
namespace std {
using ::memset;
}
#endif
#endif // BOOST_CB_ENABLE_DEBUG
namespace boost {
namespace cb_details {
#if BOOST_CB_ENABLE_DEBUG
// The value the uninitialized memory is filled with.
const int UNINITIALIZED = 0xcc;
template <class T>
inline void do_fill_uninitialized_memory(T* data, std::size_t size_in_bytes) BOOST_NOEXCEPT {
std::memset(static_cast<void*>(data), UNINITIALIZED, size_in_bytes);
}
template <class T>
inline void do_fill_uninitialized_memory(T& /*data*/, std::size_t /*size_in_bytes*/) BOOST_NOEXCEPT {
// Do nothing
}
class debug_iterator_registry;
/*!
\class debug_iterator_base
\brief Registers/unregisters iterators into the registry of valid iterators.
This class is intended to be a base class of an iterator.
*/
class debug_iterator_base {
private:
// Members
//! Iterator registry.
mutable const debug_iterator_registry* m_registry;
//! Next iterator in the iterator chain.
mutable const debug_iterator_base* m_next;
public:
// Construction/destruction
//! Default constructor.
debug_iterator_base();
//! Constructor taking the iterator registry as a parameter.
debug_iterator_base(const debug_iterator_registry* registry);
//! Copy constructor.
debug_iterator_base(const debug_iterator_base& rhs);
//! Destructor.
~debug_iterator_base();
// Methods
//! Assign operator.
debug_iterator_base& operator = (const debug_iterator_base& rhs);
//! Is the iterator valid?
bool is_valid(const debug_iterator_registry* registry) const;
//! Invalidate the iterator.
/*!
\note The method is const in order to invalidate const iterators, too.
*/
void invalidate() const;
//! Return the next iterator in the iterator chain.
const debug_iterator_base* next() const;
//! Set the next iterator in the iterator chain.
/*!
\note The method is const in order to set a next iterator to a const iterator, too.
*/
void set_next(const debug_iterator_base* it) const;
private:
// Helpers
//! Register self as a valid iterator.
void register_self();
//! Unregister self from valid iterators.
void unregister_self();
};
/*!
\class debug_iterator_registry
\brief Registry of valid iterators.
This class is intended to be a base class of a container.
*/
class debug_iterator_registry {
//! Pointer to the chain of valid iterators.
mutable const debug_iterator_base* m_iterators;
public:
// Methods
//! Default constructor.
debug_iterator_registry() : m_iterators(0) {}
//! Register an iterator into the list of valid iterators.
/*!
\note The method is const in order to register iterators into const containers, too.
*/
void register_iterator(const debug_iterator_base* it) const {
it->set_next(m_iterators);
m_iterators = it;
}
//! Unregister an iterator from the list of valid iterators.
/*!
\note The method is const in order to unregister iterators from const containers, too.
*/
void unregister_iterator(const debug_iterator_base* it) const {
const debug_iterator_base* previous = 0;
for (const debug_iterator_base* p = m_iterators; p != it; previous = p, p = p->next()) {}
remove(it, previous);
}
//! Invalidate every iterator pointing to the same element as the iterator passed as a parameter.
template <class Iterator>
void invalidate_iterators(const Iterator& it) {
const debug_iterator_base* previous = 0;
for (const debug_iterator_base* p = m_iterators; p != 0; p = p->next()) {
if (((Iterator*)p)->m_it == it.m_it) {
p->invalidate();
remove(p, previous);
continue;
}
previous = p;
}
}
//! Invalidate all iterators except an iterator poining to the same element as the iterator passed as a parameter.
template <class Iterator>
void invalidate_iterators_except(const Iterator& it) {
const debug_iterator_base* previous = 0;
for (const debug_iterator_base* p = m_iterators; p != 0; p = p->next()) {
if (((Iterator*)p)->m_it != it.m_it) {
p->invalidate();
remove(p, previous);
continue;
}
previous = p;
}
}
//! Invalidate all iterators.
void invalidate_all_iterators() {
for (const debug_iterator_base* p = m_iterators; p != 0; p = p->next())
p->invalidate();
m_iterators = 0;
}
private:
// Helpers
//! Remove the current iterator from the iterator chain.
void remove(const debug_iterator_base* current,
const debug_iterator_base* previous) const {
if (previous == 0)
m_iterators = m_iterators->next();
else
previous->set_next(current->next());
}
};
// Implementation of the debug_iterator_base methods.
inline debug_iterator_base::debug_iterator_base() : m_registry(0), m_next(0) {}
inline debug_iterator_base::debug_iterator_base(const debug_iterator_registry* registry)
: m_registry(registry), m_next(0) {
register_self();
}
inline debug_iterator_base::debug_iterator_base(const debug_iterator_base& rhs)
: m_registry(rhs.m_registry), m_next(0) {
register_self();
}
inline debug_iterator_base::~debug_iterator_base() { unregister_self(); }
inline debug_iterator_base& debug_iterator_base::operator = (const debug_iterator_base& rhs) {
if (m_registry == rhs.m_registry)
return *this;
unregister_self();
m_registry = rhs.m_registry;
register_self();
return *this;
}
inline bool debug_iterator_base::is_valid(const debug_iterator_registry* registry) const {
return m_registry == registry;
}
inline void debug_iterator_base::invalidate() const { m_registry = 0; }
inline const debug_iterator_base* debug_iterator_base::next() const { return m_next; }
inline void debug_iterator_base::set_next(const debug_iterator_base* it) const { m_next = it; }
inline void debug_iterator_base::register_self() {
if (m_registry != 0)
m_registry->register_iterator(this);
}
inline void debug_iterator_base::unregister_self() {
if (m_registry != 0)
m_registry->unregister_iterator(this);
}
#endif // #if BOOST_CB_ENABLE_DEBUG
} // namespace cb_details
} // namespace boost
#endif // #if !defined(BOOST_CIRCULAR_BUFFER_DEBUG_HPP)

Some files were not shown because too many files have changed in this diff Show More