Keep vanetza-idf in obu-firmware, so a plain clone builds the firmware
obu-firmware builds against the vanetza-idf C-ITS library, which until now came from the colleague's microbu-esp32c5 tree beside the repository and was not tracked here, so a clone of this repository could not build the firmware it ships. The library alone is now part of obu-firmware, as obu-firmware/external/vanetza-idf: their external/vanetza-idf at commit cf4b99f, unchanged (9775 files; see its PROVENANCE.md). CMake takes it from there by default; -DVANETZA_IDF_DIR still points the build elsewhere. The rest of the colleague's tree (their own VAM firmware, PKI tooling, station-link Python tools, the V2X2MAP bridge) stays out of this repository and gitignored; nothing is pushed to their repository. NOTES.md, docs/06, TODO.md and the pcap verifier's usage line point at the new location.
This commit is contained in:
@@ -0,0 +1,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
|
||||
+290
@@ -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&) {}
|
||||
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
[
|
||||
"TC_GEONW_FDV_SHB_BV_01",
|
||||
"TC_GEONW_PON_FPB_BV_11_05",
|
||||
"TC_GEONW_PON_SHB_BV_01"
|
||||
]
|
||||
+540
@@ -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, ¶ms) == -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
|
||||
+70
@@ -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
|
||||
+9
@@ -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
|
||||
+10
@@ -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"
|
||||
]
|
||||
+100
@@ -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
|
||||
+28
@@ -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
|
||||
}
|
||||
+1026
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;
|
||||
}
|
||||
Reference in New Issue
Block a user