Keep vanetza-idf in obu-firmware, so a plain clone builds the firmware

obu-firmware builds against the vanetza-idf C-ITS library, which until now
came from the colleague's microbu-esp32c5 tree beside the repository and was
not tracked here, so a clone of this repository could not build the firmware
it ships. The library alone is now part of obu-firmware, as
obu-firmware/external/vanetza-idf: their external/vanetza-idf at commit
cf4b99f, unchanged (9775 files; see its PROVENANCE.md). CMake takes it from
there by default; -DVANETZA_IDF_DIR still points the build elsewhere.

The rest of the colleague's tree (their own VAM firmware, PKI tooling,
station-link Python tools, the V2X2MAP bridge) stays out of this repository
and gitignored; nothing is pushed to their repository. NOTES.md, docs/06,
TODO.md and the pcap verifier's usage line point at the new location.
This commit is contained in:
Ashin Walpola
2026-09-24 10:56:05 +02:00
parent 2f60623e18
commit d107534eb2
9781 changed files with 1560475 additions and 17 deletions
@@ -0,0 +1,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;
}