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,74 @@
|
||||
find_package(Boost REQUIRED)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
find_package(Threads)
|
||||
|
||||
include(FetchContent)
|
||||
find_package(CLI11 2.6 QUIET)
|
||||
if(NOT CLI11_FOUND)
|
||||
set(CLI11_PRECOMPILED ON CACHE BOOL "Build CLI11 as a compiled library" FORCE)
|
||||
FetchContent_Declare(CLI11
|
||||
GIT_REPOSITORY https://github.com/CLIUtils/CLI11.git
|
||||
GIT_TAG v2.6.2
|
||||
GIT_SHALLOW TRUE)
|
||||
FetchContent_MakeAvailable(CLI11)
|
||||
endif()
|
||||
|
||||
add_library(pki_library STATIC
|
||||
asn1.cpp
|
||||
at_request.cpp
|
||||
at_response.cpp
|
||||
authorization.cpp
|
||||
certificate.cpp
|
||||
certificate_filesystem_storage.cpp
|
||||
certificate_revocation_list.cpp
|
||||
certificate_trust_list.cpp
|
||||
config.cpp
|
||||
credential_filesystem_storage.cpp
|
||||
crl_store.cpp
|
||||
dc_command.cpp
|
||||
distribution_centre.cpp
|
||||
ea_request.cpp
|
||||
ea_response.cpp
|
||||
ecies.cpp
|
||||
encrypted_data.cpp
|
||||
filesystem.cpp
|
||||
hashed_id8.cpp
|
||||
hexstring.cpp
|
||||
http.cpp
|
||||
key_command.cpp
|
||||
keys.cpp
|
||||
openssl.cpp
|
||||
openssl_security_module.cpp
|
||||
pem.cpp
|
||||
prune_command.cpp
|
||||
psid_ssp.cpp
|
||||
response_codes.cpp
|
||||
security_module.cpp
|
||||
signed_builder.cpp
|
||||
signed_data.cpp
|
||||
station_config_filesystem.cpp
|
||||
time.cpp
|
||||
trust_list_filesystem_storage.cpp
|
||||
validation.cpp
|
||||
xdg.cpp
|
||||
)
|
||||
target_link_libraries(pki_library PUBLIC
|
||||
OpenSSL::Crypto OpenSSL::SSL
|
||||
Boost::headers
|
||||
CLI11::CLI11
|
||||
asn1 common security)
|
||||
target_compile_definitions(pki_library PRIVATE OPENSSL_API_COMPAT=0x10101000L)
|
||||
target_compile_features(pki_library PUBLIC cxx_std_17)
|
||||
|
||||
add_executable(pki
|
||||
cpoc.cpp
|
||||
enrolment.cpp
|
||||
main.cpp
|
||||
printing.cpp
|
||||
station.cpp
|
||||
)
|
||||
target_link_libraries(pki PUBLIC pki_library)
|
||||
set_target_properties(pki PROPERTIES INSTALL_RPATH $ORIGIN/../${CMAKE_INSTALL_LIBDIR})
|
||||
install(TARGETS pki DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
|
||||
add_test_subdirectory(tests)
|
||||
@@ -0,0 +1,110 @@
|
||||
# Vanetza PKI Client
|
||||
|
||||
Our PKI client allows you to interact with any ETSI compliant C-ITS PKI provider.
|
||||
Ideally, your PKI provider is listed in the European Certificate Trust List (ECTL).
|
||||
|
||||
In the examples below we use Eviden's L0 PKI (c-its-pki.eu). However, this tool is not limited to any particular PKI provider!
|
||||
|
||||
|
||||
## Enrol at PKI included in ECTL
|
||||
|
||||
1. Fetch latest TLM from CPOC
|
||||
|
||||
`pki cpoc tlm fetch`
|
||||
|
||||
> Added TLM certificate "EU-TLM_L0" (8FFE810BDB0D71E6)
|
||||
|
||||
Writes ~/.local/share/vanetza/pki/certificates/8FFE810BDB0D71E6.tlm
|
||||
|
||||
|
||||
2. Fetch ECTL from CPOC
|
||||
|
||||
`pki cpoc ectl fetch`
|
||||
|
||||
> Stored ECTL
|
||||
|
||||
Writes ~/.local/share/vanetza/pki/ectl.ctl and populates certificates/*.rca with Root CA certificaties listed in ECTL.
|
||||
|
||||
|
||||
3. Select Root CA for your station
|
||||
|
||||
`pki station set-root-ca 1B5CB4BEBE6FE9E9`
|
||||
|
||||
> Found Root CA certificate in cache.
|
||||
|
||||
|
||||
4. Fetch CTL from Distribution Centre (DC) of your Root CA
|
||||
|
||||
`pki dc info`
|
||||
|
||||
> DC URL: https://0.eu-dc.l0.c-its-pki.eu/
|
||||
|
||||
Prints the DC URL as found in the ECTL for the previously selected Root CA.
|
||||
|
||||
`pki dc getctl --print`
|
||||
|
||||
> - EA: http://0.eu-ea.l0.c-its-pki.eu/ [AA] \
|
||||
> - AA: http://0.eu-aa.l0.c-its-pki.eu/ \
|
||||
> Fetched CTL matches Root CA digest \
|
||||
> CTL is valid. Added to local trust list storage.
|
||||
|
||||
Writes ~/.local/share/vanetza/pki/ctls/1B5CB4BEBE6FE9E9.ctl
|
||||
|
||||
|
||||
5. Perform initial enrolment at EA
|
||||
|
||||
Initial enrolment needs a canonical (bootstrap) key pair.
|
||||
In a real deployment this key is provisioned by the station manufacturer.
|
||||
For testing you can generate one:
|
||||
|
||||
`pki key generate --out ~/station_key.pem`
|
||||
|
||||
> Wrote /home/user/station_key.pem \
|
||||
> Key type: BrainpoolP256r1 \
|
||||
> Canonical public key: 021234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF
|
||||
|
||||
Use `--key-type` to pick a curve (`BrainpoolP256r1` (default), `NistP256`, `BrainpoolP384r1`) and `--force` to overwrite an existing file.
|
||||
|
||||
`pki enrol init --canonical-id station_name --canonical-keyfile ~/station_key.pem`
|
||||
|
||||
> Root CA 1B5CB4BEBE6FE9E9 \
|
||||
> Canonical identifier: station_name \
|
||||
> Canonical public key X=1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF \
|
||||
> Enroling against EA certificate: D07F4E6D4D1DFA70 \
|
||||
> Enroling at EA URL: http://0.eu-ea.l0.c-its-pki.eu/ \
|
||||
> Stored new EC with HashedId8 = 1234567890ABCDEF \
|
||||
> Station can sign with received EC
|
||||
|
||||
|
||||
6. Check your station's state
|
||||
|
||||
`pki station`
|
||||
|
||||
> Canonical identifier: station_name \
|
||||
> Enrolled: yes \
|
||||
> EC digest: 1234567890ABCDEF \
|
||||
> EC certificate: [available] \
|
||||
> Root CA: 1B5CB4BEBE6FE9E9 \
|
||||
> DC URL: https://0.eu-dc.l0.c-its-pki.eu/
|
||||
|
||||
|
||||
## Fetch Authorization Tickets (ATs) from PKI's AA
|
||||
|
||||
Your station needs to be enrolled with valid EC certificate before you can retrieve ATs.
|
||||
|
||||
The following command fetches a single AT with default settings.
|
||||
The command line option `--permission` is required at least once to determine the requested AT application permissions.
|
||||
However, it is more convenient to set these permissions in the local configuration file (`~/.config/vanetza/pki.cfg`):
|
||||
|
||||
```
|
||||
[authorization.request]
|
||||
permission = ["36:01FFFC", "37:01FFFFFF", "141"]
|
||||
```
|
||||
|
||||
`pki at request`
|
||||
|
||||
> Authorizing against AA AF65A276F2D4EBC9 at http://0.eu-aa.l0.c-its-pki.eu/ \
|
||||
> Stored new AT ABCDEF1234567890 \
|
||||
> ABCDEF1234567890 [valid now] \
|
||||
> valid: 2026-06-06 07:06:29 until 2026-06-13 07:06:29 \
|
||||
> permissions: 36:01FFFC 37:01FFFFFF 141
|
||||
@@ -0,0 +1,241 @@
|
||||
#include "asn1.hpp"
|
||||
#include "exception.hpp"
|
||||
#include "keys.hpp"
|
||||
#include "sha.hpp"
|
||||
#include <vanetza/asn1/security/EtsiTs103097Data.h>
|
||||
#include <vanetza/asn1/support/OCTET_STRING.h>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
ByteBuffer copy(const OCTET_STRING_t& octets)
|
||||
{
|
||||
ByteBuffer buffer(octets.size);
|
||||
std::memcpy(buffer.data(), octets.buf, octets.size);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
const OCTET_STRING_t* get_signed_payload(const Vanetza_Security_Ieee1609Dot2Content_t* content)
|
||||
{
|
||||
const OCTET_STRING_t* payload = nullptr;
|
||||
if (content && content->present == Vanetza_Security_Ieee1609Dot2Content_PR_signedData) {
|
||||
const Vanetza_Security_SignedData_t* data = content->choice.signedData;
|
||||
if (data && data->tbsData && data->tbsData->payload) {
|
||||
const Vanetza_Security_SignedDataPayload_t& spayload = *data->tbsData->payload;
|
||||
if (spayload.data && spayload.data->content &&
|
||||
spayload.data->content->present == Vanetza_Security_Ieee1609Dot2Content_PR_unsecuredData) {
|
||||
payload = &spayload.data->content->choice.unsecuredData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
bool MgmtData::decode(const Vanetza_Security_Opaque_t& opaque)
|
||||
{
|
||||
return wrapper::decode(opaque.buf, opaque.size);
|
||||
}
|
||||
|
||||
MgmtData MgmtData::decode_expecting(const void* buffer, std::size_t size,
|
||||
Vanetza_Security_EtsiTs102941DataContent_PR expected)
|
||||
{
|
||||
MgmtData mgmt;
|
||||
if (!mgmt.decode(buffer, size)) {
|
||||
throw DecodingFailure("decoding management data failed");
|
||||
}
|
||||
if (mgmt->content.present != expected) {
|
||||
throw DecodingFailure("management data contains unexpected content");
|
||||
}
|
||||
return mgmt;
|
||||
}
|
||||
|
||||
MgmtData MgmtData::decode_expecting(const Vanetza_Security_Opaque_t& opaque,
|
||||
Vanetza_Security_EtsiTs102941DataContent_PR expected)
|
||||
{
|
||||
return decode_expecting(opaque.buf, opaque.size, expected);
|
||||
}
|
||||
|
||||
TlmCtlData TlmCtlData::from_buffer(const void* buffer, std::size_t size)
|
||||
{
|
||||
return TlmCtlData { decode_expecting(buffer, size,
|
||||
Vanetza_Security_EtsiTs102941DataContent_PR_certificateTrustListTlm) };
|
||||
}
|
||||
|
||||
TlmCtlData TlmCtlData::from_opaque(const Vanetza_Security_Opaque_t& opaque)
|
||||
{
|
||||
return from_buffer(opaque.buf, opaque.size);
|
||||
}
|
||||
|
||||
RcaCtlData RcaCtlData::from_buffer(const void* buffer, std::size_t size)
|
||||
{
|
||||
return RcaCtlData { decode_expecting(buffer, size,
|
||||
Vanetza_Security_EtsiTs102941DataContent_PR_certificateTrustListRca) };
|
||||
}
|
||||
|
||||
RcaCtlData RcaCtlData::from_opaque(const Vanetza_Security_Opaque_t& opaque)
|
||||
{
|
||||
return from_buffer(opaque.buf, opaque.size);
|
||||
}
|
||||
|
||||
EnrolmentResponseData EnrolmentResponseData::from_buffer(const void* buffer, std::size_t size)
|
||||
{
|
||||
return EnrolmentResponseData { decode_expecting(buffer, size,
|
||||
Vanetza_Security_EtsiTs102941DataContent_PR_enrolmentResponse) };
|
||||
}
|
||||
|
||||
EnrolmentResponseData EnrolmentResponseData::from_opaque(const Vanetza_Security_Opaque_t& opaque)
|
||||
{
|
||||
return from_buffer(opaque.buf, opaque.size);
|
||||
}
|
||||
|
||||
AuthorizationResponseData AuthorizationResponseData::from_buffer(const void* buffer, std::size_t size)
|
||||
{
|
||||
return AuthorizationResponseData { decode_expecting(buffer, size,
|
||||
Vanetza_Security_EtsiTs102941DataContent_PR_authorizationResponse) };
|
||||
}
|
||||
|
||||
AuthorizationResponseData AuthorizationResponseData::from_opaque(const Vanetza_Security_Opaque_t& opaque)
|
||||
{
|
||||
return from_buffer(opaque.buf, opaque.size);
|
||||
}
|
||||
|
||||
void copy(const OCTET_STRING_t& src, ByteBuffer& dst)
|
||||
{
|
||||
static_assert(sizeof(ByteBuffer::value_type) == sizeof(char), "sizes do not match");
|
||||
|
||||
dst.resize(src.size);
|
||||
std::copy_n(src.buf, src.size, dst.data());
|
||||
}
|
||||
|
||||
void copy(const ByteBuffer& src, OCTET_STRING_t& dst)
|
||||
{
|
||||
static_assert(sizeof(ByteBuffer::value_type) == sizeof(char), "sizes do not match");
|
||||
|
||||
auto src_buf = reinterpret_cast<const char*>(src.data());
|
||||
if (OCTET_STRING_fromBuf(&dst, src_buf, src.size()) != 0) {
|
||||
throw std::runtime_error("copying buffer into OCTET_STRING failed");
|
||||
}
|
||||
}
|
||||
|
||||
void copy_left_padded(const ByteBuffer& src, OCTET_STRING_t& dst, std::size_t len)
|
||||
{
|
||||
static_assert(sizeof(ByteBuffer::value_type) == sizeof(char), "sizes do not match");
|
||||
|
||||
if (src.size() > len) {
|
||||
throw std::runtime_error("source bytes exceed desired length of destination buffer");
|
||||
} else if (src.size() == len) {
|
||||
copy(src, dst);
|
||||
assert(dst.size == len);
|
||||
} else {
|
||||
std::string tmp;
|
||||
tmp.assign(len, '\0');
|
||||
std::copy(src.begin(), src.end(), std::next(tmp.begin(), len - src.size()));
|
||||
if (OCTET_STRING_fromBuf(&dst, tmp.data(), tmp.size()) != 0) {
|
||||
throw std::runtime_error("copying buffer into OCTET_STRING failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void fill_curve_point(const PublicKey& key, Vanetza_Security_EccP256CurvePoint_t& point)
|
||||
{
|
||||
switch (key.compression) {
|
||||
case KeyCompression::NoCompression:
|
||||
point.present = Vanetza_Security_EccP256CurvePoint_PR_uncompressedP256;
|
||||
copy_left_padded(key.x, point.choice.uncompressedP256.x, 32);
|
||||
copy_left_padded(key.y, point.choice.uncompressedP256.y, 32);
|
||||
break;
|
||||
case KeyCompression::Y0:
|
||||
point.present = Vanetza_Security_EccP256CurvePoint_PR_compressed_y_0;
|
||||
copy_left_padded(key.x, point.choice.compressed_y_0, 32);
|
||||
break;
|
||||
case KeyCompression::Y1:
|
||||
point.present = Vanetza_Security_EccP256CurvePoint_PR_compressed_y_1;
|
||||
copy_left_padded(key.x, point.choice.compressed_y_1, 32);
|
||||
break;
|
||||
default:
|
||||
throw std::invalid_argument("unknown key compression");
|
||||
}
|
||||
}
|
||||
|
||||
void fill_curve_point(const PublicKey& key, Vanetza_Security_EccP384CurvePoint_t& point)
|
||||
{
|
||||
switch (key.compression) {
|
||||
case KeyCompression::NoCompression:
|
||||
point.present = Vanetza_Security_EccP384CurvePoint_PR_uncompressedP384;
|
||||
copy_left_padded(key.x, point.choice.uncompressedP384.x, 48);
|
||||
copy_left_padded(key.y, point.choice.uncompressedP384.y, 48);
|
||||
break;
|
||||
case KeyCompression::Y0:
|
||||
point.present = Vanetza_Security_EccP384CurvePoint_PR_compressed_y_0;
|
||||
copy_left_padded(key.x, point.choice.compressed_y_0, 48);
|
||||
break;
|
||||
case KeyCompression::Y1:
|
||||
point.present = Vanetza_Security_EccP384CurvePoint_PR_compressed_y_1;
|
||||
copy_left_padded(key.x, point.choice.compressed_y_1, 48);
|
||||
break;
|
||||
default:
|
||||
throw std::invalid_argument("unknown key compression");
|
||||
}
|
||||
}
|
||||
|
||||
Vanetza_Security_HashAlgorithm_t convert(HashAlgorithm from)
|
||||
{
|
||||
switch (from) {
|
||||
case HashAlgorithm::SHA256:
|
||||
return Vanetza_Security_HashAlgorithm_sha256;
|
||||
break;
|
||||
case HashAlgorithm::SHA384:
|
||||
return Vanetza_Security_HashAlgorithm_sha384;
|
||||
break;
|
||||
default:
|
||||
throw std::invalid_argument("unknown hash algorithm");
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuffer to_buffer(const OCTET_STRING_t& input)
|
||||
{
|
||||
return ByteBuffer { input.buf, input.buf + input.size };
|
||||
}
|
||||
|
||||
std::string to_string(const OCTET_STRING_t& input)
|
||||
{
|
||||
if (input.buf) {
|
||||
return std::string { reinterpret_cast<const char*>(input.buf), input.size };
|
||||
} else {
|
||||
return std::string {};
|
||||
}
|
||||
}
|
||||
|
||||
bool operator==(const OCTET_STRING_t& lhs, const ByteBuffer& rhs)
|
||||
{
|
||||
if (lhs.size == rhs.size()) {
|
||||
if (std::memcmp(lhs.buf, rhs.data(), lhs.size) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool operator!=(const OCTET_STRING_t& lhs, const ByteBuffer& rhs)
|
||||
{
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
|
||||
bool operator==(const ByteBuffer& lhs, const OCTET_STRING_t& rhs)
|
||||
{
|
||||
return rhs == lhs;
|
||||
}
|
||||
|
||||
bool operator!=(const ByteBuffer& lhs, const OCTET_STRING_t& rhs)
|
||||
{
|
||||
return !(rhs == lhs);
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,187 @@
|
||||
#pragma once
|
||||
|
||||
#include "keys.hpp"
|
||||
#include "sha.hpp"
|
||||
#include <vanetza/asn1/asn1c_wrapper.hpp>
|
||||
#include <vanetza/asn1/security/EtsiTs102941Data.h>
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <cstring>
|
||||
|
||||
// forward declaration
|
||||
typedef struct OCTET_STRING OCTET_STRING_t;
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
ByteBuffer copy(const OCTET_STRING_t&);
|
||||
const OCTET_STRING_t* get_signed_payload(const Vanetza_Security_Ieee1609Dot2Content_t*);
|
||||
|
||||
class MgmtData : public asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t>
|
||||
{
|
||||
public:
|
||||
using wrapper = asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t>;
|
||||
|
||||
MgmtData() : asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t>(asn_DEF_Vanetza_Security_EtsiTs102941Data)
|
||||
{
|
||||
}
|
||||
|
||||
using wrapper::decode;
|
||||
bool decode(const Vanetza_Security_Opaque_t&);
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Decode and assert the content variant matches `expected`.
|
||||
*
|
||||
* \throws DecodingFailure on decode failure or unexpected content variant
|
||||
*/
|
||||
static MgmtData decode_expecting(const void* buffer, std::size_t size,
|
||||
Vanetza_Security_EtsiTs102941DataContent_PR expected);
|
||||
static MgmtData decode_expecting(const Vanetza_Security_Opaque_t&,
|
||||
Vanetza_Security_EtsiTs102941DataContent_PR expected);
|
||||
};
|
||||
|
||||
// Management data known to carry a TLM certificate trust list.
|
||||
class TlmCtlData : public MgmtData
|
||||
{
|
||||
public:
|
||||
TlmCtlData() = default;
|
||||
|
||||
static TlmCtlData from_buffer(const void* buffer, std::size_t size);
|
||||
static TlmCtlData from_opaque(const Vanetza_Security_Opaque_t&);
|
||||
|
||||
private:
|
||||
explicit TlmCtlData(MgmtData&& base) : MgmtData(std::move(base))
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
// Management data known to carry an RCA certificate trust list.
|
||||
class RcaCtlData : public MgmtData
|
||||
{
|
||||
public:
|
||||
RcaCtlData() = default;
|
||||
|
||||
static RcaCtlData from_buffer(const void* buffer, std::size_t size);
|
||||
static RcaCtlData from_opaque(const Vanetza_Security_Opaque_t&);
|
||||
|
||||
private:
|
||||
explicit RcaCtlData(MgmtData&& base) : MgmtData(std::move(base))
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
// Management data known to carry an enrolment response.
|
||||
class EnrolmentResponseData : public MgmtData
|
||||
{
|
||||
public:
|
||||
EnrolmentResponseData() = default;
|
||||
|
||||
static EnrolmentResponseData from_buffer(const void* buffer, std::size_t size);
|
||||
static EnrolmentResponseData from_opaque(const Vanetza_Security_Opaque_t&);
|
||||
|
||||
private:
|
||||
explicit EnrolmentResponseData(MgmtData&& base) : MgmtData(std::move(base))
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
// Management data known to carry an authorization response.
|
||||
class AuthorizationResponseData : public MgmtData
|
||||
{
|
||||
public:
|
||||
AuthorizationResponseData() = default;
|
||||
|
||||
static AuthorizationResponseData from_buffer(const void* buffer, std::size_t size);
|
||||
static AuthorizationResponseData from_opaque(const Vanetza_Security_Opaque_t&);
|
||||
|
||||
private:
|
||||
explicit AuthorizationResponseData(MgmtData&& base) : MgmtData(std::move(base))
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
void fill_curve_point(const PublicKey&, Vanetza_Security_EccP256CurvePoint_t&);
|
||||
void fill_curve_point(const PublicKey&, Vanetza_Security_EccP384CurvePoint_t&);
|
||||
|
||||
void copy(const OCTET_STRING_t& src, ByteBuffer& dst);
|
||||
void copy(const ByteBuffer& src, OCTET_STRING_t& dst);
|
||||
void copy_left_padded(const ByteBuffer& src, OCTET_STRING_t& dst, std::size_t len);
|
||||
|
||||
Vanetza_Security_HashAlgorithm_t convert(HashAlgorithm);
|
||||
ByteBuffer to_buffer(const OCTET_STRING_t&);
|
||||
|
||||
std::string to_string(const OCTET_STRING_t&);
|
||||
|
||||
/**
|
||||
* \brief strong-typed wrapper around asn1c-generated enum
|
||||
*
|
||||
* \tparam Tag generated C enum
|
||||
* \tparam T underlying type
|
||||
*/
|
||||
template<typename Tag, typename T = long> struct asn1c_enum
|
||||
{
|
||||
T value;
|
||||
constexpr explicit asn1c_enum(T v = T {}) : value(v)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
template<typename Tag, typename T> constexpr bool operator==(asn1c_enum<Tag, T> l, asn1c_enum<Tag, T> r)
|
||||
{
|
||||
return l.value == r.value;
|
||||
}
|
||||
|
||||
template<typename Tag, typename T> constexpr bool operator!=(asn1c_enum<Tag, T> l, asn1c_enum<Tag, T> r)
|
||||
{
|
||||
return l.value != r.value;
|
||||
}
|
||||
|
||||
template<typename Tag, typename T> constexpr bool operator==(asn1c_enum<Tag, T> l, Tag r)
|
||||
{
|
||||
return l.value == static_cast<T>(r);
|
||||
}
|
||||
|
||||
template<typename Tag, typename T> constexpr bool operator!=(asn1c_enum<Tag, T> l, Tag r)
|
||||
{
|
||||
return l.value != static_cast<T>(r);
|
||||
}
|
||||
|
||||
template<typename Tag, typename T> constexpr bool operator==(Tag l, asn1c_enum<Tag, T> r)
|
||||
{
|
||||
return static_cast<T>(l) == r.value;
|
||||
}
|
||||
|
||||
template<typename Tag, typename T> constexpr bool operator!=(Tag l, asn1c_enum<Tag, T> r)
|
||||
{
|
||||
return static_cast<T>(l) != r.value;
|
||||
}
|
||||
|
||||
bool operator==(const OCTET_STRING_t&, const ByteBuffer&);
|
||||
bool operator!=(const OCTET_STRING_t&, const ByteBuffer&);
|
||||
bool operator==(const ByteBuffer&, const OCTET_STRING_t&);
|
||||
bool operator!=(const ByteBuffer&, const OCTET_STRING_t&);
|
||||
|
||||
template<std::size_t N> bool operator==(const OCTET_STRING_t& lhs, const std::array<std::uint8_t, N>& rhs)
|
||||
{
|
||||
return lhs.size == N && std::memcmp(lhs.buf, rhs.data(), N) == 0;
|
||||
}
|
||||
|
||||
template<std::size_t N> bool operator!=(const OCTET_STRING_t& lhs, const std::array<std::uint8_t, N>& rhs)
|
||||
{
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
|
||||
template<std::size_t N> bool operator==(const std::array<std::uint8_t, N>& lhs, const OCTET_STRING_t& rhs)
|
||||
{
|
||||
return rhs == lhs;
|
||||
}
|
||||
|
||||
template<std::size_t N> bool operator!=(const std::array<std::uint8_t, N>& lhs, const OCTET_STRING_t& rhs)
|
||||
{
|
||||
return !(rhs == lhs);
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,179 @@
|
||||
#include "at_request.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "encrypted_data.hpp"
|
||||
#include "exception.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "psid_ssp.hpp"
|
||||
#include "security_module.hpp"
|
||||
#include "sha.hpp"
|
||||
#include "signed_builder.hpp"
|
||||
#include <vanetza/asn1/security/EtsiTs102941Data.h>
|
||||
#include <vanetza/asn1/security/InnerAtRequest.h>
|
||||
#include <vanetza/asn1/security/SharedAtRequest.h>
|
||||
#include <vanetza/asn1/security/ValidityPeriod.h>
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
#include <vanetza/security/v3/basic_elements.hpp>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
void add_app_permissions(Vanetza_Security_CertificateSubjectAttributes_t& csa, const std::list<PsidSsp>& permissions)
|
||||
{
|
||||
csa.appPermissions = asn1::allocate<Vanetza_Security_SequenceOfPsidSsp_t>();
|
||||
for (const auto& perm : permissions) {
|
||||
auto* psid_ssp = asn1::allocate<Vanetza_Security_PsidSsp_t>();
|
||||
psid_ssp->psid = perm.psid;
|
||||
if (!perm.ssp.empty()) {
|
||||
psid_ssp->ssp = asn1::allocate<Vanetza_Security_ServiceSpecificPermissions_t>();
|
||||
psid_ssp->ssp->present = Vanetza_Security_ServiceSpecificPermissions_PR_bitmapSsp;
|
||||
copy(perm.ssp, psid_ssp->ssp->choice.bitmapSsp);
|
||||
}
|
||||
if (asn_sequence_add(csa.appPermissions, psid_ssp) != 0) {
|
||||
throw std::runtime_error("adding app permission to InnerAtRequest failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt `plaintext` to `recipient_certificate` using a fresh ECIES context
|
||||
// and populate `dest` (an EtsiTs103097Data-Encrypted value member of a parent
|
||||
// struct) in place. Precondition: `dest` is zero-initialised.
|
||||
void encrypt_into(Vanetza_Security_EtsiTs103097Data_Encrypted_85P0_t& dest, SecurityModule& security,
|
||||
const ByteBuffer& plaintext, const Certificate& recipient_certificate)
|
||||
{
|
||||
boost::optional<PublicKey> enc_key = recipient_certificate.get_encryption_key();
|
||||
if (!enc_key) {
|
||||
throw DecodingFailure("recipient certificate has no encryption key");
|
||||
}
|
||||
Sha256Hash recipient_hash = calculate_sha256_hash(security, recipient_certificate);
|
||||
auto ecies = security.create_ecies_context(*enc_key, recipient_hash);
|
||||
|
||||
EncryptedData::init(dest);
|
||||
EncryptedData::set_aes_ccm_ciphertext(dest, *ecies, plaintext);
|
||||
EncryptedData::append_recipient_info(dest, *ecies, recipient_certificate.calculate_hashed_id8(security));
|
||||
}
|
||||
|
||||
void validate(const AuthorizationRequestParameters& p)
|
||||
{
|
||||
if (!p.ec) {
|
||||
throw std::invalid_argument("AuthorizationRequest: ec is required");
|
||||
}
|
||||
if (!p.ea_certificate) {
|
||||
throw std::invalid_argument("AuthorizationRequest: ea_certificate is required");
|
||||
}
|
||||
if (!p.aa_certificate) {
|
||||
throw std::invalid_argument("AuthorizationRequest: aa_certificate is required");
|
||||
}
|
||||
if (p.permissions.empty()) {
|
||||
throw std::invalid_argument("AuthorizationRequest: at least one permission must be requested");
|
||||
}
|
||||
if (p.hash_algo != HashAlgorithm::SHA256) {
|
||||
throw std::invalid_argument("AuthorizationRequest: only SHA-256 is supported");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ByteBuffer build_signed_authorization_request(SecurityModule& security, const AuthorizationRequestParameters& params)
|
||||
{
|
||||
validate(params);
|
||||
|
||||
// Outer EtsiTs102941Data{authorizationRequest = InnerAtRequest}.
|
||||
// We populate the InnerAtRequest fields in place inside the wrapper.
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
|
||||
mgmt->version = Vanetza_Security_Version_v1;
|
||||
mgmt->content.present = Vanetza_Security_EtsiTs102941DataContent_PR_authorizationRequest;
|
||||
Vanetza_Security_InnerAtRequest_t& iar = mgmt->content.choice.authorizationRequest;
|
||||
|
||||
// 1. Public keys carried in the request
|
||||
// verificationKey is required and will be the new AT's verification key
|
||||
set_verification_key(iar.publicKeys.verificationKey, params.verification_key);
|
||||
// encryptionKey is optional for future encrypted communication
|
||||
if (params.at_encryption_key) {
|
||||
iar.publicKeys.encryptionKey = asn1::allocate<Vanetza_Security_PublicEncryptionKey_t>();
|
||||
set_encryption_key(*iar.publicKeys.encryptionKey, *params.at_encryption_key);
|
||||
}
|
||||
|
||||
// 2. Random 32-byte hmacKey
|
||||
ByteBuffer hmac_key = security.generate_nonce(32);
|
||||
OCTET_STRING_fromBuf(&iar.hmacKey, reinterpret_cast<const char*>(hmac_key.data()), hmac_key.size());
|
||||
|
||||
// 3. SharedAtRequest
|
||||
Vanetza_Security_SharedAtRequest_t& sar = iar.sharedAtRequest;
|
||||
HashedId8 ea_hid8 = params.ea_certificate->calculate_hashed_id8(security);
|
||||
OCTET_STRING_fromBuf(&sar.eaId, reinterpret_cast<const char*>(ea_hid8.octets.data()), ea_hid8.octets.size());
|
||||
sar.certificateFormat = Vanetza_Security_CertificateFormat_ts103097v131;
|
||||
add_app_permissions(sar.requestedSubjectAttributes, params.permissions);
|
||||
if (params.validity_period) {
|
||||
sar.requestedSubjectAttributes.validityPeriod = asn1::allocate<Vanetza_Security_ValidityPeriod_t>();
|
||||
auto* vp = sar.requestedSubjectAttributes.validityPeriod;
|
||||
vp->start = security::v3::convert_time32(params.validity_period->start);
|
||||
vp->duration.present = Vanetza_Security_Duration_PR_hours;
|
||||
vp->duration.choice.hours = params.validity_period->duration.count();
|
||||
}
|
||||
|
||||
// 4. keyTag = first 16 bytes of HMAC-SHA256(hmacKey, verifyKey [|| encKey])
|
||||
ByteBuffer verify_oer =
|
||||
asn1::encode_oer(asn_DEF_Vanetza_Security_PublicVerificationKey, &iar.publicKeys.verificationKey);
|
||||
ByteBuffer hmac_input = verify_oer;
|
||||
if (iar.publicKeys.encryptionKey) {
|
||||
ByteBuffer enc_oer =
|
||||
asn1::encode_oer(asn_DEF_Vanetza_Security_PublicEncryptionKey, iar.publicKeys.encryptionKey);
|
||||
hmac_input.insert(hmac_input.end(), enc_oer.begin(), enc_oer.end());
|
||||
}
|
||||
ByteBuffer full_tag = security.calculate_hmac_sha256(hmac_key, hmac_input);
|
||||
OCTET_STRING_fromBuf(&sar.keyTag, reinterpret_cast<const char*>(full_tag.data()), 16);
|
||||
|
||||
// 5. EC proof: external-payload signed data over SharedAtRequest, signed by EC.
|
||||
ByteBuffer sar_encoded = asn1::encode_oer(asn_DEF_Vanetza_Security_SharedAtRequest, &sar);
|
||||
SignedData ec_signed =
|
||||
create_external_signed(sar_encoded, security, params.ec->get_public_key(), params.hash_algo, params.ec);
|
||||
ByteBuffer ec_signed_encoded = ec_signed.encode();
|
||||
|
||||
// 6. Encrypt the EC proof to the EA → encryptedEcSignature
|
||||
iar.ecSignature.present = Vanetza_Security_EcSignature_PR_encryptedEcSignature;
|
||||
encrypt_into(iar.ecSignature.choice.encryptedEcSignature, security, ec_signed_encoded, *params.ea_certificate);
|
||||
|
||||
if (!mgmt.validate()) {
|
||||
throw std::runtime_error("AuthorizationRequest: constructed InnerAtRequest is invalid");
|
||||
}
|
||||
ByteBuffer mgmt_encoded = mgmt.encode();
|
||||
|
||||
if (!params.include_pop) {
|
||||
return mgmt_encoded;
|
||||
}
|
||||
|
||||
// 7. POP wrap: EtsiTs103097Data-Signed (signer = self) signed with the new AT verification key
|
||||
SignedData pop_signed = create_signed(mgmt_encoded, security, params.verification_key, params.hash_algo, nullptr);
|
||||
return pop_signed.encode();
|
||||
}
|
||||
|
||||
EncryptedData build_authorization_request(SecurityModule& security, const AuthorizationRequestParameters& params)
|
||||
{
|
||||
validate(params);
|
||||
|
||||
ByteBuffer plaintext = build_signed_authorization_request(security, params);
|
||||
|
||||
boost::optional<PublicKey> aa_enc_key = params.aa_certificate->get_encryption_key();
|
||||
if (!aa_enc_key) {
|
||||
throw DecodingFailure("AuthorizationRequest: AA certificate has no encryption key");
|
||||
}
|
||||
Sha256Hash aa_hash = calculate_sha256_hash(security, *params.aa_certificate);
|
||||
auto ecies_unique = security.create_ecies_context(*aa_enc_key, aa_hash);
|
||||
std::shared_ptr<SecurityModule::EciesContext> ecies { std::move(ecies_unique) };
|
||||
|
||||
EncryptedData encrypted { ecies };
|
||||
encrypted.generate_ciphertext(plaintext);
|
||||
encrypted.add_recipient_info(params.aa_certificate->calculate_hashed_id8(security));
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include "keys.hpp"
|
||||
#include "psid_ssp.hpp"
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <vanetza/common/clock.hpp>
|
||||
#include <vanetza/security/hash_algorithm.hpp>
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <chrono>
|
||||
#include <list>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class Certificate;
|
||||
class EncryptedData;
|
||||
class SecurityModule;
|
||||
|
||||
using security::HashAlgorithm;
|
||||
|
||||
/**
|
||||
* \brief Hint for sharedAtRequest.requestedSubjectAttributes.validityPeriod.
|
||||
* \see IEEE 1609.2 Time32 / Duration::hours
|
||||
*/
|
||||
struct ValidityPeriodHint
|
||||
{
|
||||
Clock::time_point start;
|
||||
std::chrono::hours duration;
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Parameters for an AuthorizationRequest (encryptedEcSignature variant).
|
||||
* \see TS 102 941 §6.2.3.3.1
|
||||
*/
|
||||
struct AuthorizationRequestParameters
|
||||
{
|
||||
const Certificate* ec = nullptr; // current EC, signs the inner EC proof; mandatory
|
||||
const Certificate* ea_certificate = nullptr; // recipient of encryptedEcSignature; mandatory
|
||||
const Certificate* aa_certificate = nullptr; // recipient of outer encryption; mandatory
|
||||
PublicKey verification_key; // fresh key to be certified; private key in SecurityModule; mandatory
|
||||
boost::optional<PublicKey> at_encryption_key; // optional encryption key embedded in the AT
|
||||
std::list<PsidSsp> permissions; // requested PSID/SSP set; must be non-empty
|
||||
HashAlgorithm hash_algo = HashAlgorithm::SHA256; // EC proof and extDataHash; SHA-256 only
|
||||
boost::optional<ValidityPeriodHint> validity_period; // optional; AA decides within CP §7.2.1 bounds
|
||||
bool include_pop = true; // send AuthorizationRequestMessageWithPop; required by deployed AAs
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Build the inner EtsiTs102941Data{authorizationRequest} plaintext.
|
||||
*
|
||||
* Exposed for testing; production code should call build_authorization_request().
|
||||
*/
|
||||
ByteBuffer build_signed_authorization_request(SecurityModule& security,
|
||||
const AuthorizationRequestParameters& parameters);
|
||||
|
||||
/**
|
||||
* \brief Build the AA-encrypted authorization request.
|
||||
*
|
||||
* Call .encode() on the result for the OER bytes to POST as `application/x-its-request`.
|
||||
*/
|
||||
EncryptedData build_authorization_request(SecurityModule& security, const AuthorizationRequestParameters& parameters);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,74 @@
|
||||
#include "at_response.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "exception.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "security_module.hpp"
|
||||
#include "signed_data.hpp"
|
||||
#include "validation.hpp"
|
||||
#include <vanetza/asn1/security/EtsiTs102941Data.h>
|
||||
#include <vanetza/asn1/security/InnerAtResponse.h>
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
AuthorizationResponse parse_authorization_response(SecurityModule& security, const ByteBuffer& decrypted,
|
||||
const Certificate& aa_certificate)
|
||||
{
|
||||
SignedData outer;
|
||||
if (!outer.decode(decrypted)) {
|
||||
throw DecodingFailure("decoding signed authorization response failed");
|
||||
}
|
||||
if (outer->content->present != Vanetza_Security_Ieee1609Dot2Content_PR_signedData) {
|
||||
throw DecodingFailure("authorization response content is not signedData");
|
||||
}
|
||||
const Vanetza_Security_SignedData_t& sd = *outer->content->choice.signedData;
|
||||
|
||||
// Per TS 102 941 §6.2.3.3.2 the AA signs with signer = digest(AA cert).
|
||||
const HashedId8 aa_hid8 = aa_certificate.calculate_hashed_id8(security);
|
||||
if (sd.signer.present != Vanetza_Security_SignerIdentifier_PR_digest) {
|
||||
throw DecodingFailure("authorization response must have signer = digest");
|
||||
}
|
||||
if (sd.signer.choice.digest != aa_hid8.octets) {
|
||||
throw VerificationFailure("authorization response signer digest does not match AA certificate");
|
||||
}
|
||||
|
||||
if (sd.tbsData->headerInfo.psid != aid::SCR) {
|
||||
throw DecodingFailure("authorization response PSID is not SCR");
|
||||
}
|
||||
|
||||
if (!validate(security, sd, aa_certificate.raw())) {
|
||||
throw VerificationFailure("authorization response signature does not verify against AA certificate");
|
||||
}
|
||||
|
||||
const Vanetza_Security_Opaque_t* inner_opaque = get_signed_payload(outer->content);
|
||||
if (!inner_opaque) {
|
||||
throw DecodingFailure("authorization response carries no unsecured signed payload");
|
||||
}
|
||||
|
||||
AuthorizationResponseData inner = AuthorizationResponseData::from_opaque(*inner_opaque);
|
||||
if (inner->version != Vanetza_Security_Version_v1) {
|
||||
throw DecodingFailure("inner EtsiTs102941Data version is not v1");
|
||||
}
|
||||
|
||||
const Vanetza_Security_InnerAtResponse_t& at_resp = inner->content.choice.authorizationResponse;
|
||||
|
||||
AuthorizationResponse result;
|
||||
result.code = AuthorizationResponseCode { at_resp.responseCode };
|
||||
result.request_hash.assign(at_resp.requestHash.buf, at_resp.requestHash.buf + at_resp.requestHash.size);
|
||||
|
||||
if (at_resp.certificate) {
|
||||
result.certificate = Certificate(*at_resp.certificate);
|
||||
}
|
||||
|
||||
if (at_resp.responseCode == Vanetza_Security_AuthorizationResponseCode_ok && !result.certificate) {
|
||||
throw DecodingFailure("authorization response code is ok but no certificate is included");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include "certificate.hpp"
|
||||
#include "response_codes.hpp"
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <boost/optional/optional.hpp>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class SecurityModule;
|
||||
|
||||
// Result of parsing a decrypted AuthorizationResponse payload as specified
|
||||
// in ETSI TS 102 941 §6.2.3.3.2.
|
||||
struct AuthorizationResponse
|
||||
{
|
||||
// Response code from InnerAtResponse. Parsing succeeds regardless of
|
||||
// the code value; the caller decides how to act on non-ok results.
|
||||
AuthorizationResponseCode code;
|
||||
|
||||
// requestHash from InnerAtResponse (SHA-256 prefix of the request).
|
||||
ByteBuffer request_hash;
|
||||
|
||||
// New Authorization Ticket returned by the AA. Present iff the parser
|
||||
// was able to decode a certificate; the caller must still check that
|
||||
// `code` is `ok` before relying on it.
|
||||
boost::optional<Certificate> certificate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a decrypted AuthorizationResponse per TS 102 941 §6.2.3.3.2.
|
||||
*
|
||||
* Verifies:
|
||||
* - outer Ieee1609Dot2Data is signedData
|
||||
* - signer = digest, matching HashedId8(aa_certificate)
|
||||
* - tbsData.headerInfo.psid == aid::SCR
|
||||
* - outer signature verifies against aa_certificate
|
||||
* - inner EtsiTs102941Data.version == v1
|
||||
* - inner content variant is authorizationResponse
|
||||
*
|
||||
* If `code == ok` the message MUST carry a certificate per TS 102 941;
|
||||
* for non-ok codes the parser returns normally with whatever certificate
|
||||
* (if any) was provided.
|
||||
*
|
||||
* \throws DecodingFailure on structural failure or missing required certificate
|
||||
* \throws VerificationFailure on signer-digest or signature mismatch
|
||||
*/
|
||||
AuthorizationResponse parse_authorization_response(SecurityModule& security, const ByteBuffer& decrypted,
|
||||
const Certificate& aa_certificate);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,343 @@
|
||||
#include "authorization.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "at_request.hpp"
|
||||
#include "at_response.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "certificate_trust_list.hpp"
|
||||
#include "ea_request.hpp"
|
||||
#include "encrypted_data.hpp"
|
||||
#include "exception.hpp"
|
||||
#include "hexstring.hpp"
|
||||
#include "http.hpp"
|
||||
#include "prune_command.hpp"
|
||||
#include "psid_ssp.hpp"
|
||||
#include "response_codes.hpp"
|
||||
#include "time.hpp"
|
||||
#include "validation.hpp"
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
#include <CLI/CLI.hpp>
|
||||
#include <boost/date_time/posix_time/posix_time.hpp>
|
||||
#include <boost/date_time/posix_time/posix_time_io.hpp>
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct Context
|
||||
{
|
||||
Context(const MainConfig& c) : cfg(c)
|
||||
{
|
||||
}
|
||||
|
||||
std::string url(const AuthorizationAuthority& aa) const
|
||||
{
|
||||
return resolve_url(aa.access_point, url_override);
|
||||
}
|
||||
|
||||
const MainConfig& cfg;
|
||||
std::string url_override;
|
||||
std::function<void()> action;
|
||||
|
||||
KeyType key_type = KeyType::BrainpoolP256r1;
|
||||
HashAlgorithm hash_algo = HashAlgorithm::SHA256;
|
||||
std::list<PsidSsp> permissions;
|
||||
unsigned count = 1;
|
||||
Clock::time_point validity_start = current_time();
|
||||
std::chrono::hours validity_duration { 24 * 7 }; // 1 week, the CP §7.2.1 max
|
||||
bool custom_validity = false; // true if user set --validity-start/-duration; else no hint, AA picks
|
||||
};
|
||||
|
||||
const std::map<std::string, KeyType> key_type_map = {
|
||||
{ "NistP256", KeyType::NistP256 },
|
||||
{ "BrainpoolP256r1", KeyType::BrainpoolP256r1 },
|
||||
{ "BrainpoolP384r1", KeyType::BrainpoolP384r1 },
|
||||
};
|
||||
|
||||
const std::map<std::string, HashAlgorithm> hash_algo_map = {
|
||||
{ "SHA256", HashAlgorithm::SHA256 },
|
||||
{ "SHA384", HashAlgorithm::SHA384 },
|
||||
};
|
||||
|
||||
void list_tickets(Context& ctx);
|
||||
void request_ticket(Context& ctx);
|
||||
|
||||
} // namespace
|
||||
|
||||
std::shared_ptr<CLI::App> build_authorization_command(const MainConfig& cfg)
|
||||
{
|
||||
auto ctx = std::make_shared<Context>(cfg);
|
||||
auto app = std::make_shared<CLI::App>("authorization tickets (AT)", "authorization");
|
||||
app->alias("auth");
|
||||
app->alias("at");
|
||||
app->add_option("--url", ctx->url_override, "override the Authorization Authority URL");
|
||||
|
||||
auto list = app->add_subcommand("list", "list stored authorization tickets");
|
||||
list->callback([ctx]() { ctx->action = [ctx]() { list_tickets(*ctx); }; });
|
||||
|
||||
auto request = app->add_subcommand("request", "request a new authorization ticket");
|
||||
request->fallthrough();
|
||||
request->add_option("--key-type", ctx->key_type, "type of generated verification key")
|
||||
->default_val(KeyType::BrainpoolP256r1)
|
||||
->capture_default_str()
|
||||
->transform(CLI::CheckedTransformer(key_type_map, CLI::ignore_case));
|
||||
request->add_option("--hash-algorithm", ctx->hash_algo, "hash algorithm for signing")
|
||||
->default_val(HashAlgorithm::SHA256)
|
||||
->capture_default_str()
|
||||
->transform(CLI::CheckedTransformer(hash_algo_map, CLI::ignore_case));
|
||||
request->add_option("--permission", ctx->permissions, "requested AT permission (PSID[:HEX_SSP]); repeatable")
|
||||
->required();
|
||||
request->add_option("--count", ctx->count, "number of ATs to request in this run")
|
||||
->default_val(1)
|
||||
->capture_default_str()
|
||||
->check(CLI::PositiveNumber);
|
||||
CLI::callback_t validity_start_cb = [ctx](const CLI::results_t& v) -> bool {
|
||||
if (v.size() != 1) {
|
||||
return false;
|
||||
}
|
||||
auto parsed = parse_validity_start(v[0]);
|
||||
if (!parsed) {
|
||||
return false;
|
||||
}
|
||||
ctx->validity_start = *parsed;
|
||||
ctx->custom_validity = true;
|
||||
return true;
|
||||
};
|
||||
CLI::callback_t validity_duration_cb = [ctx](const CLI::results_t& v) -> bool {
|
||||
if (v.size() != 1) {
|
||||
return false;
|
||||
}
|
||||
auto parsed = parse_duration_hours(v[0]);
|
||||
if (!parsed) {
|
||||
return false;
|
||||
}
|
||||
ctx->validity_duration = *parsed;
|
||||
ctx->custom_validity = true;
|
||||
return true;
|
||||
};
|
||||
request
|
||||
->add_option("--validity-start", validity_start_cb,
|
||||
"AT validity start: 'YYYY-MM-DD[THH:MM:SS]' or relative '+Nd/h/w'")
|
||||
->default_str("now");
|
||||
request->add_option("--validity-duration", validity_duration_cb, "AT validity duration: 'Nh', 'Nd', 'Nw'")
|
||||
->default_str("1w");
|
||||
request->callback([ctx]() { ctx->action = [ctx]() { request_ticket(*ctx); }; });
|
||||
|
||||
auto prune = app->add_subcommand("prune", "delete expired authorization tickets");
|
||||
auto prune_dry = prune->add_flag("--dry-run,-n", "list only; do not delete");
|
||||
prune->callback([ctx, prune_dry]() {
|
||||
const bool dry_run = prune_dry->as<bool>();
|
||||
ctx->action = [ctx, dry_run]() { prune_expired_tickets(ctx->cfg, current_time(), dry_run); };
|
||||
});
|
||||
|
||||
// Default when no subcommand is picked: list.
|
||||
app->final_callback([ctx]() {
|
||||
if (!ctx->action) {
|
||||
ctx->action = [ctx]() { list_tickets(*ctx); };
|
||||
}
|
||||
ctx->action();
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct AuthoritiesFromCtl
|
||||
{
|
||||
EnrolmentAuthority ea;
|
||||
AuthorizationAuthority aa;
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Look up the EA (encryptedEcSignature recipient) and AA (outer encryption)
|
||||
* from the current Root CA's stored CTL.
|
||||
* \throws if the CTL is missing, either authority is not listed, or encryption keys are missing
|
||||
*/
|
||||
AuthoritiesFromCtl lookup_authorities(Context& ctx)
|
||||
{
|
||||
auto processor = process_stored_ctl(*ctx.cfg.trust_lists, ctx.cfg.security, ctx.cfg.root_ca_hid8);
|
||||
return AuthoritiesFromCtl { require_enrolment_authority(processor, ctx.cfg.root_ca_hid8),
|
||||
require_authorization_authority(processor, ctx.cfg.root_ca_hid8) };
|
||||
}
|
||||
|
||||
/// \brief Format one appPermissions entry as 'PSID[:HEX_SSP]', mirroring --permission.
|
||||
std::string format_app_permission(const Vanetza_Security_PsidSsp_t& entry)
|
||||
{
|
||||
std::string out = std::to_string(entry.psid);
|
||||
if (entry.ssp) {
|
||||
const auto& ssp = *entry.ssp;
|
||||
const std::uint8_t* buf = nullptr;
|
||||
std::size_t len = 0;
|
||||
if (ssp.present == Vanetza_Security_ServiceSpecificPermissions_PR_bitmapSsp) {
|
||||
buf = ssp.choice.bitmapSsp.buf;
|
||||
len = ssp.choice.bitmapSsp.size;
|
||||
} else if (ssp.present == Vanetza_Security_ServiceSpecificPermissions_PR_opaque) {
|
||||
buf = ssp.choice.opaque.buf;
|
||||
len = ssp.choice.opaque.size;
|
||||
}
|
||||
if (buf && len > 0) {
|
||||
out += ":";
|
||||
out += hexstring(buf, len);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/// \brief Print one stored AT to stdout: header line, validity range, appPermissions.
|
||||
void describe_ticket(Context& ctx, const HashedId8& at_id, const Certificate& at)
|
||||
{
|
||||
Clock::time_point start = at.valid_since();
|
||||
Clock::time_point stop = at.valid_until();
|
||||
Clock::time_point now = current_time();
|
||||
const char* state = (now < start) ? "[not yet valid]" : (now > stop) ? "[expired]" : "[valid now]";
|
||||
std::cout << hexstring(at_id);
|
||||
auto name = at.get_name();
|
||||
if (!name.empty()) {
|
||||
std::cout << " " << name;
|
||||
}
|
||||
std::cout << " " << state << "\n";
|
||||
std::cout << " valid: " << Clock::at(start) << " until " << Clock::at(stop) << "\n";
|
||||
|
||||
const auto* ap = at.raw().toBeSigned.appPermissions;
|
||||
if (ap && ap->list.count > 0) {
|
||||
std::cout << " permissions:";
|
||||
for (int i = 0; i < ap->list.count; ++i) {
|
||||
if (ap->list.array[i]) {
|
||||
std::cout << " " << format_app_permission(*ap->list.array[i]);
|
||||
}
|
||||
}
|
||||
std::cout << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
/// \brief Print every stored AT plus a count line.
|
||||
void list_tickets(Context& ctx)
|
||||
{
|
||||
std::size_t count = 0;
|
||||
for (const auto& id : ctx.cfg.tickets->list()) {
|
||||
if (count == 0) {
|
||||
std::cout << "Stored authorization ticket(s):\n";
|
||||
}
|
||||
++count;
|
||||
auto at = ctx.cfg.tickets->fetch(id);
|
||||
if (!at) {
|
||||
std::cout << hexstring(id) << " [unreadable]\n";
|
||||
continue;
|
||||
}
|
||||
describe_ticket(ctx, id, *at);
|
||||
}
|
||||
if (count == 0) {
|
||||
std::cout << "No authorization tickets stored.\n";
|
||||
} else {
|
||||
std::cout << count << " authorization ticket(s) total.\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief One AA round-trip: fresh AT key, build, POST, decrypt, parse, store.
|
||||
*
|
||||
* EA/AA are looked up by the caller so they can be reused across a batch.
|
||||
* \throws on any failure
|
||||
*/
|
||||
void request_one_ticket(Context& ctx, const Certificate& ec, const AuthoritiesFromCtl& auth,
|
||||
const boost::optional<ValidityPeriodHint>& validity)
|
||||
{
|
||||
ScopedKeyPair scoped_at_key(*ctx.cfg.security, ctx.key_type);
|
||||
|
||||
AuthorizationRequestParameters params;
|
||||
params.ec = &ec;
|
||||
params.ea_certificate = &auth.ea.certificate;
|
||||
params.aa_certificate = &auth.aa.certificate;
|
||||
params.verification_key = scoped_at_key.public_key();
|
||||
params.permissions = ctx.permissions;
|
||||
params.hash_algo = ctx.hash_algo;
|
||||
params.validity_period = validity;
|
||||
|
||||
EncryptedData encrypted_data = build_authorization_request(*ctx.cfg.security, params);
|
||||
|
||||
auto query = HttpQuery::from_url(ctx.url(auth.aa));
|
||||
auto encoded = encrypted_data.encode();
|
||||
auto req_hash = ctx.cfg.security->calculate_sha256_hash(encoded.data(), encoded.size());
|
||||
auto response = http_post(query, "application/x-its-request", encoded);
|
||||
|
||||
if (response.result() != boost::beast::http::status::ok) {
|
||||
throw HttpException("AA returned an unexpected HTTP status for the authorization request",
|
||||
std::move(response));
|
||||
} else if (response[boost::beast::http::field::content_type] != "application/x-its-response") {
|
||||
throw HttpException("expected application/x-its-response");
|
||||
}
|
||||
|
||||
if (!encrypted_data.decode(response.body().data(), response.body().size())) {
|
||||
throw DecodingFailure("decoding encrypted AT response failed");
|
||||
}
|
||||
ByteBuffer dec_resp = encrypted_data.decrypt();
|
||||
|
||||
AuthorizationResponse at_resp = parse_authorization_response(*ctx.cfg.security, dec_resp, auth.aa.certificate);
|
||||
if (!check_request_hash(req_hash, at_resp.request_hash)) {
|
||||
throw VerificationFailure("mismatch between request and response hash");
|
||||
}
|
||||
if (at_resp.code != Vanetza_Security_AuthorizationResponseCode_ok) {
|
||||
throw std::runtime_error("AA response code: " + vanetza::pki::to_string(at_resp.code));
|
||||
}
|
||||
|
||||
Certificate& at = *at_resp.certificate;
|
||||
HashedId8 at_id = at.calculate_hashed_id8(*ctx.cfg.security);
|
||||
scoped_at_key.commit();
|
||||
ctx.cfg.tickets->store(at);
|
||||
std::cout << "Stored new AT " << hexstring(at_id) << "\n";
|
||||
describe_ticket(ctx, at_id, at);
|
||||
}
|
||||
|
||||
/// \brief Validate preconditions, look up the AA, then request `ctx.count` ATs in sequence.
|
||||
void request_ticket(Context& ctx)
|
||||
{
|
||||
if (ctx.permissions.empty()) {
|
||||
throw UsageError("at least one --permission must be specified");
|
||||
}
|
||||
|
||||
// Preconditions and authority lookup are shared across the whole batch.
|
||||
auto ec_id = ctx.cfg.station->get_ec_identifier();
|
||||
if (!ec_id) {
|
||||
throw UsageError("no EC available", "run 'enrolment initial' first");
|
||||
}
|
||||
auto ec = ctx.cfg.enrolment_credentials->fetch(*ec_id);
|
||||
if (!ec) {
|
||||
throw UsageError("EC certificate missing from storage", "re-run 'enrolment initial'");
|
||||
}
|
||||
if (!ctx.cfg.security->can_sign(ec->get_public_key())) {
|
||||
throw UsageError("no EC private key available", "re-run 'enrolment initial'");
|
||||
}
|
||||
|
||||
// Send a validityPeriod hint only when the user explicitly set --validity-start or --validity-duration.
|
||||
// Otherwise leave it out so the AA picks unilaterally.
|
||||
boost::optional<ValidityPeriodHint> validity;
|
||||
if (ctx.custom_validity) {
|
||||
validity = ValidityPeriodHint { ctx.validity_start, ctx.validity_duration };
|
||||
}
|
||||
|
||||
AuthoritiesFromCtl auth = lookup_authorities(ctx);
|
||||
HashedId8 aa_hid8 = auth.aa.certificate.calculate_hashed_id8(*ctx.cfg.security);
|
||||
std::cout << "Authorizing against AA " << hexstring(aa_hid8) << " at " << ctx.url(auth.aa) << "\n";
|
||||
|
||||
for (unsigned i = 1; i <= ctx.count; ++i) {
|
||||
if (ctx.count > 1) {
|
||||
std::cout << "--- AT " << i << " of " << ctx.count << " ---\n";
|
||||
}
|
||||
request_one_ticket(ctx, *ec, auth, validity);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "main.hpp"
|
||||
#include <CLI/App.hpp>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
std::shared_ptr<CLI::App> build_authorization_command(const MainConfig&);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,514 @@
|
||||
#include "certificate.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "security_module.hpp"
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
bool is_compressed(const Vanetza_Security_EccP256CurvePoint& point)
|
||||
{
|
||||
switch (point.present) {
|
||||
case Vanetza_Security_EccP256CurvePoint_PR_compressed_y_0:
|
||||
case Vanetza_Security_EccP256CurvePoint_PR_compressed_y_1:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_compressed(const Vanetza_Security_EccP384CurvePoint& point)
|
||||
{
|
||||
switch (point.present) {
|
||||
case Vanetza_Security_EccP384CurvePoint_PR_compressed_y_0:
|
||||
case Vanetza_Security_EccP384CurvePoint_PR_compressed_y_1:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_signature_x_only(const Vanetza_Security_Signature_t& sig)
|
||||
{
|
||||
switch (sig.present) {
|
||||
case Vanetza_Security_Signature_PR_ecdsaNistP256Signature:
|
||||
return sig.choice.ecdsaNistP256Signature.rSig.present == Vanetza_Security_EccP256CurvePoint_PR_x_only;
|
||||
case Vanetza_Security_Signature_PR_ecdsaBrainpoolP256r1Signature:
|
||||
return sig.choice.ecdsaBrainpoolP256r1Signature.rSig.present ==
|
||||
Vanetza_Security_EccP256CurvePoint_PR_x_only;
|
||||
case Vanetza_Security_Signature_PR_ecdsaBrainpoolP384r1Signature:
|
||||
return sig.choice.ecdsaBrainpoolP384r1Signature.rSig.present ==
|
||||
Vanetza_Security_EccP384CurvePoint_PR_x_only;
|
||||
default:
|
||||
return true; // not an ECDSA signature at all
|
||||
}
|
||||
}
|
||||
|
||||
void copy_coordinates(const Vanetza_Security_EccP256CurvePoint_t& point, PublicKey& key)
|
||||
{
|
||||
switch (point.present) {
|
||||
case Vanetza_Security_EccP256CurvePoint_PR_uncompressedP256:
|
||||
key.compression = KeyCompression::NoCompression;
|
||||
pki::copy(point.choice.uncompressedP256.x, key.x);
|
||||
pki::copy(point.choice.uncompressedP256.y, key.y);
|
||||
break;
|
||||
case Vanetza_Security_EccP256CurvePoint_PR_compressed_y_0:
|
||||
key.compression = KeyCompression::Y0;
|
||||
pki::copy(point.choice.compressed_y_0, key.x);
|
||||
break;
|
||||
case Vanetza_Security_EccP256CurvePoint_PR_compressed_y_1:
|
||||
key.compression = KeyCompression::Y1;
|
||||
pki::copy(point.choice.compressed_y_1, key.x);
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("unsupported curve point type");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
template<KeyType> PublicKey make_public_key(const Vanetza_Security_EccP256CurvePoint_t& point);
|
||||
|
||||
template<> PublicKey make_public_key<KeyType::NistP256>(const Vanetza_Security_EccP256CurvePoint_t& point)
|
||||
{
|
||||
PublicKey pub;
|
||||
pub.type = KeyType::NistP256;
|
||||
copy_coordinates(point, pub);
|
||||
return pub;
|
||||
}
|
||||
|
||||
template<> PublicKey make_public_key<KeyType::BrainpoolP256r1>(const Vanetza_Security_EccP256CurvePoint_t& point)
|
||||
{
|
||||
PublicKey pub;
|
||||
pub.type = KeyType::BrainpoolP256r1;
|
||||
copy_coordinates(point, pub);
|
||||
return pub;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Certificate::Certificate() : m_asn1(asn_DEF_Vanetza_Security_EtsiTs103097Certificate)
|
||||
{
|
||||
}
|
||||
|
||||
Certificate::Certificate(const Vanetza_Security_EtsiTs103097Certificate_t& src) :
|
||||
m_asn1(asn_DEF_Vanetza_Security_EtsiTs103097Certificate, &src)
|
||||
{
|
||||
}
|
||||
|
||||
std::string Certificate::get_name() const
|
||||
{
|
||||
return pki::get_name(*m_asn1);
|
||||
}
|
||||
|
||||
PublicKey Certificate::get_public_key() const
|
||||
{
|
||||
return pki::get_public_key(*m_asn1);
|
||||
}
|
||||
|
||||
boost::optional<PublicKey> Certificate::get_encryption_key() const
|
||||
{
|
||||
if (m_asn1->toBeSigned.encryptionKey) {
|
||||
const Vanetza_Security_PublicEncryptionKey_t& enckey = *m_asn1->toBeSigned.encryptionKey;
|
||||
switch (enckey.publicKey.present) {
|
||||
case Vanetza_Security_BasePublicEncryptionKey_PR_eciesNistP256:
|
||||
return make_public_key<KeyType::NistP256>(enckey.publicKey.choice.eciesNistP256);
|
||||
break;
|
||||
case Vanetza_Security_BasePublicEncryptionKey_PR_eciesBrainpoolP256r1:
|
||||
return make_public_key<KeyType::BrainpoolP256r1>(enckey.publicKey.choice.eciesBrainpoolP256r1);
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("unsupported encryption key type");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
return boost::none;
|
||||
}
|
||||
}
|
||||
|
||||
Clock::time_point Certificate::valid_since() const
|
||||
{
|
||||
return Clock::time_point { std::chrono::seconds(m_asn1->toBeSigned.validityPeriod.start) };
|
||||
}
|
||||
|
||||
Clock::time_point Certificate::valid_until() const
|
||||
{
|
||||
Clock::duration d;
|
||||
const Vanetza_Security_Duration& asn1_d = m_asn1->toBeSigned.validityPeriod.duration;
|
||||
switch (asn1_d.present) {
|
||||
case Vanetza_Security_Duration_PR_years:
|
||||
// IEEE 1609.2: "A year is considered to be 31556952 seconds"
|
||||
d = asn1_d.choice.years * std::chrono::seconds(31556952);
|
||||
break;
|
||||
case Vanetza_Security_Duration_PR_sixtyHours:
|
||||
d = std::chrono::hours(60 * asn1_d.choice.sixtyHours);
|
||||
break;
|
||||
case Vanetza_Security_Duration_PR_hours:
|
||||
d = std::chrono::hours(asn1_d.choice.hours);
|
||||
break;
|
||||
case Vanetza_Security_Duration_PR_minutes:
|
||||
d = std::chrono::minutes(asn1_d.choice.minutes);
|
||||
break;
|
||||
case Vanetza_Security_Duration_PR_seconds:
|
||||
d = std::chrono::seconds(asn1_d.choice.seconds);
|
||||
break;
|
||||
case Vanetza_Security_Duration_PR_milliseconds:
|
||||
d = std::chrono::milliseconds(asn1_d.choice.milliseconds);
|
||||
break;
|
||||
case Vanetza_Security_Duration_PR_microseconds:
|
||||
d = std::chrono::microseconds(asn1_d.choice.microseconds);
|
||||
break;
|
||||
default:
|
||||
// no validity duration as safe fallback
|
||||
d = std::chrono::seconds(0);
|
||||
break;
|
||||
}
|
||||
return valid_since() + d;
|
||||
}
|
||||
|
||||
bool Certificate::decode(const char* data, std::size_t length)
|
||||
{
|
||||
return m_asn1.decode(data, length);
|
||||
}
|
||||
|
||||
bool Certificate::decode(const std::string& buffer)
|
||||
{
|
||||
return m_asn1.decode(buffer.data(), buffer.size());
|
||||
}
|
||||
|
||||
bool Certificate::decode(const ByteBuffer& buffer)
|
||||
{
|
||||
return m_asn1.decode(buffer.data(), buffer.size());
|
||||
}
|
||||
|
||||
ByteBuffer Certificate::encode() const
|
||||
{
|
||||
return m_asn1.encode();
|
||||
}
|
||||
|
||||
void Certificate::print() const
|
||||
{
|
||||
xer_fprint(stdout, &asn_DEF_Vanetza_Security_EtsiTs103097Certificate, &*m_asn1);
|
||||
}
|
||||
|
||||
bool Certificate::is_canonical() const
|
||||
{
|
||||
return vanetza::pki::is_canonical(*m_asn1);
|
||||
}
|
||||
|
||||
HashedId8 Certificate::calculate_hashed_id8(SecurityModule& sec) const
|
||||
{
|
||||
return vanetza::pki::calculate_hashed_id8(sec, *m_asn1);
|
||||
}
|
||||
|
||||
Sha256Hash calculate_sha256_hash(SecurityModule& sec, const Certificate& cert)
|
||||
{
|
||||
ByteBuffer buffer = cert.encode();
|
||||
return sec.calculate_sha256_hash(buffer.data(), buffer.size());
|
||||
}
|
||||
|
||||
Sha384Hash calculate_sha384_hash(SecurityModule& sec, const Certificate& cert)
|
||||
{
|
||||
ByteBuffer buffer = cert.encode();
|
||||
return sec.calculate_sha384_hash(buffer.data(), buffer.size());
|
||||
}
|
||||
|
||||
bool is_currently_valid(const Certificate& cert, Clock::time_point t)
|
||||
{
|
||||
return cert.valid_since() <= t && cert.valid_until() >= t;
|
||||
}
|
||||
|
||||
bool is_root_ca(const Certificate& cert)
|
||||
{
|
||||
const auto& issuer = cert.raw().issuer;
|
||||
const bool self_issued = (issuer.present == Vanetza_Security_IssuerIdentifier_PR_self);
|
||||
if (!self_issued) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& tbs = cert.raw().toBeSigned;
|
||||
const bool can_issue = tbs.certIssuePermissions && tbs.certIssuePermissions->list.count > 0;
|
||||
if (!can_issue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tbs.appPermissions) {
|
||||
bool sign_crl = false;
|
||||
bool sign_ctl = false;
|
||||
|
||||
for (int i = 0; i < tbs.appPermissions->list.count; ++i) {
|
||||
const Vanetza_Security_PsidSsp_t* permission = tbs.appPermissions->list.array[i];
|
||||
if (!permission) {
|
||||
continue;
|
||||
}
|
||||
switch (permission->psid) {
|
||||
case aid::CTL:
|
||||
sign_ctl = true;
|
||||
break;
|
||||
|
||||
case aid::CRL:
|
||||
sign_crl = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
// no op
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sign_ctl || !sign_crl) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
bool issuing_scope_contains(const Certificate& cert, ItsAid target)
|
||||
{
|
||||
const auto* cip = cert.raw().toBeSigned.certIssuePermissions;
|
||||
if (!cip) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < cip->list.count; ++i) {
|
||||
const auto* group = cip->list.array[i];
|
||||
if (!group) {
|
||||
continue;
|
||||
}
|
||||
const auto& sp = group->subjectPermissions;
|
||||
if (sp.present == Vanetza_Security_SubjectPermissions_PR_all) {
|
||||
return true;
|
||||
}
|
||||
if (sp.present == Vanetza_Security_SubjectPermissions_PR_explicit) {
|
||||
const auto& ranges = sp.choice.Explicit.list;
|
||||
for (int j = 0; j < ranges.count; ++j) {
|
||||
if (ranges.array[j] && ranges.array[j]->psid == static_cast<long>(target)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool app_permissions_contains(const Certificate& cert, ItsAid target)
|
||||
{
|
||||
const auto* ap = cert.raw().toBeSigned.appPermissions;
|
||||
if (!ap) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < ap->list.count; ++i) {
|
||||
if (ap->list.array[i] && ap->list.array[i]->psid == static_cast<long>(target)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CertificateRole certificate_role(const Certificate& cert)
|
||||
{
|
||||
if (is_root_ca(cert)) {
|
||||
return CertificateRole::RootCa;
|
||||
}
|
||||
|
||||
const auto& tbs = cert.raw().toBeSigned;
|
||||
const bool self_issued = (cert.raw().issuer.present == Vanetza_Security_IssuerIdentifier_PR_self);
|
||||
const bool has_cert_issue = tbs.certIssuePermissions && tbs.certIssuePermissions->list.count > 0;
|
||||
|
||||
if (self_issued) {
|
||||
// TS 103 097 §7.2.5: TLM is self-signed, signs the CTL, no certIssuePermissions.
|
||||
if (!has_cert_issue && app_permissions_contains(cert, aid::CTL)) {
|
||||
return CertificateRole::Tlm;
|
||||
}
|
||||
return CertificateRole::Unknown;
|
||||
}
|
||||
|
||||
if (has_cert_issue) {
|
||||
// TS 103 097 §7.2.4 subordinate CA. Per TS 102 941 Table 1 the EA issues
|
||||
// enrolment credentials (SCR in scope); the AA issues authorization tickets.
|
||||
return issuing_scope_contains(cert, aid::SCR) ? CertificateRole::EnrolmentAuthority :
|
||||
CertificateRole::AuthorizationAuthority;
|
||||
}
|
||||
|
||||
// End entity: §7.2.2 EC uses CertificateId name, §7.2.1 AT uses CertificateId none.
|
||||
switch (tbs.id.present) {
|
||||
case Vanetza_Security_CertificateId_PR_name:
|
||||
return CertificateRole::EnrolmentCredential;
|
||||
case Vanetza_Security_CertificateId_PR_none:
|
||||
return CertificateRole::AuthorizationTicket;
|
||||
default:
|
||||
return CertificateRole::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_canonical(const Vanetza_Security_EtsiTs103097Certificate_t& cert)
|
||||
{
|
||||
bool compressed_point = true;
|
||||
const Vanetza_Security_VerificationKeyIndicator& indicator = cert.toBeSigned.verifyKeyIndicator;
|
||||
if (indicator.present == Vanetza_Security_VerificationKeyIndicator_PR_verificationKey) {
|
||||
const Vanetza_Security_PublicVerificationKey& pubkey = indicator.choice.verificationKey;
|
||||
switch (pubkey.present) {
|
||||
case Vanetza_Security_PublicVerificationKey_PR_ecdsaNistP256:
|
||||
compressed_point = is_compressed(pubkey.choice.ecdsaNistP256);
|
||||
break;
|
||||
case Vanetza_Security_PublicVerificationKey_PR_ecdsaBrainpoolP256r1:
|
||||
compressed_point = is_compressed(pubkey.choice.ecdsaBrainpoolP256r1);
|
||||
break;
|
||||
case Vanetza_Security_PublicVerificationKey_PR_ecdsaBrainpoolP384r1:
|
||||
compressed_point = is_compressed(pubkey.choice.ecdsaBrainpoolP384r1);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (indicator.present == Vanetza_Security_VerificationKeyIndicator_PR_reconstructionValue) {
|
||||
compressed_point = is_compressed(indicator.choice.reconstructionValue);
|
||||
}
|
||||
|
||||
if (!compressed_point) {
|
||||
return false;
|
||||
} else if (cert.signature && !is_signature_x_only(*cert.signature)) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Sha256Hash calculate_sha256_hash(SecurityModule& security, const Vanetza_Security_Certificate_t& cert)
|
||||
{
|
||||
ByteBuffer buffer = asn1::encode_oer(asn_DEF_Vanetza_Security_Certificate, &cert);
|
||||
return security.calculate_sha256_hash(buffer.data(), buffer.size());
|
||||
}
|
||||
|
||||
Sha384Hash calculate_sha384_hash(SecurityModule& sec, const Vanetza_Security_Certificate_t& cert)
|
||||
{
|
||||
ByteBuffer buffer = asn1::encode_oer(asn_DEF_Vanetza_Security_Certificate, &cert);
|
||||
return sec.calculate_sha384_hash(buffer.data(), buffer.size());
|
||||
}
|
||||
|
||||
HashedId8 calculate_hashed_id8(SecurityModule& sec, const Vanetza_Security_Certificate_t& cert)
|
||||
{
|
||||
if (!is_canonical(cert)) {
|
||||
throw std::runtime_error("HashedId8 can only be calculated for canonical certificates");
|
||||
}
|
||||
|
||||
// all explicit certificates possess an verification key
|
||||
const Vanetza_Security_VerificationKeyIndicator& indicator = cert.toBeSigned.verifyKeyIndicator;
|
||||
if (indicator.present == Vanetza_Security_VerificationKeyIndicator_PR_verificationKey) {
|
||||
switch (indicator.choice.verificationKey.present) {
|
||||
case Vanetza_Security_PublicVerificationKey_PR_ecdsaNistP256:
|
||||
case Vanetza_Security_PublicVerificationKey_PR_ecdsaBrainpoolP256r1:
|
||||
return HashedId8 { calculate_sha256_hash(sec, cert) };
|
||||
case Vanetza_Security_PublicVerificationKey_PR_ecdsaBrainpoolP384r1:
|
||||
return HashedId8 { calculate_sha384_hash(sec, cert) };
|
||||
default:
|
||||
throw std::runtime_error("do not know how to hash the certificate");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// fall back to SHA-256
|
||||
return HashedId8 { calculate_sha256_hash(sec, cert) };
|
||||
}
|
||||
}
|
||||
|
||||
PublicKey make_public_key(KeyType t, const Vanetza_Security_EccP256CurvePoint_t& point)
|
||||
{
|
||||
PublicKey pub;
|
||||
pub.type = t;
|
||||
switch (point.present) {
|
||||
case Vanetza_Security_EccP256CurvePoint_PR_compressed_y_0:
|
||||
pub.compression = KeyCompression::Y0;
|
||||
pub.x = copy(point.choice.compressed_y_0);
|
||||
break;
|
||||
case Vanetza_Security_EccP256CurvePoint_PR_compressed_y_1:
|
||||
pub.compression = KeyCompression::Y1;
|
||||
pub.x = copy(point.choice.compressed_y_1);
|
||||
break;
|
||||
case Vanetza_Security_EccP256CurvePoint_PR_uncompressedP256:
|
||||
pub.compression = KeyCompression::NoCompression;
|
||||
pub.x = copy(point.choice.uncompressedP256.x);
|
||||
pub.y = copy(point.choice.uncompressedP256.y);
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("cannot create public key from given curve point");
|
||||
break;
|
||||
}
|
||||
return pub;
|
||||
}
|
||||
|
||||
PublicKey make_public_key(KeyType t, const Vanetza_Security_EccP384CurvePoint_t& point)
|
||||
{
|
||||
PublicKey pub;
|
||||
pub.type = t;
|
||||
switch (point.present) {
|
||||
case Vanetza_Security_EccP384CurvePoint_PR_compressed_y_0:
|
||||
pub.compression = KeyCompression::Y0;
|
||||
pub.x = copy(point.choice.compressed_y_0);
|
||||
break;
|
||||
case Vanetza_Security_EccP384CurvePoint_PR_compressed_y_1:
|
||||
pub.compression = KeyCompression::Y1;
|
||||
pub.x = copy(point.choice.compressed_y_1);
|
||||
break;
|
||||
case Vanetza_Security_EccP384CurvePoint_PR_uncompressedP384:
|
||||
pub.compression = KeyCompression::NoCompression;
|
||||
pub.x = copy(point.choice.uncompressedP384.x);
|
||||
pub.y = copy(point.choice.uncompressedP384.y);
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("cannot create public key from given curve point");
|
||||
break;
|
||||
}
|
||||
return pub;
|
||||
}
|
||||
|
||||
PublicKey get_public_key(const Vanetza_Security_PublicVerificationKey_t& input)
|
||||
{
|
||||
switch (input.present) {
|
||||
case Vanetza_Security_PublicVerificationKey_PR_ecdsaNistP256:
|
||||
return make_public_key(KeyType::NistP256, input.choice.ecdsaNistP256);
|
||||
break;
|
||||
case Vanetza_Security_PublicVerificationKey_PR_ecdsaBrainpoolP256r1:
|
||||
return make_public_key(KeyType::BrainpoolP256r1, input.choice.ecdsaBrainpoolP256r1);
|
||||
break;
|
||||
case Vanetza_Security_PublicVerificationKey_PR_ecdsaBrainpoolP384r1:
|
||||
return make_public_key(KeyType::BrainpoolP384r1, input.choice.ecdsaBrainpoolP384r1);
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("unknown public verification key type");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
PublicKey get_public_key(const Vanetza_Security_Certificate_t& cert)
|
||||
{
|
||||
switch (cert.toBeSigned.verifyKeyIndicator.present) {
|
||||
case Vanetza_Security_VerificationKeyIndicator_PR_verificationKey:
|
||||
return get_public_key(cert.toBeSigned.verifyKeyIndicator.choice.verificationKey);
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("unable to fetch public key from certificate");
|
||||
}
|
||||
}
|
||||
|
||||
std::string get_name(const Vanetza_Security_Certificate_t& cert)
|
||||
{
|
||||
std::string name;
|
||||
if (cert.toBeSigned.id.present == Vanetza_Security_CertificateId_PR_name) {
|
||||
const UTF8String_t& hostname = cert.toBeSigned.id.choice.name;
|
||||
name = std::string { hostname.buf, hostname.buf + hostname.size };
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include "hashed_id8.hpp"
|
||||
#include "keys.hpp"
|
||||
#include <vanetza/asn1/asn1c_wrapper.hpp>
|
||||
#include <vanetza/asn1/security/Certificate.h>
|
||||
#include <vanetza/asn1/security/EtsiTs103097Certificate.h>
|
||||
#include <vanetza/common/clock.hpp>
|
||||
|
||||
// asn1c quirk: complete the struct tags it forward-declares but never defines.
|
||||
struct Vanetza_Security_Certificate : Vanetza_Security_CertificateBase
|
||||
{
|
||||
};
|
||||
|
||||
struct Vanetza_Security_EtsiTs103097Certificate : Vanetza_Security_ExplicitCertificate_t
|
||||
{
|
||||
};
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class SecurityModule;
|
||||
|
||||
class Certificate
|
||||
{
|
||||
public:
|
||||
Certificate();
|
||||
explicit Certificate(const Vanetza_Security_EtsiTs103097Certificate_t&);
|
||||
|
||||
HashedId8 calculate_hashed_id8(SecurityModule&) const;
|
||||
bool is_canonical() const;
|
||||
std::string get_name() const;
|
||||
PublicKey get_public_key() const;
|
||||
Clock::time_point valid_since() const;
|
||||
Clock::time_point valid_until() const;
|
||||
boost::optional<PublicKey> get_encryption_key() const;
|
||||
|
||||
bool decode(const char* data, std::size_t length);
|
||||
bool decode(const std::string&);
|
||||
bool decode(const ByteBuffer&);
|
||||
ByteBuffer encode() const;
|
||||
|
||||
const Vanetza_Security_EtsiTs103097Certificate_t& raw() const
|
||||
{
|
||||
return *m_asn1;
|
||||
}
|
||||
|
||||
void print() const;
|
||||
|
||||
private:
|
||||
vanetza::asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs103097Certificate_t> m_asn1;
|
||||
};
|
||||
|
||||
bool is_currently_valid(const Certificate&, Clock::time_point);
|
||||
|
||||
/**
|
||||
* Check if certificate is a compliant Root CA certificate.
|
||||
* Rules are given by section 7.3.2 in TS 103 097 V1.3.1
|
||||
*
|
||||
* \param cert
|
||||
* \return true if certificate complies
|
||||
*/
|
||||
bool is_root_ca(const Certificate&);
|
||||
|
||||
// PKI role inferred from issuer, permissions and CertificateId per TS 103 097 V2.1.1 §7.2.
|
||||
enum class CertificateRole
|
||||
{
|
||||
RootCa, // §7.2.3: self-issued CA
|
||||
EnrolmentAuthority, // §7.2.4: sub-CA whose issuing scope grants SCR
|
||||
AuthorizationAuthority, // §7.2.4: sub-CA whose issuing scope grants services
|
||||
EnrolmentCredential, // §7.2.2: end entity with CertificateId name
|
||||
AuthorizationTicket, // §7.2.1: end entity with CertificateId none
|
||||
Tlm, // §7.2.5: self-issued, signs CTL, no certIssuePermissions
|
||||
Unknown, // matches no profile above
|
||||
};
|
||||
|
||||
// Classify the certificate's PKI role. Never throws.
|
||||
CertificateRole certificate_role(const Certificate&);
|
||||
|
||||
Sha256Hash calculate_sha256_hash(SecurityModule&, const Certificate&);
|
||||
Sha384Hash calculate_sha384_hash(SecurityModule&, const Certificate&);
|
||||
Sha256Hash calculate_sha256_hash(SecurityModule&, const Vanetza_Security_Certificate_t&);
|
||||
Sha384Hash calculate_sha384_hash(SecurityModule&, const Vanetza_Security_Certificate_t&);
|
||||
HashedId8 calculate_hashed_id8(SecurityModule&, const Vanetza_Security_Certificate_t&);
|
||||
PublicKey get_public_key(const Vanetza_Security_Certificate_t&);
|
||||
std::string get_name(const Vanetza_Security_Certificate_t&);
|
||||
bool is_canonical(const Vanetza_Security_Certificate_t&);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
#include "certificate_filesystem_storage.hpp"
|
||||
#include "filesystem.hpp"
|
||||
#include "security_module.hpp"
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <boost/range/adaptor/filtered.hpp>
|
||||
#include <boost/range/adaptor/transformed.hpp>
|
||||
#include <boost/range/iterator_range.hpp>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
CertificateFilesystemStorage::CertificateFilesystemStorage(std::shared_ptr<SecurityModule> sec,
|
||||
const std::filesystem::path& root, std::string extension) :
|
||||
m_security(sec), m_root(root), m_extension(std::move(extension))
|
||||
{
|
||||
std::filesystem::create_directories(m_root);
|
||||
}
|
||||
|
||||
boost::optional<Certificate> CertificateFilesystemStorage::fetch(const HashedId8& id) const
|
||||
{
|
||||
const std::filesystem::path path = filename(id);
|
||||
if (std::filesystem::is_regular_file(path)) {
|
||||
ByteBuffer buffer = read(path);
|
||||
Certificate cert;
|
||||
if (cert.decode(buffer)) {
|
||||
return cert;
|
||||
}
|
||||
}
|
||||
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
void CertificateFilesystemStorage::store(const Certificate& cert)
|
||||
{
|
||||
HashedId8 id = cert.calculate_hashed_id8(*m_security);
|
||||
write(filename(id), cert.encode());
|
||||
}
|
||||
|
||||
bool CertificateFilesystemStorage::erase(const HashedId8& id)
|
||||
{
|
||||
return std::filesystem::remove(filename(id));
|
||||
}
|
||||
|
||||
std::filesystem::path CertificateFilesystemStorage::filename(const HashedId8& id) const
|
||||
{
|
||||
std::string filename = hexstring(id) + m_extension;
|
||||
std::filesystem::path path = m_root / filename;
|
||||
return path;
|
||||
}
|
||||
|
||||
HashedId8Range CertificateFilesystemStorage::list() const
|
||||
{
|
||||
auto rng =
|
||||
boost::make_iterator_range(std::filesystem::directory_iterator(m_root), std::filesystem::directory_iterator()) |
|
||||
boost::adaptors::filtered([this](const std::filesystem::directory_entry& e) {
|
||||
return e.path().extension() == m_extension && std::filesystem::is_regular_file(e.path()) &&
|
||||
HashedId8::from_hexstring(e.path().stem().string()).has_value();
|
||||
}) |
|
||||
boost::adaptors::transformed([](const std::filesystem::directory_entry& e) {
|
||||
return *HashedId8::from_hexstring(e.path().stem().string());
|
||||
});
|
||||
return HashedId8Range(rng);
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "certificate_storage.hpp"
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class SecurityModule;
|
||||
|
||||
class CertificateFilesystemStorage : public CertificateStorage
|
||||
{
|
||||
public:
|
||||
CertificateFilesystemStorage(std::shared_ptr<SecurityModule>, const std::filesystem::path&,
|
||||
std::string extension = ".oer");
|
||||
boost::optional<Certificate> fetch(const HashedId8&) const override;
|
||||
void store(const Certificate&) override;
|
||||
bool erase(const HashedId8&) override;
|
||||
HashedId8Range list() const override;
|
||||
|
||||
protected:
|
||||
std::filesystem::path filename(const HashedId8&) const;
|
||||
|
||||
private:
|
||||
std::shared_ptr<SecurityModule> m_security;
|
||||
std::filesystem::path m_root;
|
||||
std::string m_extension;
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,102 @@
|
||||
#include "certificate_revocation_list.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "exception.hpp"
|
||||
#include "filesystem.hpp"
|
||||
#include "validation.hpp"
|
||||
#include <vanetza/asn1/security/ToBeSignedCrl.h>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
CertificateRevocationList::CertificateRevocationList() :
|
||||
m_asn1(asn_DEF_Vanetza_Security_CertificateRevocationListMessage)
|
||||
{
|
||||
}
|
||||
|
||||
bool CertificateRevocationList::decode(const std::string& buffer)
|
||||
{
|
||||
return m_asn1.decode(buffer.data(), buffer.size());
|
||||
}
|
||||
|
||||
bool CertificateRevocationList::decode(const ByteBuffer& buffer)
|
||||
{
|
||||
return m_asn1.decode(buffer.data(), buffer.size());
|
||||
}
|
||||
|
||||
ByteBuffer CertificateRevocationList::encode() const
|
||||
{
|
||||
return m_asn1.encode();
|
||||
}
|
||||
|
||||
CertificateRevocationList CertificateRevocationList::from_file(const std::filesystem::path& path)
|
||||
{
|
||||
ByteBuffer buffer = read(path);
|
||||
if (buffer.empty()) {
|
||||
throw DecodingFailure("CRL file is empty or missing");
|
||||
}
|
||||
|
||||
CertificateRevocationList crl;
|
||||
if (!crl.decode(buffer)) {
|
||||
throw DecodingFailure("CRL message could not be decoded");
|
||||
}
|
||||
return crl;
|
||||
}
|
||||
|
||||
boost::optional<HashedId8> CertificateRevocationList::get_hashed_id8(SecurityModule& security) const
|
||||
{
|
||||
const Vanetza_Security_SignedData_t* signed_data = get_signed_data(raw());
|
||||
if (signed_data) {
|
||||
switch (signed_data->signer.present) {
|
||||
case Vanetza_Security_SignerIdentifier_PR_digest:
|
||||
return HashedId8::from_buffer(signed_data->signer.choice.digest);
|
||||
case Vanetza_Security_SignerIdentifier_PR_certificate: {
|
||||
const Vanetza_Security_SequenceOfCertificate_t& certs = signed_data->signer.choice.certificate;
|
||||
if (certs.list.count == 1 && certs.list.array[0]) {
|
||||
return calculate_hashed_id8(security, *certs.list.array[0]);
|
||||
}
|
||||
} break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
boost::optional<std::vector<HashedId8>> CertificateRevocationList::revoked_entries() const
|
||||
{
|
||||
const OCTET_STRING_t* opaque = get_signed_payload(raw().content);
|
||||
if (!opaque) {
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
// Signed payload is an EtsiTs102941Data carrying the ToBeSignedCrl in its content CHOICE.
|
||||
MgmtData mgmt;
|
||||
if (!mgmt.decode(*opaque)) {
|
||||
return boost::none;
|
||||
}
|
||||
if (mgmt->content.present != Vanetza_Security_EtsiTs102941DataContent_PR_certificateRevocationList) {
|
||||
return boost::none;
|
||||
}
|
||||
const Vanetza_Security_ToBeSignedCrl_t& tbs = mgmt->content.choice.certificateRevocationList;
|
||||
|
||||
const auto& list = tbs.entries.list;
|
||||
std::vector<HashedId8> result;
|
||||
result.reserve(list.count);
|
||||
for (int i = 0; i < list.count; ++i) {
|
||||
const Vanetza_Security_CrlEntry_t* entry = list.array[i];
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
if (auto id = HashedId8::from_buffer(*entry)) {
|
||||
result.push_back(*id);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include "hashed_id8.hpp"
|
||||
#include <vanetza/asn1/asn1c_wrapper.hpp>
|
||||
#include <vanetza/asn1/security/CertificateRevocationListMessage.h>
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class SecurityModule;
|
||||
|
||||
class CertificateRevocationList
|
||||
{
|
||||
public:
|
||||
CertificateRevocationList();
|
||||
bool decode(const std::string&);
|
||||
bool decode(const ByteBuffer&);
|
||||
ByteBuffer encode() const;
|
||||
|
||||
/**
|
||||
* Read the OER-encoded CRL message from a file and decode it.
|
||||
*
|
||||
* \throws DecodingFailure if the file is empty or decoding fails
|
||||
*/
|
||||
static CertificateRevocationList from_file(const std::filesystem::path&);
|
||||
|
||||
boost::optional<HashedId8> get_hashed_id8(SecurityModule&) const;
|
||||
|
||||
/**
|
||||
* Decode the signed payload and return the revoked HashedId8 entries.
|
||||
* An empty vector is valid: CRLs are reissued periodically even when
|
||||
* nothing is revoked. boost::none indicates a decode failure.
|
||||
*/
|
||||
boost::optional<std::vector<HashedId8>> revoked_entries() const;
|
||||
|
||||
const Vanetza_Security_EtsiTs103097Data_t& raw() const
|
||||
{
|
||||
return *m_asn1;
|
||||
}
|
||||
|
||||
private:
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_CertificateRevocationListMessage_t> m_asn1;
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "certificate.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <boost/range/any_range.hpp>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
/// \brief Lazy, single-pass view over stored HashedId8s.
|
||||
using HashedId8Range = boost::any_range<HashedId8, boost::single_pass_traversal_tag, HashedId8, std::ptrdiff_t>;
|
||||
|
||||
class CertificateStorage
|
||||
{
|
||||
public:
|
||||
virtual ~CertificateStorage() = default;
|
||||
virtual boost::optional<Certificate> fetch(const HashedId8&) const = 0;
|
||||
virtual void store(const Certificate&) = 0;
|
||||
virtual bool erase(const HashedId8&) = 0;
|
||||
virtual HashedId8Range list() const = 0;
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,524 @@
|
||||
#include "certificate_trust_list.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "certificate_storage.hpp"
|
||||
#include "exception.hpp"
|
||||
#include "filesystem.hpp"
|
||||
#include "hexstring.hpp"
|
||||
#include "trust_list_storage.hpp"
|
||||
#include "validation.hpp"
|
||||
#include <vanetza/security/v3/asn1_conversions.hpp>
|
||||
#include <boost/date_time/posix_time/posix_time_io.hpp>
|
||||
#include <iostream>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
CertificateTrustList::CertificateTrustList() : m_asn1(asn_DEF_Vanetza_Security_TlmCertificateTrustListMessage)
|
||||
{
|
||||
}
|
||||
|
||||
bool CertificateTrustList::decode(const std::string& buffer)
|
||||
{
|
||||
return m_asn1.decode(buffer.data(), buffer.size());
|
||||
}
|
||||
|
||||
bool CertificateTrustList::decode(const ByteBuffer& buffer)
|
||||
{
|
||||
return m_asn1.decode(buffer.data(), buffer.size());
|
||||
}
|
||||
|
||||
ByteBuffer CertificateTrustList::encode() const
|
||||
{
|
||||
return m_asn1.encode();
|
||||
}
|
||||
|
||||
CertificateTrustList CertificateTrustList::from_file(const std::filesystem::path& path)
|
||||
{
|
||||
ByteBuffer buffer = read(path);
|
||||
if (buffer.empty()) {
|
||||
throw DecodingFailure("trust list file is empty or missing");
|
||||
}
|
||||
|
||||
CertificateTrustList ctl;
|
||||
if (!ctl.decode(buffer)) {
|
||||
throw DecodingFailure("trust list message could not be decoded");
|
||||
}
|
||||
return ctl;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
const OCTET_STRING_t& require_signed_payload(const Vanetza_Security_EtsiTs103097Data_t& msg)
|
||||
{
|
||||
const OCTET_STRING_t* opaque = get_signed_payload(msg.content);
|
||||
if (!opaque) {
|
||||
throw DecodingFailure("cannot access signed payload of trust list message");
|
||||
}
|
||||
return *opaque;
|
||||
}
|
||||
|
||||
template<class Commands> void dispatch_ctl_commands(const Commands& commands, CtlVisitor& visitor)
|
||||
{
|
||||
for (int i = 0; i < commands.list.count; ++i) {
|
||||
const Vanetza_Security_CtlCommand_t* cmd = commands.list.array[i];
|
||||
if (!cmd) {
|
||||
continue;
|
||||
} else if (cmd->present == Vanetza_Security_CtlCommand_PR_add) {
|
||||
const Vanetza_Security_CtlEntry_t& entry = cmd->choice.add;
|
||||
switch (entry.present) {
|
||||
case Vanetza_Security_CtlEntry_PR_rca:
|
||||
visitor.add_root_ca(entry.choice.rca);
|
||||
break;
|
||||
case Vanetza_Security_CtlEntry_PR_tlm:
|
||||
visitor.add_trust_list_manager(entry.choice.tlm);
|
||||
break;
|
||||
case Vanetza_Security_CtlEntry_PR_dc:
|
||||
visitor.add_distribution_centre(entry.choice.dc);
|
||||
break;
|
||||
case Vanetza_Security_CtlEntry_PR_aa:
|
||||
visitor.add_authorization_authority(entry.choice.aa);
|
||||
break;
|
||||
case Vanetza_Security_CtlEntry_PR_ea:
|
||||
visitor.add_enrolment_authority(entry.choice.ea);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (cmd->present == Vanetza_Security_CtlCommand_PR_delete) {
|
||||
const Vanetza_Security_CtlDelete_t& del = cmd->choice.Delete;
|
||||
switch (del.present) {
|
||||
case Vanetza_Security_CtlDelete_PR_cert:
|
||||
visitor.remove_certificate(del.choice.cert);
|
||||
break;
|
||||
case Vanetza_Security_CtlDelete_PR_dc:
|
||||
visitor.remove_distribution_centre(del.choice.dc);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Vanetza_Security_CtlFormat_t* find_ctl_format(const Vanetza_Security_EtsiTs102941Data_t& mgmt)
|
||||
{
|
||||
switch (mgmt.content.present) {
|
||||
case Vanetza_Security_EtsiTs102941DataContent_PR_certificateTrustListRca:
|
||||
return &mgmt.content.choice.certificateTrustListRca;
|
||||
case Vanetza_Security_EtsiTs102941DataContent_PR_certificateTrustListTlm:
|
||||
return &mgmt.content.choice.certificateTrustListTlm;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void CertificateTrustList::visit_tlm_ctl(CtlVisitor& visitor) const
|
||||
{
|
||||
auto mgmt = TlmCtlData::from_opaque(require_signed_payload(*m_asn1));
|
||||
dispatch_ctl_commands(mgmt->content.choice.certificateTrustListTlm.ctlCommands, visitor);
|
||||
}
|
||||
|
||||
void CertificateTrustList::visit_rca_ctl(CtlVisitor& visitor) const
|
||||
{
|
||||
auto mgmt = RcaCtlData::from_opaque(require_signed_payload(*m_asn1));
|
||||
dispatch_ctl_commands(mgmt->content.choice.certificateTrustListRca.ctlCommands, visitor);
|
||||
}
|
||||
|
||||
boost::optional<bool> CertificateTrustList::is_full_ctl() const
|
||||
{
|
||||
const OCTET_STRING_t* opaque = get_signed_payload(raw().content);
|
||||
if (!opaque) {
|
||||
return boost::none;
|
||||
}
|
||||
MgmtData mgmt;
|
||||
if (!mgmt.decode(*opaque)) {
|
||||
return boost::none;
|
||||
}
|
||||
const Vanetza_Security_CtlFormat_t* format = find_ctl_format(*mgmt);
|
||||
if (!format) {
|
||||
return boost::none;
|
||||
}
|
||||
return format->isFullCtl != 0;
|
||||
}
|
||||
|
||||
boost::optional<std::uint8_t> CertificateTrustList::ctl_sequence() const
|
||||
{
|
||||
const OCTET_STRING_t* opaque = get_signed_payload(raw().content);
|
||||
if (!opaque) {
|
||||
return boost::none;
|
||||
}
|
||||
MgmtData mgmt;
|
||||
if (!mgmt.decode(*opaque)) {
|
||||
return boost::none;
|
||||
}
|
||||
const Vanetza_Security_CtlFormat_t* format = find_ctl_format(*mgmt);
|
||||
if (!format) {
|
||||
return boost::none;
|
||||
}
|
||||
return static_cast<std::uint8_t>(format->ctlSequence);
|
||||
}
|
||||
|
||||
void CertificateTrustList::print() const
|
||||
{
|
||||
xer_fprint(stdout, &asn_DEF_Vanetza_Security_TlmCertificateTrustListMessage, &*m_asn1);
|
||||
}
|
||||
|
||||
boost::optional<HashedId8> CertificateTrustList::get_hashed_id8(SecurityModule& security) const
|
||||
{
|
||||
const Vanetza_Security_SignedData_t* signed_data = get_signed_data(raw());
|
||||
if (signed_data) {
|
||||
switch (signed_data->signer.present) {
|
||||
case Vanetza_Security_SignerIdentifier_PR_digest:
|
||||
return HashedId8::from_buffer(signed_data->signer.choice.digest);
|
||||
case Vanetza_Security_SignerIdentifier_PR_certificate: {
|
||||
const Vanetza_Security_SequenceOfCertificate_t& certs = signed_data->signer.choice.certificate;
|
||||
if (certs.list.count == 1 && certs.list.array[0]) {
|
||||
return calculate_hashed_id8(security, *certs.list.array[0]);
|
||||
}
|
||||
} break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
CertificateTrustListProcessor::CertificateTrustListProcessor(std::shared_ptr<SecurityModule> security) :
|
||||
m_security(security)
|
||||
{
|
||||
}
|
||||
|
||||
void CertificateTrustListProcessor::process(const CertificateTrustList& ctl)
|
||||
{
|
||||
const Vanetza_Security_Opaque_t* payload = get_signed_payload(ctl.raw().content);
|
||||
boost::optional<HashedId8> ctl_signer = ctl.get_hashed_id8(*m_security);
|
||||
if (!payload || !ctl_signer) {
|
||||
return;
|
||||
}
|
||||
|
||||
MgmtData mgmt;
|
||||
if (!mgmt.decode(*payload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Vanetza_Security_CtlFormat_t* format = find_ctl_format(*mgmt);
|
||||
if (!format) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Per TS 102 941 v1.4.1 §6.3.4: a full CTL is the complete trust state.
|
||||
// Drop everything before applying so entries absent from the new full list
|
||||
// don't linger. This is correct under the single-CTL-chain contract on this
|
||||
// processor (one instance per issuer; see class comment).
|
||||
if (format->isFullCtl) {
|
||||
m_enrolment_authorities.clear();
|
||||
m_authorization_authorities.clear();
|
||||
m_distribution_centres.clear();
|
||||
m_root_cas.clear();
|
||||
m_trust_list_managers.clear();
|
||||
}
|
||||
for (int i = 0; i < format->ctlCommands.list.count; ++i) {
|
||||
this->process(*format->ctlCommands.list.array[i], *ctl_signer);
|
||||
}
|
||||
}
|
||||
|
||||
void CertificateTrustListProcessor::process(const Vanetza_Security_CtlCommand_t& cmd, const HashedId8& ctl_signer)
|
||||
{
|
||||
switch (cmd.present) {
|
||||
case Vanetza_Security_CtlCommand_PR_add:
|
||||
add(cmd.choice.add, ctl_signer);
|
||||
break;
|
||||
case Vanetza_Security_CtlCommand_PR_delete:
|
||||
remove(cmd.choice.Delete);
|
||||
break;
|
||||
default:
|
||||
// no op
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void CertificateTrustListProcessor::add(const Vanetza_Security_CtlEntry_t& entry, const HashedId8& ctl_signer)
|
||||
{
|
||||
switch (entry.present) {
|
||||
case Vanetza_Security_CtlEntry_PR_aa:
|
||||
add_authorization_authority(entry.choice.aa, ctl_signer);
|
||||
break;
|
||||
case Vanetza_Security_CtlEntry_PR_ea:
|
||||
add_enrolment_authority(entry.choice.ea, ctl_signer);
|
||||
break;
|
||||
case Vanetza_Security_CtlEntry_PR_dc:
|
||||
add_distribution_centre(entry.choice.dc);
|
||||
break;
|
||||
case Vanetza_Security_CtlEntry_PR_rca:
|
||||
add_root_ca(entry.choice.rca);
|
||||
break;
|
||||
case Vanetza_Security_CtlEntry_PR_tlm:
|
||||
add_trust_list_manager(entry.choice.tlm);
|
||||
break;
|
||||
default:
|
||||
// no op
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void CertificateTrustListProcessor::remove(const Vanetza_Security_CtlDelete_t& removal)
|
||||
{
|
||||
switch (removal.present) {
|
||||
case Vanetza_Security_CtlDelete_PR_cert:
|
||||
remove_certificate(removal.choice.cert);
|
||||
break;
|
||||
case Vanetza_Security_CtlDelete_PR_dc:
|
||||
remove_dc(removal.choice.dc);
|
||||
break;
|
||||
default:
|
||||
// no op
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void CertificateTrustListProcessor::add_root_ca(const Vanetza_Security_RootCaEntry_t& rca)
|
||||
{
|
||||
// The RCA's self-signed certificate is the trust anchor; key by its own HashedId8.
|
||||
// The optional successorTo backlink (TS 102 941 v1.4.1 §6.3.4) is informational and
|
||||
// ignored here — see [[gap-link-certificates]] for the rollover mechanism.
|
||||
Certificate cert(rca.selfsignedRootCa);
|
||||
HashedId8 hid = cert.calculate_hashed_id8(*m_security);
|
||||
m_root_cas.insert_or_assign(hid, std::move(cert));
|
||||
}
|
||||
|
||||
void CertificateTrustListProcessor::add_authorization_authority(const Vanetza_Security_AaEntry_t& entry,
|
||||
const HashedId8& ctl_signer)
|
||||
{
|
||||
AuthorizationAuthority aa;
|
||||
aa.access_point.assign(reinterpret_cast<const char*>(entry.accessPoint.buf), entry.accessPoint.size);
|
||||
aa.certificate = Certificate(entry.aaCertificate);
|
||||
m_authorization_authorities.insert_or_assign(ctl_signer, std::move(aa));
|
||||
}
|
||||
|
||||
void CertificateTrustListProcessor::add_enrolment_authority(const Vanetza_Security_EaEntry_t& entry,
|
||||
const HashedId8& ctl_signer)
|
||||
{
|
||||
EnrolmentAuthority ea;
|
||||
ea.aa_access_point.assign(reinterpret_cast<const char*>(entry.aaAccessPoint.buf), entry.aaAccessPoint.size);
|
||||
if (entry.itsAccessPoint) {
|
||||
auto buf = reinterpret_cast<const char*>(entry.itsAccessPoint->buf);
|
||||
ea.its_access_point.assign(buf, entry.itsAccessPoint->size);
|
||||
}
|
||||
ea.certificate = Certificate(entry.eaCertificate);
|
||||
m_enrolment_authorities.insert_or_assign(ctl_signer, std::move(ea));
|
||||
}
|
||||
|
||||
void CertificateTrustListProcessor::add_distribution_centre(const Vanetza_Security_DcEntry_t& dc)
|
||||
{
|
||||
std::string url(reinterpret_cast<const char*>(dc.url.buf), dc.url.size);
|
||||
for (int i = 0; i < dc.cert.list.count; ++i) {
|
||||
const Vanetza_Security_HashedId8_t* cert = dc.cert.list.array[i];
|
||||
if (!cert) {
|
||||
continue;
|
||||
}
|
||||
if (auto hid = HashedId8::from_buffer(*cert)) {
|
||||
m_distribution_centres.insert_or_assign(*hid, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CertificateTrustListProcessor::add_trust_list_manager(const Vanetza_Security_TlmEntry_t& tlm)
|
||||
{
|
||||
// CPOC access point (tlm.accessPoint) not tracked here; add a parallel map if callers need it.
|
||||
Certificate cert(tlm.selfSignedTLMCertificate);
|
||||
HashedId8 hid = cert.calculate_hashed_id8(*m_security);
|
||||
m_trust_list_managers.insert_or_assign(hid, std::move(cert));
|
||||
}
|
||||
|
||||
void CertificateTrustListProcessor::remove_certificate(const Vanetza_Security_HashedId8_t& id)
|
||||
{
|
||||
HashedId8 hid;
|
||||
hid.octets = security::v3::convert(id);
|
||||
m_enrolment_authorities.erase(hid);
|
||||
m_authorization_authorities.erase(hid);
|
||||
m_distribution_centres.erase(hid);
|
||||
m_root_cas.erase(hid);
|
||||
m_trust_list_managers.erase(hid);
|
||||
}
|
||||
|
||||
void CertificateTrustListProcessor::remove_dc(const Vanetza_Security_Url_t& url)
|
||||
{
|
||||
std::string target(reinterpret_cast<const char*>(url.buf), url.size);
|
||||
for (auto it = m_distribution_centres.begin(); it != m_distribution_centres.end();) {
|
||||
if (it->second == target) {
|
||||
it = m_distribution_centres.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boost::optional<EnrolmentAuthority>
|
||||
CertificateTrustListProcessor::get_enrolment_authority(const HashedId8& root_ca) const
|
||||
{
|
||||
auto found = m_enrolment_authorities.find(root_ca);
|
||||
if (found != m_enrolment_authorities.end()) {
|
||||
return found->second;
|
||||
} else {
|
||||
return boost::none;
|
||||
}
|
||||
}
|
||||
|
||||
boost::optional<AuthorizationAuthority>
|
||||
CertificateTrustListProcessor::get_authorization_authority(const HashedId8& root_ca) const
|
||||
{
|
||||
auto found = m_authorization_authorities.find(root_ca);
|
||||
if (found != m_authorization_authorities.end()) {
|
||||
return found->second;
|
||||
} else {
|
||||
return boost::none;
|
||||
}
|
||||
}
|
||||
|
||||
boost::optional<Certificate> CertificateTrustListProcessor::get_root_ca(const HashedId8& digest) const
|
||||
{
|
||||
auto found = m_root_cas.find(digest);
|
||||
if (found != m_root_cas.end()) {
|
||||
return found->second;
|
||||
}
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
boost::optional<Certificate> CertificateTrustListProcessor::get_trust_list_manager(const HashedId8& digest) const
|
||||
{
|
||||
auto found = m_trust_list_managers.find(digest);
|
||||
if (found != m_trust_list_managers.end()) {
|
||||
return found->second;
|
||||
}
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
boost::optional<std::string> CertificateTrustListProcessor::get_distribution_centre(const HashedId8& cert_digest) const
|
||||
{
|
||||
auto found = m_distribution_centres.find(cert_digest);
|
||||
if (found != m_distribution_centres.end()) {
|
||||
return found->second;
|
||||
}
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::string build_ctl_name(SecurityModule& security, const Vanetza_Security_EtsiTs103097Certificate_t& cert)
|
||||
{
|
||||
auto name = get_name(cert);
|
||||
auto hid8 = hexstring(calculate_hashed_id8(security, cert));
|
||||
if (name.empty()) {
|
||||
return "<HashedId8:" + hid8 + ">";
|
||||
} else {
|
||||
return name + " (" + hid8 + ")";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void CtlListingVisitor::add_root_ca(const Vanetza_Security_RootCaEntry_t& rca)
|
||||
{
|
||||
std::cout << "- Root CA: " << build_ctl_name(m_security, rca.selfsignedRootCa) << "\n";
|
||||
Certificate root_ca_cert { rca.selfsignedRootCa };
|
||||
std::cout << "|-> valid: from " << Clock::at(root_ca_cert.valid_since()) << " until "
|
||||
<< Clock::at(root_ca_cert.valid_until()) << "\n";
|
||||
}
|
||||
|
||||
void CtlListingVisitor::add_trust_list_manager(const Vanetza_Security_TlmEntry_t& tlm)
|
||||
{
|
||||
std::cout << "- TLM: " << build_ctl_name(m_security, tlm.selfSignedTLMCertificate) << "\n";
|
||||
std::cout << "|-> access point: " << to_string(tlm.accessPoint) << "\n";
|
||||
}
|
||||
|
||||
void CtlListingVisitor::add_distribution_centre(const Vanetza_Security_DcEntry_t& dc)
|
||||
{
|
||||
std::cout << "- DC: " << to_string(dc.url) << "\n";
|
||||
for (int i = 0; i < dc.cert.list.count; ++i) {
|
||||
auto digest = security::v3::convert(*dc.cert.list.array[i]);
|
||||
std::cout << "|-> for Root CA " << hexstring(digest) << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
void CtlListingVisitor::add_authorization_authority(const Vanetza_Security_AaEntry_t& aa)
|
||||
{
|
||||
std::cout << "- AA: " << to_string(aa.accessPoint) << "\n";
|
||||
}
|
||||
|
||||
void CtlListingVisitor::add_enrolment_authority(const Vanetza_Security_EaEntry_t& ea)
|
||||
{
|
||||
std::cout << "- EA: " << to_string(ea.aaAccessPoint) << " [AA]\n";
|
||||
if (ea.itsAccessPoint) {
|
||||
std::cout << "- EA: " << to_string(*ea.itsAccessPoint) << " [ITS]\n";
|
||||
}
|
||||
}
|
||||
|
||||
CertificateExportVisitor::CertificateExportVisitor(
|
||||
std::shared_ptr<CertificateStorage> aa, std::shared_ptr<CertificateStorage> ea)
|
||||
: m_aa(std::move(aa)), m_ea(std::move(ea))
|
||||
{
|
||||
}
|
||||
|
||||
void CertificateExportVisitor::add_authorization_authority(const Vanetza_Security_AaEntry_t& entry)
|
||||
{
|
||||
if (m_aa) {
|
||||
m_aa->store(Certificate(entry.aaCertificate));
|
||||
++m_exported_aa;
|
||||
}
|
||||
}
|
||||
|
||||
void CertificateExportVisitor::add_enrolment_authority(const Vanetza_Security_EaEntry_t& entry)
|
||||
{
|
||||
if (m_ea) {
|
||||
m_ea->store(Certificate(entry.eaCertificate));
|
||||
++m_exported_ea;
|
||||
}
|
||||
}
|
||||
|
||||
CertificateTrustListProcessor process_stored_ctl(const TrustListStorage& trust_lists,
|
||||
std::shared_ptr<SecurityModule> security, const HashedId8& root_ca)
|
||||
{
|
||||
auto maybe_ctl = trust_lists.fetch(root_ca);
|
||||
if (!maybe_ctl) {
|
||||
throw UsageError("no CTL is in store for the current Root CA", "fetch a CTL via 'dc fetch ctl'");
|
||||
}
|
||||
CertificateTrustListProcessor processor(std::move(security));
|
||||
processor.process(*maybe_ctl);
|
||||
return processor;
|
||||
}
|
||||
|
||||
EnrolmentAuthority require_enrolment_authority(const CertificateTrustListProcessor& processor, const HashedId8& root_ca)
|
||||
{
|
||||
auto maybe_ea = processor.get_enrolment_authority(root_ca);
|
||||
if (!maybe_ea) {
|
||||
throw UsageError("no Enrolment Authority is known for the current Root CA", "fetch a fresh CTL");
|
||||
}
|
||||
if (!maybe_ea->certificate.get_encryption_key()) {
|
||||
throw DecodingFailure("missing encryption key in EA certificate");
|
||||
}
|
||||
return std::move(*maybe_ea);
|
||||
}
|
||||
|
||||
AuthorizationAuthority require_authorization_authority(const CertificateTrustListProcessor& processor,
|
||||
const HashedId8& root_ca)
|
||||
{
|
||||
auto maybe_aa = processor.get_authorization_authority(root_ca);
|
||||
if (!maybe_aa) {
|
||||
throw UsageError("no Authorization Authority is known for the current Root CA", "fetch a fresh CTL");
|
||||
}
|
||||
if (!maybe_aa->certificate.get_encryption_key()) {
|
||||
throw DecodingFailure("missing encryption key in AA certificate");
|
||||
}
|
||||
return std::move(*maybe_aa);
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,228 @@
|
||||
#pragma once
|
||||
|
||||
#include "certificate.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include <vanetza/asn1/asn1c_wrapper.hpp>
|
||||
#include <vanetza/asn1/security/CtlCommand.h>
|
||||
#include <vanetza/asn1/security/TlmCertificateTrustListMessage.h>
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
// forward declarations
|
||||
class Certificate;
|
||||
class CertificateStorage;
|
||||
class SecurityModule;
|
||||
class TrustListStorage;
|
||||
|
||||
/// \brief Visitor for the entries of a TLM certificate trust list.
|
||||
class CtlVisitor
|
||||
{
|
||||
public:
|
||||
virtual ~CtlVisitor() = default;
|
||||
|
||||
// add commands
|
||||
virtual void add_root_ca(const Vanetza_Security_RootCaEntry_t&)
|
||||
{
|
||||
}
|
||||
|
||||
virtual void add_trust_list_manager(const Vanetza_Security_TlmEntry_t&)
|
||||
{
|
||||
}
|
||||
|
||||
virtual void add_distribution_centre(const Vanetza_Security_DcEntry_t&)
|
||||
{
|
||||
}
|
||||
|
||||
virtual void add_authorization_authority(const Vanetza_Security_AaEntry_t&)
|
||||
{
|
||||
}
|
||||
|
||||
virtual void add_enrolment_authority(const Vanetza_Security_EaEntry_t&)
|
||||
{
|
||||
}
|
||||
|
||||
// delete commands
|
||||
virtual void remove_certificate(const Vanetza_Security_HashedId8_t&)
|
||||
{
|
||||
}
|
||||
|
||||
virtual void remove_distribution_centre(const Vanetza_Security_Url_t&)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Prints CTL entries to stdout as a human-readable tree.
|
||||
*
|
||||
* Works for both RCA CTLs (visit_rca_ctl) and TLM CTLs/ECTLs (visit_tlm_ctl).
|
||||
*/
|
||||
class CtlListingVisitor : public CtlVisitor
|
||||
{
|
||||
public:
|
||||
explicit CtlListingVisitor(SecurityModule& security) : m_security(security)
|
||||
{
|
||||
}
|
||||
|
||||
void add_root_ca(const Vanetza_Security_RootCaEntry_t&) override;
|
||||
void add_trust_list_manager(const Vanetza_Security_TlmEntry_t&) override;
|
||||
void add_distribution_centre(const Vanetza_Security_DcEntry_t&) override;
|
||||
void add_authorization_authority(const Vanetza_Security_AaEntry_t&) override;
|
||||
void add_enrolment_authority(const Vanetza_Security_EaEntry_t&) override;
|
||||
|
||||
private:
|
||||
SecurityModule& m_security;
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Exports the AA/EA certificates embedded in an RCA CTL to standalone storage.
|
||||
*
|
||||
* Run after a CTL has been validated and stored so downstream consumers (e.g. a
|
||||
* security stack) can load issuer certificates as plain cert files without
|
||||
* having to parse CTLs themselves.
|
||||
*/
|
||||
class CertificateExportVisitor : public CtlVisitor
|
||||
{
|
||||
public:
|
||||
CertificateExportVisitor(std::shared_ptr<CertificateStorage> aa, std::shared_ptr<CertificateStorage> ea);
|
||||
|
||||
void add_authorization_authority(const Vanetza_Security_AaEntry_t&) override;
|
||||
void add_enrolment_authority(const Vanetza_Security_EaEntry_t&) override;
|
||||
|
||||
std::size_t exported_aa_certificates() const { return m_exported_aa; }
|
||||
std::size_t exported_ea_certificates() const { return m_exported_ea; }
|
||||
|
||||
private:
|
||||
std::shared_ptr<CertificateStorage> m_aa;
|
||||
std::shared_ptr<CertificateStorage> m_ea;
|
||||
std::size_t m_exported_aa = 0;
|
||||
std::size_t m_exported_ea = 0;
|
||||
};
|
||||
|
||||
class CertificateTrustList
|
||||
{
|
||||
public:
|
||||
CertificateTrustList();
|
||||
bool decode(const std::string&);
|
||||
bool decode(const ByteBuffer&);
|
||||
ByteBuffer encode() const;
|
||||
|
||||
/**
|
||||
* Read the OER-encoded trust list message from a file and decode it.
|
||||
*
|
||||
* \throws DecodingFailure if the file is empty or decoding fails
|
||||
*/
|
||||
static CertificateTrustList from_file(const std::filesystem::path&);
|
||||
|
||||
boost::optional<HashedId8> get_hashed_id8(SecurityModule&) const;
|
||||
|
||||
/**
|
||||
* Per TS 102 941 v1.4.1 §6.3.4: full CTL carries the complete trust state
|
||||
* (only add commands); delta CTL carries changes (adds + deletes) on top of
|
||||
* the previous full list with ctlSequence one less than this one.
|
||||
*
|
||||
* Returns boost::none if the inner CtlFormat cannot be decoded.
|
||||
*/
|
||||
boost::optional<bool> is_full_ctl() const;
|
||||
boost::optional<std::uint8_t> ctl_sequence() const;
|
||||
|
||||
/**
|
||||
* Iterate the TLM certificate trust list entries through `visitor`.
|
||||
*
|
||||
* \throws DecodingFailure on missing payload, malformed management data,
|
||||
* or content that is not a TLM certificate trust list
|
||||
*/
|
||||
void visit_tlm_ctl(CtlVisitor&) const;
|
||||
|
||||
// Same as visit_tlm_ctl but for RCA certificate trust lists.
|
||||
void visit_rca_ctl(CtlVisitor&) const;
|
||||
|
||||
void print() const;
|
||||
|
||||
const Vanetza_Security_EtsiTs103097Data_t& raw() const
|
||||
{
|
||||
return *m_asn1;
|
||||
}
|
||||
|
||||
private:
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_TlmCertificateTrustListMessage_t> m_asn1;
|
||||
};
|
||||
|
||||
struct EnrolmentAuthority
|
||||
{
|
||||
Certificate certificate;
|
||||
std::string aa_access_point;
|
||||
std::string its_access_point;
|
||||
};
|
||||
|
||||
struct AuthorizationAuthority
|
||||
{
|
||||
Certificate certificate;
|
||||
std::string access_point;
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Decoded view of a CertificateTrustList's commands.
|
||||
*
|
||||
* One instance processes a single CTL chain — either (full, delta, delta, ...)
|
||||
* from one RCA, or the same from one TLM. Mixing issuers (e.g. an RCA CTL and
|
||||
* an ECTL) on the same instance is unsupported: a full CTL clears all state,
|
||||
* so later content from a different issuer would silently wipe earlier content.
|
||||
* Build a fresh processor per chain.
|
||||
*/
|
||||
class CertificateTrustListProcessor
|
||||
{
|
||||
public:
|
||||
CertificateTrustListProcessor(std::shared_ptr<SecurityModule>);
|
||||
void process(const CertificateTrustList&);
|
||||
|
||||
boost::optional<EnrolmentAuthority> get_enrolment_authority(const HashedId8&) const;
|
||||
boost::optional<AuthorizationAuthority> get_authorization_authority(const HashedId8&) const;
|
||||
boost::optional<Certificate> get_root_ca(const HashedId8&) const;
|
||||
boost::optional<Certificate> get_trust_list_manager(const HashedId8&) const;
|
||||
boost::optional<std::string> get_distribution_centre(const HashedId8&) const;
|
||||
|
||||
protected:
|
||||
void process(const Vanetza_Security_CtlCommand_t&, const HashedId8& ctl_signer);
|
||||
void add(const Vanetza_Security_CtlEntry_t&, const HashedId8& ctl_signer);
|
||||
void remove(const Vanetza_Security_CtlDelete_t&);
|
||||
|
||||
void add_root_ca(const Vanetza_Security_RootCaEntry_t&);
|
||||
void add_authorization_authority(const Vanetza_Security_AaEntry_t&, const HashedId8& ctl_signer); // signer = issuing RCA, key for AA map
|
||||
void add_enrolment_authority(const Vanetza_Security_EaEntry_t&, const HashedId8& ctl_signer); // signer = issuing RCA, key for EA map
|
||||
void add_distribution_centre(const Vanetza_Security_DcEntry_t&);
|
||||
void add_trust_list_manager(const Vanetza_Security_TlmEntry_t&);
|
||||
|
||||
void remove_certificate(const Vanetza_Security_HashedId8_t&);
|
||||
void remove_dc(const Vanetza_Security_Url_t&);
|
||||
|
||||
private:
|
||||
std::shared_ptr<SecurityModule> m_security;
|
||||
std::map<HashedId8, EnrolmentAuthority> m_enrolment_authorities;
|
||||
std::map<HashedId8, AuthorizationAuthority> m_authorization_authorities;
|
||||
std::map<HashedId8, std::string> m_distribution_centres;
|
||||
std::map<HashedId8, Certificate> m_root_cas;
|
||||
std::map<HashedId8, Certificate> m_trust_list_managers;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch and process the stored CTL for `root_ca`.
|
||||
*
|
||||
* \throws UsageError if no CTL is stored for `root_ca`
|
||||
*/
|
||||
CertificateTrustListProcessor process_stored_ctl(const TrustListStorage& trust_lists,
|
||||
std::shared_ptr<SecurityModule> security, const HashedId8& root_ca);
|
||||
|
||||
// Require the EA/AA for `root_ca` to be listed and carry an encryption key.
|
||||
EnrolmentAuthority require_enrolment_authority(const CertificateTrustListProcessor&, const HashedId8& root_ca);
|
||||
AuthorizationAuthority require_authorization_authority(const CertificateTrustListProcessor&, const HashedId8& root_ca);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "asn1.hpp"
|
||||
#include "certificate_trust_list.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
boost::optional<std::string> lookup_dc_url(const std::filesystem::path& ectl_file, const HashedId8& root_ca)
|
||||
{
|
||||
struct DcUrlFinder : CtlVisitor
|
||||
{
|
||||
const HashedId8& root_ca;
|
||||
boost::optional<std::string> url;
|
||||
|
||||
explicit DcUrlFinder(const HashedId8& ca) : root_ca(ca)
|
||||
{
|
||||
}
|
||||
|
||||
void add_distribution_centre(const Vanetza_Security_DcEntry_t& dc) override
|
||||
{
|
||||
if (url) {
|
||||
return;
|
||||
}
|
||||
for (int j = 0; j < dc.cert.list.count; ++j) {
|
||||
if (dc.cert.list.array[j] && equals(*dc.cert.list.array[j], root_ca)) {
|
||||
url = to_string(dc.url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
CertificateTrustList ctl = CertificateTrustList::from_file(ectl_file);
|
||||
DcUrlFinder finder { root_ca };
|
||||
ctl.visit_tlm_ctl(finder);
|
||||
return finder.url;
|
||||
} catch (const std::exception&) {
|
||||
// ECTL missing or malformed: treat as "no DC URL available"
|
||||
return boost::none;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,322 @@
|
||||
#include "cpoc.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "certificate_storage.hpp"
|
||||
#include "certificate_trust_list.hpp"
|
||||
#include "exception.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "hashed_id8_validator.hpp"
|
||||
#include "http.hpp"
|
||||
#include "time.hpp"
|
||||
#include "validation.hpp"
|
||||
#include <boost/beast/http/field.hpp>
|
||||
#include <boost/date_time/posix_time/posix_time_io.hpp>
|
||||
#include <functional>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct Context
|
||||
{
|
||||
Context(const MainConfig& c) : cfg(c)
|
||||
{
|
||||
}
|
||||
|
||||
std::filesystem::path ectl_file() const
|
||||
{
|
||||
return cfg.data_path / "ectl.ctl";
|
||||
}
|
||||
|
||||
const MainConfig& cfg;
|
||||
std::string cpoc_url = "https://cpoc.jrc.ec.europa.eu/L0";
|
||||
HashedId8 hid8;
|
||||
std::function<void()> action;
|
||||
};
|
||||
|
||||
const HashedId8Validator hid8_validator;
|
||||
|
||||
Certificate fetch_tlm_certificate(const std::string& base_url, const HashedId8* id)
|
||||
{
|
||||
auto query = HttpQuery::from_url(base_url + "/gettlmcertificate/" + (id ? hexstring(*id) : ""));
|
||||
auto response = http_get(query);
|
||||
if (response.result() != boost::beast::http::status::ok) {
|
||||
throw HttpException("CPOC returned an unexpected HTTP status when fetching TLM certificate",
|
||||
std::move(response));
|
||||
} else if (response[boost::beast::http::field::content_type] != "application/octet-stream") {
|
||||
throw HttpException("did not receive bytes from CPOC when fetching TLM certificate");
|
||||
} else {
|
||||
Certificate cert;
|
||||
if (!cert.decode(response.body())) {
|
||||
throw DecodingFailure("decoding received TLM certificate failed");
|
||||
} else {
|
||||
return cert;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boost::optional<std::tuple<Certificate, HashedId8>> lookup_default_tlm_certificate(const Context& context)
|
||||
{
|
||||
const CertificateStorage& storage = *context.cfg.tlm;
|
||||
boost::optional<std::tuple<Certificate, HashedId8>> match;
|
||||
Clock::time_point now = current_time();
|
||||
|
||||
for (const HashedId8& hid8 : storage.list()) {
|
||||
boost::optional<Certificate> candidate = storage.fetch(hid8);
|
||||
if (candidate && is_currently_valid(*candidate, now)) {
|
||||
if (!match) {
|
||||
match = std::make_tuple(*candidate, hid8);
|
||||
} else if (std::get<0>(*match).valid_since() < candidate->valid_since()) {
|
||||
match = std::make_tuple(*candidate, hid8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
void list_tlm(Context& context)
|
||||
{
|
||||
const Clock::time_point now = current_time();
|
||||
std::cout << "Trusted TLM certificates are:\n";
|
||||
for (const HashedId8& hid8 : context.cfg.tlm->list()) {
|
||||
boost::optional<Certificate> cert = context.cfg.tlm->fetch(hid8);
|
||||
if (!cert) {
|
||||
std::cout << "- " << hexstring(hid8) << "\n";
|
||||
continue;
|
||||
}
|
||||
const std::string name = cert->get_name();
|
||||
std::cout << "- " << (name.empty() ? hexstring(hid8) : name + " (" + hexstring(hid8) + ")") << "\n";
|
||||
const char* status = "valid";
|
||||
if (now < cert->valid_since()) {
|
||||
status = "not yet valid";
|
||||
} else if (now > cert->valid_until()) {
|
||||
status = "expired";
|
||||
}
|
||||
std::cout << " |-> valid from " << Clock::at(cert->valid_since()) << " until "
|
||||
<< Clock::at(cert->valid_until()) << " [" << status << "]\n";
|
||||
}
|
||||
}
|
||||
|
||||
void fetch_tlm(Context& context, const HashedId8* id, bool dry_run)
|
||||
{
|
||||
Certificate tlm = fetch_tlm_certificate(context.cpoc_url, id);
|
||||
const HashedId8 tlm_hid8 = tlm.calculate_hashed_id8(*context.cfg.security);
|
||||
if (id && *id != tlm_hid8) {
|
||||
throw VerificationFailure("fetched TLM certificate's HashedId8 does not match requested HashedId8");
|
||||
}
|
||||
const std::string name = tlm.get_name();
|
||||
const std::string hex_hid8 = hexstring(tlm_hid8);
|
||||
if (dry_run) {
|
||||
std::cout << "CPOC can provide TLM certificate "
|
||||
<< (name.empty() ? hex_hid8 : "\"" + name + "\" (" + hex_hid8 + ")")
|
||||
<< ". Storage unchanged in this dry run.\n";
|
||||
return;
|
||||
}
|
||||
context.cfg.tlm->store(tlm);
|
||||
if (name.empty()) {
|
||||
std::cout << "Added TLM certificate (" << hex_hid8 << ")\n";
|
||||
} else {
|
||||
std::cout << "Added TLM certificate \"" << name << "\" (" << hex_hid8 << ")\n";
|
||||
}
|
||||
}
|
||||
|
||||
void discard_tlm(Context& context, const HashedId8& id)
|
||||
{
|
||||
if (context.cfg.tlm->erase(id)) {
|
||||
std::cout << "Removed " << hexstring(id) << " from trusted TLM certificates\n";
|
||||
} else {
|
||||
std::cout << "No TLM certificate with " << hexstring(id) << " found in local storage\n";
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<CLI::App> build_tlm_command(std::shared_ptr<Context> context)
|
||||
{
|
||||
auto app = std::make_shared<CLI::App>("Trust List Manager", "tlm");
|
||||
|
||||
auto list = app->add_subcommand("list", "list all trusted TLM certificates");
|
||||
list->callback([context]() { context->action = [context]() { list_tlm(*context); }; });
|
||||
|
||||
auto fetch = app->add_subcommand("fetch", "fetch a TLM certificate from CPOC (omit the id to fetch the latest)");
|
||||
auto fetch_id =
|
||||
fetch->add_option("hid8", context->hid8, "HashedId8 of the TLM certificate to fetch; omit for the latest")
|
||||
->check(hid8_validator);
|
||||
auto fetch_dry = fetch->add_flag("--dry-run,-n", "do not store the fetched certificate");
|
||||
fetch->callback([context, fetch_id, fetch_dry]() {
|
||||
const bool has_id = fetch_id->count() > 0;
|
||||
const bool dry_run = fetch_dry->as<bool>();
|
||||
context->action = [context, has_id, dry_run]() {
|
||||
fetch_tlm(*context, has_id ? &context->hid8 : nullptr, dry_run);
|
||||
};
|
||||
});
|
||||
|
||||
auto discard = app->add_subcommand("discard", "discard a TLM certificate (distrust it)");
|
||||
discard->add_option("hid8", context->hid8, "HashedId8 of the TLM certificate to discard")
|
||||
->required()
|
||||
->check(hid8_validator);
|
||||
discard->callback([context]() { context->action = [context]() { discard_tlm(*context, context->hid8); }; });
|
||||
|
||||
app->final_callback([context]() {
|
||||
if (!context->action) {
|
||||
context->action = [context]() { list_tlm(*context); };
|
||||
}
|
||||
context->action();
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
void list_ectl(Context& context)
|
||||
{
|
||||
CertificateTrustList ctl = CertificateTrustList::from_file(context.ectl_file());
|
||||
std::cout << "ECTL contains:\n";
|
||||
CtlListingVisitor visitor(*context.cfg.security);
|
||||
ctl.visit_tlm_ctl(visitor);
|
||||
}
|
||||
|
||||
// tlm_id == nullptr -> use the locally known latest TLM certificate.
|
||||
void fetch_ectl(Context& context, const HashedId8* tlm_id, bool dry_run)
|
||||
{
|
||||
CertificateStorage& storage = *context.cfg.tlm;
|
||||
|
||||
HashedId8 tlm_hid8;
|
||||
if (tlm_id) {
|
||||
tlm_hid8 = *tlm_id;
|
||||
} else {
|
||||
auto lookup = lookup_default_tlm_certificate(context);
|
||||
if (!lookup) {
|
||||
throw UsageError("no TLM certificate found", "run 'cpoc tlm fetch' first");
|
||||
}
|
||||
tlm_hid8 = std::get<1>(*lookup);
|
||||
}
|
||||
|
||||
struct UpdateVisitor : CtlVisitor
|
||||
{
|
||||
UpdateVisitor(std::shared_ptr<CertificateStorage> certs) : certificates(certs)
|
||||
{
|
||||
}
|
||||
|
||||
void add_root_ca(const Vanetza_Security_RootCaEntry_t& rca) override
|
||||
{
|
||||
Certificate root_ca { rca.selfsignedRootCa };
|
||||
certificates->store(root_ca);
|
||||
}
|
||||
|
||||
std::shared_ptr<CertificateStorage> certificates;
|
||||
};
|
||||
|
||||
auto query = HttpQuery::from_url(context.cpoc_url + "/getectl/" + hexstring(tlm_hid8));
|
||||
auto response = http_get(query);
|
||||
if (response.result() != boost::beast::http::status::ok) {
|
||||
throw HttpException("CPOC returned an unexpected HTTP status when fetching full ECTL",
|
||||
std::move(response));
|
||||
} else if (response[boost::beast::http::field::content_type] != "application/octet-stream") {
|
||||
throw HttpException("did not receive bytes from CPOC when fetching full ECTL");
|
||||
}
|
||||
|
||||
CertificateTrustList tlm_message;
|
||||
if (!tlm_message.decode(response.body())) {
|
||||
throw DecodingFailure("decoding received TLM certificate list message failed");
|
||||
} else if (const Vanetza_Security_SignedData_t* sdata = get_signed_data(tlm_message.raw())) {
|
||||
const Vanetza_Security_EtsiTs103097Certificate_t* ectl_certificate = nullptr;
|
||||
if (sdata->signer.present == Vanetza_Security_SignerIdentifier_PR_digest) {
|
||||
if (!equals(sdata->signer.choice.digest, tlm_hid8)) {
|
||||
throw VerificationFailure("expected a different HashedId8 digest in response message");
|
||||
}
|
||||
} else if (sdata->signer.present == Vanetza_Security_SignerIdentifier_PR_certificate) {
|
||||
const Vanetza_Security_SequenceOfCertificate& certlist = sdata->signer.choice.certificate;
|
||||
if (certlist.list.count >= 1) {
|
||||
ectl_certificate = certlist.list.array[0];
|
||||
HashedId8 cert_hid8 = calculate_hashed_id8(*context.cfg.security, *ectl_certificate);
|
||||
if (cert_hid8 != tlm_hid8) {
|
||||
throw VerificationFailure("signing certificate's digest does not match requested HashedId8");
|
||||
}
|
||||
} else {
|
||||
throw DecodingFailure("missing certificate used for signing");
|
||||
}
|
||||
} else {
|
||||
throw VerificationFailure("received ECTL message is not signed by expected TLM HashedId8");
|
||||
}
|
||||
|
||||
boost::optional<Certificate> stored_tlm = storage.fetch(tlm_hid8);
|
||||
if (!stored_tlm) {
|
||||
if (!ectl_certificate) {
|
||||
throw UsageError("missing TLM certificate to verify ECTL", "run 'cpoc tlm fetch' first");
|
||||
}
|
||||
stored_tlm = Certificate(*ectl_certificate);
|
||||
if (!dry_run) {
|
||||
storage.store(*stored_tlm);
|
||||
}
|
||||
}
|
||||
|
||||
if (!validate(*context.cfg.security, *sdata, stored_tlm->raw())) {
|
||||
throw VerificationFailure("signature verification of ECTL failed");
|
||||
}
|
||||
|
||||
if (dry_run) {
|
||||
std::cout << "ECTL signature verified. Storage unchanged in this dry run.\n";
|
||||
return;
|
||||
}
|
||||
|
||||
write(context.ectl_file(), ByteBuffer { response.body().begin(), response.body().end() });
|
||||
std::cout << "Stored ECTL\n";
|
||||
|
||||
UpdateVisitor visitor(context.cfg.root_ca);
|
||||
tlm_message.visit_tlm_ctl(visitor);
|
||||
} else {
|
||||
throw DecodingFailure("message contains no signed data");
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<CLI::App> build_ectl_command(std::shared_ptr<Context> context)
|
||||
{
|
||||
auto app = std::make_shared<CLI::App>("European Certificate Trust List", "ectl");
|
||||
|
||||
auto list = app->add_subcommand("list", "list trusted CAs from the locally stored ECTL");
|
||||
list->callback([context]() { context->action = [context]() { list_ectl(*context); }; });
|
||||
|
||||
auto fetch =
|
||||
app->add_subcommand("fetch", "fetch the full ECTL signed by the TLM (omit the id to use the latest known TLM)");
|
||||
auto fetch_id =
|
||||
fetch->add_option("hid8", context->hid8, "HashedId8 of the TLM; omit to use the latest known TLM certificate")
|
||||
->check(hid8_validator);
|
||||
auto fetch_dry = fetch->add_flag("--dry-run,-n", "fetch and verify only; do not store the ECTL");
|
||||
fetch->callback([context, fetch_id, fetch_dry]() {
|
||||
const bool has_id = fetch_id->count() > 0;
|
||||
const bool dry_run = fetch_dry->as<bool>();
|
||||
context->action = [context, has_id, dry_run]() {
|
||||
fetch_ectl(*context, has_id ? &context->hid8 : nullptr, dry_run);
|
||||
};
|
||||
});
|
||||
|
||||
app->final_callback([context]() {
|
||||
if (!context->action) {
|
||||
context->action = [context]() { list_ectl(*context); };
|
||||
}
|
||||
context->action();
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::shared_ptr<CLI::App> build_cpoc_command(const MainConfig& config)
|
||||
{
|
||||
auto ctx = std::make_shared<Context>(config);
|
||||
auto app = std::make_shared<CLI::App>("C-ITS Point of Contact Protocol", "cpoc");
|
||||
app->add_option("--url", ctx->cpoc_url, "CPOC base URL")->capture_default_str();
|
||||
|
||||
app->add_subcommand(build_ectl_command(ctx));
|
||||
app->add_subcommand(build_tlm_command(ctx));
|
||||
app->require_subcommand();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "main.hpp"
|
||||
#include <CLI/App.hpp>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
std::shared_ptr<CLI::App> build_cpoc_command(const MainConfig&);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
#include "credential_filesystem_storage.hpp"
|
||||
#include "openssl.hpp"
|
||||
#include <boost/range/adaptor/filtered.hpp>
|
||||
#include <boost/range/adaptor/transformed.hpp>
|
||||
#include <boost/range/iterator_range.hpp>
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <system_error>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
CredentialFilesystemStorage::CredentialFilesystemStorage(const std::filesystem::path& root) : m_root(root)
|
||||
{
|
||||
std::filesystem::create_directories(m_root);
|
||||
}
|
||||
|
||||
std::filesystem::path CredentialFilesystemStorage::build_key_path(const PublicKey& key)
|
||||
{
|
||||
auto key_path = m_root / canonical_hexstring(key);
|
||||
key_path += ".pem";
|
||||
return key_path;
|
||||
}
|
||||
|
||||
void CredentialFilesystemStorage::store(const PublicKey& pub, const PrivateKey& priv)
|
||||
{
|
||||
std::filesystem::path path = build_key_path(pub);
|
||||
OpenSslPointer<BIO> out = make_owner_only_bio(path);
|
||||
|
||||
int nid = openssl_nid(priv.type);
|
||||
OpenSslPointer<EC_KEY> ec_key { EC_KEY_new_by_curve_name(nid), "construction of EC key failed" };
|
||||
OpenSslPointer<BIGNUM> bn_priv { BN_bin2bn(priv.key.data(), priv.key.size(), nullptr),
|
||||
"converting private key to BIGNUM failed" };
|
||||
openssl_result(EC_KEY_set_private_key(ec_key.raw(), bn_priv.raw()), "setting private key failed");
|
||||
|
||||
OpenSslPointer<EC_POINT> ecp_pub = make_ec_point(pub);
|
||||
openssl_result(EC_KEY_set_public_key(ec_key.raw(), ecp_pub.raw()), "setting public key failed");
|
||||
|
||||
openssl_result(EC_KEY_check_key(ec_key.raw()), "EC key is invalid");
|
||||
|
||||
OpenSslPointer<EVP_PKEY> key { EVP_PKEY_new() };
|
||||
openssl_result(EVP_PKEY_set1_EC_KEY(key.raw(), ec_key.raw()), "set1_EC_KEY");
|
||||
|
||||
openssl_result(PEM_write_bio_PKCS8PrivateKey(out.raw(), key.raw(), nullptr, nullptr, 0, nullptr, nullptr),
|
||||
"writing PEM encoded private key failed");
|
||||
}
|
||||
|
||||
boost::optional<PrivateKey> CredentialFilesystemStorage::fetch(const PublicKey& pub)
|
||||
{
|
||||
std::filesystem::path path = build_key_path(pub);
|
||||
if (std::filesystem::is_regular_file(path)) {
|
||||
OpenSslPointer<BIO> in { BIO_new_file(path.c_str(), "r"),
|
||||
"could not create OpenSSL BIO to read file at " + path.string() };
|
||||
OpenSslPointer<EVP_PKEY> key { PEM_read_bio_PrivateKey(in.raw(), nullptr, nullptr, nullptr),
|
||||
"reading private key failed" };
|
||||
|
||||
int pub_nid = openssl_nid(pub.type);
|
||||
const EC_KEY* ec_key = EVP_PKEY_get0_EC_KEY(key.raw());
|
||||
if (!ec_key) {
|
||||
throw std::runtime_error("key is not an EC key");
|
||||
}
|
||||
const EC_GROUP* ec_group = EC_KEY_get0_group(ec_key);
|
||||
if (!ec_group) {
|
||||
throw std::runtime_error("EC group is not set");
|
||||
}
|
||||
int key_nid = EC_GROUP_get_curve_name(ec_group);
|
||||
if (pub_nid != key_nid) {
|
||||
throw std::runtime_error("private key has different type than public key");
|
||||
}
|
||||
|
||||
PrivateKey priv;
|
||||
priv.type = pub.type;
|
||||
const BIGNUM* bn_priv = EC_KEY_get0_private_key(ec_key);
|
||||
priv.key.resize(BN_num_bytes(bn_priv));
|
||||
BN_bn2bin(bn_priv, priv.key.data());
|
||||
return priv;
|
||||
}
|
||||
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
bool CredentialFilesystemStorage::discard(const PublicKey& key)
|
||||
{
|
||||
std::error_code ec;
|
||||
return std::filesystem::remove(build_key_path(key), ec);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
/**
|
||||
* \brief Accept "02"|"03" prefix + 64 hex (P-256) or 96 hex (P-384) chars.
|
||||
*
|
||||
* Near-misses are rejected here so they are never flagged as orphans and deleted.
|
||||
*/
|
||||
bool is_valid_canonical_hex_stem(const std::string& stem)
|
||||
{
|
||||
if (stem.size() != 66 && stem.size() != 98) {
|
||||
return false;
|
||||
} else if (stem[0] != '0' || (stem[1] != '2' && stem[1] != '3')) {
|
||||
return false;
|
||||
}
|
||||
for (std::size_t i = 2; i < stem.size(); ++i) {
|
||||
char c = stem[i];
|
||||
const bool ok = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::filesystem::path CredentialFilesystemStorage::build_key_path(const std::string& canonical_hex) const
|
||||
{
|
||||
auto p = m_root / canonical_hex;
|
||||
p += ".pem";
|
||||
return p;
|
||||
}
|
||||
|
||||
CredentialNameRange CredentialFilesystemStorage::list() const
|
||||
{
|
||||
auto rng =
|
||||
boost::make_iterator_range(std::filesystem::directory_iterator(m_root), std::filesystem::directory_iterator()) |
|
||||
boost::adaptors::filtered([](const std::filesystem::directory_entry& e) {
|
||||
return e.path().extension() == ".pem" && std::filesystem::is_regular_file(e.path()) &&
|
||||
is_valid_canonical_hex_stem(e.path().stem().string());
|
||||
}) |
|
||||
boost::adaptors::transformed([](const std::filesystem::directory_entry& e) {
|
||||
return e.path().stem().string();
|
||||
});
|
||||
return CredentialNameRange(rng);
|
||||
}
|
||||
|
||||
bool CredentialFilesystemStorage::discard(const std::string& canonical_hex)
|
||||
{
|
||||
std::error_code ec;
|
||||
return std::filesystem::remove(build_key_path(canonical_hex), ec);
|
||||
}
|
||||
|
||||
bool CredentialFilesystemStorage::contains(const std::string& canonical_hex) const
|
||||
{
|
||||
return std::filesystem::is_regular_file(build_key_path(canonical_hex));
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "credential_storage.hpp"
|
||||
#include <filesystem>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class CredentialFilesystemStorage : public CredentialStorage
|
||||
{
|
||||
public:
|
||||
CredentialFilesystemStorage(const std::filesystem::path& root);
|
||||
|
||||
void store(const PublicKey&, const PrivateKey&) override;
|
||||
boost::optional<PrivateKey> fetch(const PublicKey&) override;
|
||||
bool discard(const PublicKey&) override;
|
||||
CredentialNameRange list() const override;
|
||||
bool discard(const std::string& canonical_hex) override;
|
||||
bool contains(const std::string& canonical_hex) const override;
|
||||
|
||||
private:
|
||||
std::filesystem::path build_key_path(const PublicKey&);
|
||||
std::filesystem::path build_key_path(const std::string& canonical_hex) const;
|
||||
|
||||
std::filesystem::path m_root;
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
|
||||
#include "keys.hpp"
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <boost/range/any_range.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
/// \brief Lazy, single-pass view over canonical-hex credential names.
|
||||
using CredentialNameRange =
|
||||
boost::any_range<std::string, boost::single_pass_traversal_tag, std::string, std::ptrdiff_t>;
|
||||
|
||||
class CredentialStorage
|
||||
{
|
||||
public:
|
||||
virtual ~CredentialStorage() = default;
|
||||
virtual void store(const PublicKey&, const PrivateKey&) = 0;
|
||||
virtual boost::optional<PrivateKey> fetch(const PublicKey&) = 0;
|
||||
virtual bool discard(const PublicKey&) = 0;
|
||||
|
||||
/// \brief Lazy iteration over the canonical-hex name of every stored credential.
|
||||
virtual CredentialNameRange list() const = 0;
|
||||
|
||||
/// \brief Discard a credential by its canonical-hex name; returns true iff one was removed.
|
||||
virtual bool discard(const std::string& canonical_hex) = 0;
|
||||
|
||||
/// \brief Existence check by canonical-hex name; cheap, no key parsing.
|
||||
virtual bool contains(const std::string& canonical_hex) const = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief RAII handle that discards a credential on destruction unless commit() was called.
|
||||
*
|
||||
* Use to bind a freshly generated key to the lifetime of a protocol step:
|
||||
* store now, drop the key if anything throws before the step succeeds.
|
||||
*/
|
||||
class ScopedCredential
|
||||
{
|
||||
public:
|
||||
ScopedCredential(CredentialStorage& storage, PublicKey pub) : m_storage(&storage), m_public_key(std::move(pub))
|
||||
{
|
||||
}
|
||||
|
||||
ScopedCredential(CredentialStorage& storage, PublicKey pub, const PrivateKey& priv) : ScopedCredential(storage, pub)
|
||||
{
|
||||
m_storage->store(m_public_key, priv);
|
||||
}
|
||||
|
||||
~ScopedCredential()
|
||||
{
|
||||
if (m_storage) {
|
||||
m_storage->discard(m_public_key);
|
||||
}
|
||||
}
|
||||
|
||||
// no copy
|
||||
ScopedCredential(const ScopedCredential&) = delete;
|
||||
ScopedCredential& operator=(const ScopedCredential&) = delete;
|
||||
// no move
|
||||
ScopedCredential(ScopedCredential&&) = delete;
|
||||
ScopedCredential& operator=(ScopedCredential&&) = delete;
|
||||
|
||||
void commit()
|
||||
{
|
||||
m_storage = nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
CredentialStorage* m_storage = nullptr;
|
||||
PublicKey m_public_key;
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,76 @@
|
||||
#include "crl_store.hpp"
|
||||
#include "certificate_revocation_list.hpp"
|
||||
#include "filesystem.hpp"
|
||||
#include "hexstring.hpp"
|
||||
#include "security_module.hpp"
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
CrlFilesystemStore::CrlFilesystemStore(std::shared_ptr<SecurityModule> security, const std::filesystem::path& root) :
|
||||
m_security(security), m_root(root)
|
||||
{
|
||||
std::filesystem::create_directories(m_root);
|
||||
for (const auto& entry : std::filesystem::directory_iterator(m_root)) {
|
||||
if (!entry.is_regular_file() || entry.path().extension() != ".crl") {
|
||||
continue;
|
||||
}
|
||||
ByteBuffer buf = read(entry.path());
|
||||
CertificateRevocationList crl;
|
||||
if (crl.decode(buf)) {
|
||||
index(crl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool CrlFilesystemStore::store(const CertificateRevocationList& crl)
|
||||
{
|
||||
auto issuer = index(crl);
|
||||
if (!issuer) {
|
||||
return false;
|
||||
}
|
||||
write(filename(*issuer), crl.encode());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CrlFilesystemStore::is_revoked(const security::HashedId8& issuer, const security::HashedId8& cert) const
|
||||
{
|
||||
auto it = m_revoked.find(issuer);
|
||||
if (it == m_revoked.end()) {
|
||||
return false;
|
||||
}
|
||||
return it->second.count(cert) != 0;
|
||||
}
|
||||
|
||||
std::filesystem::path CrlFilesystemStore::filename(const HashedId8& id) const
|
||||
{
|
||||
return m_root / (hexstring(id.octets) + ".crl");
|
||||
}
|
||||
|
||||
boost::optional<HashedId8> CrlFilesystemStore::index(const CertificateRevocationList& crl)
|
||||
{
|
||||
auto issuer = crl.get_hashed_id8(*m_security);
|
||||
if (!issuer) {
|
||||
return boost::none;
|
||||
}
|
||||
auto entries = crl.revoked_entries();
|
||||
if (!entries) {
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
// Empty CRL is valid: it withdraws any prior revocations from this issuer.
|
||||
// Erase the existing bucket and only create a new one if there's content.
|
||||
m_revoked.erase(issuer->octets);
|
||||
if (!entries->empty()) {
|
||||
auto& bucket = m_revoked[issuer->octets];
|
||||
for (const auto& cert : *entries) {
|
||||
bucket.insert(cert.octets);
|
||||
}
|
||||
}
|
||||
return issuer;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include "hashed_id8.hpp"
|
||||
#include <vanetza/security/hashed_id.hpp>
|
||||
#include <vanetza/security/v3/revocation_lookup.hpp>
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class CertificateRevocationList;
|
||||
class SecurityModule;
|
||||
|
||||
/**
|
||||
* Constructor scans \p root and indexes every "*.crl" file.
|
||||
* store() replaces any prior CRL from the same issuer (one file per issuer).
|
||||
*/
|
||||
class CrlFilesystemStore : public security::v3::RevocationLookup
|
||||
{
|
||||
public:
|
||||
CrlFilesystemStore(std::shared_ptr<SecurityModule>, const std::filesystem::path& root);
|
||||
|
||||
// Returns false if the CRL has no extractable issuer or a malformed payload.
|
||||
bool store(const CertificateRevocationList&);
|
||||
|
||||
bool is_revoked(const security::HashedId8& issuer, const security::HashedId8& cert) const override;
|
||||
|
||||
protected:
|
||||
std::filesystem::path filename(const HashedId8&) const;
|
||||
boost::optional<HashedId8> index(const CertificateRevocationList&);
|
||||
|
||||
private:
|
||||
std::shared_ptr<SecurityModule> m_security;
|
||||
std::filesystem::path m_root;
|
||||
std::unordered_map<security::HashedId8, std::unordered_set<security::HashedId8>> m_revoked;
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,219 @@
|
||||
#include "dc_command.hpp"
|
||||
#include "certificate_trust_list.hpp"
|
||||
#include "distribution_centre.hpp"
|
||||
#include "exception.hpp"
|
||||
#include "hashed_id8_validator.hpp"
|
||||
#include "http.hpp"
|
||||
#include "validation.hpp"
|
||||
#include <boost/date_time/posix_time/posix_time_io.hpp>
|
||||
#include <functional>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
const HashedId8Validator hid8_validator;
|
||||
|
||||
struct Context
|
||||
{
|
||||
Context(const MainConfig& c) : cfg(c)
|
||||
{
|
||||
}
|
||||
|
||||
void lookup_root_ca(CLI::Option* hid8_opt)
|
||||
{
|
||||
if (hid8_opt->count() == 0) {
|
||||
auto root_ca = cfg.station->get_root_ca();
|
||||
if (!root_ca) {
|
||||
throw UsageError("hid8 is required (station has no Root CA configured)");
|
||||
}
|
||||
hid8 = *root_ca;
|
||||
}
|
||||
}
|
||||
|
||||
std::string url() const
|
||||
{
|
||||
return resolve_url(cfg.dc_url, url_override);
|
||||
}
|
||||
|
||||
const MainConfig& cfg;
|
||||
HashedId8 hid8;
|
||||
std::string url_override;
|
||||
bool print = false;
|
||||
std::function<void()> action;
|
||||
};
|
||||
|
||||
void show_info(Context& ctx)
|
||||
{
|
||||
std::cout << "DC URL: " << (ctx.url().empty() ? "[not set]" : ctx.url()) << "\n";
|
||||
}
|
||||
|
||||
void fetch_ctl(Context& ctx, CLI::Option* hid8_opt, CLI::Option* dry_flag)
|
||||
{
|
||||
ctx.lookup_root_ca(hid8_opt);
|
||||
DistributionCentre dc;
|
||||
dc.set_url(ctx.url());
|
||||
auto ctl = dc.fetch_trust_list(ctx.hid8);
|
||||
if (ctl) {
|
||||
if (ctx.print) {
|
||||
CtlListingVisitor visitor(*ctx.cfg.security);
|
||||
ctl->visit_rca_ctl(visitor);
|
||||
}
|
||||
|
||||
auto ctl_digest = ctl->get_hashed_id8(*ctx.cfg.security);
|
||||
if (ctl_digest == ctx.hid8) {
|
||||
std::cout << "Fetched CTL matches Root CA digest\n";
|
||||
} else if (ctl_digest) {
|
||||
std::cout << "Fetched CTL has " << hexstring(*ctl_digest) << " digest mismatch.\n";
|
||||
} else {
|
||||
std::cout << "Cannot determine digest of CTL.\n";
|
||||
}
|
||||
|
||||
if (auto cert = ctx.cfg.root_ca->fetch(ctx.hid8)) {
|
||||
bool valid = validate(*ctx.cfg.security, ctl->raw(), *cert);
|
||||
if (valid) {
|
||||
if (dry_flag->as<bool>()) {
|
||||
std::cout << "CTL is valid. Storage unchanged in this dry run.\n";
|
||||
} else {
|
||||
ctx.cfg.trust_lists->store(*ctl);
|
||||
std::cout << "CTL is valid. Added to local trust list storage.\n";
|
||||
// Materialize the AA/EA certs embedded in the validated CTL
|
||||
CertificateExportVisitor exporter(ctx.cfg.authorization_authorities, ctx.cfg.enrolment_authorities);
|
||||
ctl->visit_rca_ctl(exporter);
|
||||
std::cout << "Exported " << exporter.exported_aa_certificates() << " AA and "
|
||||
<< exporter.exported_ea_certificates() << " EA certificate(s).\n";
|
||||
}
|
||||
} else {
|
||||
std::cout << "CTL cannot be trusted.\n";
|
||||
}
|
||||
} else {
|
||||
std::cout << "Cannot validate CTL because of missing Root CA certificate.\n";
|
||||
}
|
||||
} else {
|
||||
throw std::runtime_error("DC has no trust list matching HashedId8 " + hexstring(ctx.hid8));
|
||||
}
|
||||
}
|
||||
|
||||
void fetch_crl(Context& ctx, CLI::Option* hid8_opt, CLI::Option* dry_flag)
|
||||
{
|
||||
ctx.lookup_root_ca(hid8_opt);
|
||||
DistributionCentre dc;
|
||||
dc.set_url(ctx.url());
|
||||
auto crl = dc.fetch_revocation_list(ctx.hid8);
|
||||
if (!crl) {
|
||||
throw std::runtime_error("DC has no revocation list matching HashedId8 " + hexstring(ctx.hid8));
|
||||
}
|
||||
|
||||
auto crl_digest = crl->get_hashed_id8(*ctx.cfg.security);
|
||||
if (crl_digest == ctx.hid8) {
|
||||
std::cout << "Fetched CRL matches Root CA digest\n";
|
||||
} else if (crl_digest) {
|
||||
std::cout << "Fetched CRL has " << hexstring(*crl_digest) << " digest mismatch.\n";
|
||||
} else {
|
||||
std::cout << "Cannot determine digest of CRL.\n";
|
||||
}
|
||||
|
||||
if (ctx.print) {
|
||||
if (crl_digest) {
|
||||
std::cout << "Issuer: " << hexstring(*crl_digest) << "\n";
|
||||
}
|
||||
if (auto entries = crl->revoked_entries()) {
|
||||
std::cout << "Revoked certificates (" << entries->size() << "):\n";
|
||||
for (const auto& id : *entries) {
|
||||
std::cout << "- " << hexstring(id) << "\n";
|
||||
}
|
||||
} else {
|
||||
std::cout << "Revoked entries could not be decoded.\n";
|
||||
}
|
||||
}
|
||||
|
||||
auto root_cert = ctx.cfg.root_ca->fetch(ctx.hid8);
|
||||
if (!root_cert) {
|
||||
std::cout << "Cannot validate CRL because of missing Root CA certificate.\n";
|
||||
return;
|
||||
}
|
||||
if (!validate(*ctx.cfg.security, crl->raw(), *root_cert)) {
|
||||
std::cout << "CRL cannot be trusted.\n";
|
||||
return;
|
||||
}
|
||||
|
||||
if (dry_flag->as<bool>()) {
|
||||
std::cout << "CRL is valid. Storage unchanged in this dry run.\n";
|
||||
} else if (ctx.cfg.crl_store->store(*crl)) {
|
||||
std::cout << "CRL is valid. Added to local revocation list storage.\n";
|
||||
} else {
|
||||
std::cout << "CRL is valid but could not be indexed.\n";
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<CLI::App> build_info_command(std::shared_ptr<Context> ctx)
|
||||
{
|
||||
auto app = std::make_shared<CLI::App>("Distribution Centre info", "info");
|
||||
app->callback([ctx]() { ctx->action = [ctx]() { show_info(*ctx); }; });
|
||||
app->fallthrough();
|
||||
return app;
|
||||
}
|
||||
|
||||
std::shared_ptr<CLI::App> build_ctl_command(std::shared_ptr<Context> ctx)
|
||||
{
|
||||
auto app = std::make_shared<CLI::App>("fetch certificate trust list (CTL)", "ctl");
|
||||
app->alias("getctl");
|
||||
|
||||
auto hid8_opt = app->add_option("hid8", ctx->hid8, "HashedId8 of issuing entity (defaults to station's Root CA)")
|
||||
->check(hid8_validator);
|
||||
auto dry_flag = app->add_flag("--dry-run,-n", "fetch and validate only; do not store the CTL");
|
||||
|
||||
app->callback([ctx, hid8_opt, dry_flag]() {
|
||||
ctx->action = [ctx, hid8_opt, dry_flag]() { fetch_ctl(*ctx, hid8_opt, dry_flag); };
|
||||
});
|
||||
|
||||
app->fallthrough();
|
||||
return app;
|
||||
}
|
||||
|
||||
std::shared_ptr<CLI::App> build_crl_command(std::shared_ptr<Context> ctx)
|
||||
{
|
||||
auto app = std::make_shared<CLI::App>("fetch certificate revocation list (CRL)", "crl");
|
||||
app->alias("getcrl");
|
||||
|
||||
auto hid8_opt = app->add_option("hid8", ctx->hid8, "HashedId8 of issuing Root CA (defaults to station's Root CA)")
|
||||
->check(hid8_validator);
|
||||
auto dry_flag = app->add_flag("--dry-run,-n", "fetch and validate only; do not store the CRL");
|
||||
|
||||
app->callback([ctx, hid8_opt, dry_flag]() {
|
||||
ctx->action = [ctx, hid8_opt, dry_flag]() { fetch_crl(*ctx, hid8_opt, dry_flag); };
|
||||
});
|
||||
|
||||
app->fallthrough();
|
||||
return app;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::shared_ptr<CLI::App> build_dc_command(const MainConfig& cfg)
|
||||
{
|
||||
auto ctx = std::make_shared<Context>(cfg);
|
||||
auto app = std::make_shared<CLI::App>("PKI Distribution Centre", "dc");
|
||||
app->add_flag("--print", ctx->print, "print received data in addition");
|
||||
app->add_option("--url", ctx->url_override, "override the Distribution Centre URL");
|
||||
|
||||
app->add_subcommand(build_info_command(ctx));
|
||||
app->add_subcommand(build_ctl_command(ctx));
|
||||
app->add_subcommand(build_crl_command(ctx));
|
||||
|
||||
app->require_subcommand(0, 1);
|
||||
app->final_callback([ctx]() {
|
||||
if (!ctx->action) {
|
||||
ctx->action = [ctx]() { show_info(*ctx); };
|
||||
}
|
||||
ctx->action();
|
||||
});
|
||||
return app;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "main.hpp"
|
||||
#include <CLI/App.hpp>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
std::shared_ptr<CLI::App> build_dc_command(const MainConfig&);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,60 @@
|
||||
#include "distribution_centre.hpp"
|
||||
#include "http.hpp"
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
DistributionCentre::DistributionCentre()
|
||||
{
|
||||
}
|
||||
|
||||
void DistributionCentre::set_url(const std::string& url)
|
||||
{
|
||||
m_base_query = HttpQuery::from_url(url);
|
||||
}
|
||||
|
||||
boost::optional<CertificateTrustList> DistributionCentre::fetch_trust_list(const HashedId8& digest)
|
||||
{
|
||||
HttpQuery query = m_base_query;
|
||||
query.path += "/getctl/" + hexstring(digest);
|
||||
|
||||
HttpResponse response = http_get(query);
|
||||
if (response.result() != boost::beast::http::status::ok) {
|
||||
return boost::none;
|
||||
} else if (response[boost::beast::http::field::content_type] != "application/x-its-ctl") {
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
CertificateTrustList trust_list;
|
||||
if (trust_list.decode(response.body())) {
|
||||
return trust_list;
|
||||
} else {
|
||||
return boost::none;
|
||||
}
|
||||
}
|
||||
|
||||
boost::optional<CertificateRevocationList> DistributionCentre::fetch_revocation_list(const HashedId8& digest)
|
||||
{
|
||||
// TS 102 941 v1.4.1 Annex D.2: GET <dc>/getcrl/<UPPERCASE-HEX-HashedId8>; reply application/x-its-crl.
|
||||
HttpQuery query = m_base_query;
|
||||
query.path += "/getcrl/" + hexstring(digest);
|
||||
|
||||
HttpResponse response = http_get(query);
|
||||
if (response.result() != boost::beast::http::status::ok) {
|
||||
return boost::none;
|
||||
} else if (response[boost::beast::http::field::content_type] != "application/x-its-crl") {
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
CertificateRevocationList crl;
|
||||
if (crl.decode(response.body())) {
|
||||
return crl;
|
||||
} else {
|
||||
return boost::none;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "certificate_revocation_list.hpp"
|
||||
#include "certificate_trust_list.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "http.hpp"
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class DistributionCentre
|
||||
{
|
||||
public:
|
||||
DistributionCentre();
|
||||
|
||||
void set_url(const std::string&);
|
||||
|
||||
// TS 102 941 v1.4.1 Annex D.1.
|
||||
boost::optional<CertificateTrustList> fetch_trust_list(const HashedId8&);
|
||||
|
||||
// TS 102 941 v1.4.1 Annex D.2.
|
||||
boost::optional<CertificateRevocationList> fetch_revocation_list(const HashedId8&);
|
||||
|
||||
private:
|
||||
HttpQuery m_base_query;
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,119 @@
|
||||
#include "ea_request.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "encrypted_data.hpp"
|
||||
#include "exception.hpp"
|
||||
#include "psid_ssp.hpp"
|
||||
#include "security_module.hpp"
|
||||
#include "signed_builder.hpp"
|
||||
#include "signed_data.hpp"
|
||||
#include <vanetza/asn1/security/InnerEcRequest.h>
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
class InnerEcRequest : public asn1::asn1c_oer_wrapper<Vanetza_Security_InnerEcRequest_t>
|
||||
{
|
||||
public:
|
||||
using wrapper = asn1::asn1c_oer_wrapper<Vanetza_Security_InnerEcRequest_t>;
|
||||
|
||||
InnerEcRequest() : wrapper(asn_DEF_Vanetza_Security_InnerEcRequest)
|
||||
{
|
||||
m_struct->certificateFormat = Vanetza_Security_CertificateFormat_ts103097v131;
|
||||
}
|
||||
|
||||
void set_its_id(const std::string& id)
|
||||
{
|
||||
if (OCTET_STRING_fromBuf(&m_struct->itsId, id.data(), id.size()) != 0) {
|
||||
throw std::runtime_error("setting ITS ID failed");
|
||||
}
|
||||
}
|
||||
|
||||
void set_verification_key(const PublicKey& pubkey)
|
||||
{
|
||||
pki::set_verification_key(m_struct->publicKeys.verificationKey, pubkey);
|
||||
}
|
||||
|
||||
void add_permission(const PsidSsp& perm)
|
||||
{
|
||||
if (m_struct->requestedSubjectAttributes.appPermissions == nullptr) {
|
||||
m_struct->requestedSubjectAttributes.appPermissions =
|
||||
asn1::allocate<Vanetza_Security_SequenceOfPsidSsp_t>();
|
||||
}
|
||||
|
||||
auto psid_ssp = asn1::allocate<Vanetza_Security_PsidSsp_t>();
|
||||
psid_ssp->psid = perm.psid;
|
||||
if (!perm.ssp.empty()) {
|
||||
psid_ssp->ssp = asn1::allocate<Vanetza_Security_ServiceSpecificPermissions_t>();
|
||||
psid_ssp->ssp->present = Vanetza_Security_ServiceSpecificPermissions_PR_bitmapSsp;
|
||||
copy(perm.ssp, psid_ssp->ssp->choice.bitmapSsp);
|
||||
}
|
||||
|
||||
auto* perms = m_struct->requestedSubjectAttributes.appPermissions;
|
||||
if (asn_sequence_add(perms, psid_ssp) != 0) {
|
||||
throw std::runtime_error("adding app permission to InnerEcRequest failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
ByteBuffer build_signed_enrolment_request(SecurityModule& security, const EnrolmentRequestParameters& params)
|
||||
{
|
||||
if (params.its_id.empty()) {
|
||||
throw std::invalid_argument("EnrolmentRequest: its_id must not be empty");
|
||||
}
|
||||
|
||||
// 1. InnerEcRequest
|
||||
InnerEcRequest inner;
|
||||
inner.set_its_id(params.its_id);
|
||||
inner.set_verification_key(params.verification_key);
|
||||
inner.add_permission(PsidSsp { aid::SCR, { 0x01, 0xC0 } });
|
||||
if (!inner.validate()) {
|
||||
throw std::runtime_error("EnrolmentRequest: constructed InnerEcRequest is invalid");
|
||||
}
|
||||
|
||||
// 2. InnerEcRequestSignedForPop wrapped in EtsiTs102941Data
|
||||
MgmtData pop_data;
|
||||
pop_data->version = Vanetza_Security_Version_v1;
|
||||
pop_data->content.present = Vanetza_Security_EtsiTs102941DataContent_PR_enrolmentRequest;
|
||||
// Inner PoP is always signer = self (TS 102 941 §6.2.3.2.1)
|
||||
create_signed(inner.encode(), security, params.verification_key, params.hash_algo, nullptr)
|
||||
.move_into(pop_data->content.choice.enrolmentRequest);
|
||||
|
||||
// 3. Outer EtsiTs103097Data-Signed: signer = self for initial enrolment or
|
||||
// signer = digest(outer_signer_certificate) for a re-keying renewal.
|
||||
SignedData outer = create_signed(pop_data.encode(), security, params.outer_signer_key, params.hash_algo,
|
||||
params.outer_signer_certificate);
|
||||
return outer.encode();
|
||||
}
|
||||
|
||||
EncryptedData build_enrolment_request(SecurityModule& security, const EnrolmentRequestParameters& params,
|
||||
const Certificate& ea_certificate)
|
||||
{
|
||||
boost::optional<PublicKey> ea_enckey = ea_certificate.get_encryption_key();
|
||||
if (!ea_enckey) {
|
||||
throw DecodingFailure("EnrolmentRequest: EA certificate has no encryption key");
|
||||
}
|
||||
|
||||
ByteBuffer signed_request = build_signed_enrolment_request(security, params);
|
||||
|
||||
Sha256Hash ea_cert_sha256 = calculate_sha256_hash(security, ea_certificate);
|
||||
auto ecies_unique = security.create_ecies_context(*ea_enckey, ea_cert_sha256);
|
||||
std::shared_ptr<SecurityModule::EciesContext> ecies { std::move(ecies_unique) };
|
||||
EncryptedData encrypted { ecies };
|
||||
encrypted.generate_ciphertext(signed_request);
|
||||
encrypted.add_recipient_info(ea_certificate.calculate_hashed_id8(security));
|
||||
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include "keys.hpp"
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <vanetza/security/hash_algorithm.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class Certificate;
|
||||
class EncryptedData;
|
||||
class SecurityModule;
|
||||
|
||||
using security::HashAlgorithm;
|
||||
|
||||
/**
|
||||
* \brief Parameters for an InnerEcRequest / outer EtsiTs103097Data-Signed enrolment message.
|
||||
* \see TS 102 941 §6.2.3.2
|
||||
*/
|
||||
struct EnrolmentRequestParameters
|
||||
{
|
||||
std::string its_id; // canonical id (initial) or current EC HashedId8 (re-enrolment)
|
||||
PublicKey verification_key; // to be certified; private key in SecurityModule for POP signature
|
||||
PublicKey outer_signer_key; // signs outer Data-Signed; canonical key (initial) or current EC verification key (re-enrolment)
|
||||
HashAlgorithm hash_algo = HashAlgorithm::SHA256; // POP and outer signatures
|
||||
const Certificate* outer_signer_certificate = nullptr; // null → signer=self; non-null → signer=digest of this cert
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Build the signed (but not yet encrypted) enrolment request payload.
|
||||
*
|
||||
* Layered signatures: outer EtsiTs103097Data-Signed (signed with outer_signer_key;
|
||||
* signer=self for initial enrolment, digest(outer_signer_certificate) for re-keying)
|
||||
* wraps EtsiTs102941Data{enrolmentRequest=InnerEcRequestSignedForPop}, which is
|
||||
* the OER-encoded InnerEcRequest signed with verification_key (signer=self) as POP.
|
||||
* Exposed for testing; production code should call build_enrolment_request().
|
||||
*/
|
||||
ByteBuffer build_signed_enrolment_request(SecurityModule& security, const EnrolmentRequestParameters& parameters);
|
||||
|
||||
/**
|
||||
* \brief Build the EA-encrypted enrolment request.
|
||||
*
|
||||
* Call .encode() on the result for the OER bytes to POST. The ECIES context
|
||||
* held in the returned EncryptedData is also needed to decrypt the EA's
|
||||
* response (same symmetric key via pskRecipInfo).
|
||||
*/
|
||||
EncryptedData build_enrolment_request(SecurityModule& security, const EnrolmentRequestParameters& parameters,
|
||||
const Certificate& ea_certificate);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,74 @@
|
||||
#include "ea_response.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "exception.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "security_module.hpp"
|
||||
#include "signed_data.hpp"
|
||||
#include "validation.hpp"
|
||||
#include <vanetza/asn1/security/EtsiTs102941Data.h>
|
||||
#include <vanetza/asn1/security/InnerEcResponse.h>
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
EnrolmentResponse parse_enrolment_response(SecurityModule& security, const ByteBuffer& decrypted,
|
||||
const Certificate& ea_certificate)
|
||||
{
|
||||
SignedData outer;
|
||||
if (!outer.decode(decrypted)) {
|
||||
throw DecodingFailure("decoding signed enrolment response failed");
|
||||
}
|
||||
if (outer->content->present != Vanetza_Security_Ieee1609Dot2Content_PR_signedData) {
|
||||
throw DecodingFailure("enrolment response content is not signedData");
|
||||
}
|
||||
const Vanetza_Security_SignedData_t& sd = *outer->content->choice.signedData;
|
||||
|
||||
// Per TS 102 941 §6.2.3.2.2 the EA signs enrolment responses with signer = digest(EA cert).
|
||||
const HashedId8 ea_hid8 = ea_certificate.calculate_hashed_id8(security);
|
||||
if (sd.signer.present != Vanetza_Security_SignerIdentifier_PR_digest) {
|
||||
throw DecodingFailure("enrolment response must have signer = digest");
|
||||
}
|
||||
if (sd.signer.choice.digest != ea_hid8.octets) {
|
||||
throw VerificationFailure("enrolment response signer digest does not match EA certificate");
|
||||
}
|
||||
|
||||
if (sd.tbsData->headerInfo.psid != aid::SCR) {
|
||||
throw DecodingFailure("enrolment response PSID is not SCR");
|
||||
}
|
||||
|
||||
if (!validate(security, sd, ea_certificate.raw())) {
|
||||
throw VerificationFailure("enrolment response signature does not verify against EA certificate");
|
||||
}
|
||||
|
||||
const Vanetza_Security_Opaque_t* inner_opaque = get_signed_payload(outer->content);
|
||||
if (!inner_opaque) {
|
||||
throw DecodingFailure("enrolment response carries no unsecured signed payload");
|
||||
}
|
||||
|
||||
EnrolmentResponseData inner = EnrolmentResponseData::from_opaque(*inner_opaque);
|
||||
if (inner->version != Vanetza_Security_Version_v1) {
|
||||
throw DecodingFailure("inner EtsiTs102941Data version is not v1");
|
||||
}
|
||||
|
||||
const Vanetza_Security_InnerEcResponse_t& ec_resp = inner->content.choice.enrolmentResponse;
|
||||
|
||||
EnrolmentResponse result;
|
||||
result.code = EnrolmentResponseCode { ec_resp.responseCode };
|
||||
result.request_hash.assign(ec_resp.requestHash.buf, ec_resp.requestHash.buf + ec_resp.requestHash.size);
|
||||
|
||||
if (ec_resp.certificate) {
|
||||
result.certificate = Certificate(*ec_resp.certificate);
|
||||
}
|
||||
|
||||
if (ec_resp.responseCode == Vanetza_Security_EnrolmentResponseCode_ok && !result.certificate) {
|
||||
throw DecodingFailure("enrolment response code is ok but no certificate is included");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "certificate.hpp"
|
||||
#include "response_codes.hpp"
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <boost/optional/optional.hpp>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class SecurityModule;
|
||||
|
||||
/**
|
||||
* \brief Decoded view of an InnerEcResponse payload.
|
||||
* \see TS 102 941 §6.2.3.2.2
|
||||
*/
|
||||
struct EnrolmentResponse
|
||||
{
|
||||
EnrolmentResponseCode code; // EA's responseCode; parsing succeeds for any value
|
||||
ByteBuffer request_hash; // SHA-256 prefix of the matching request
|
||||
boost::optional<Certificate> certificate; // new EC; present iff one was decoded — caller must still check `code == ok`
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a decrypted EnrolmentResponse per ETSI TS 102 941 §6.2.3.2.2.
|
||||
*
|
||||
* Verifies:
|
||||
* - outer Ieee1609Dot2Data is signedData
|
||||
* - signer = digest, matching HashedId8(ea_certificate)
|
||||
* - tbsData.headerInfo.psid == aid::SCR
|
||||
* - outer signature verifies against ea_certificate
|
||||
* - inner EtsiTs102941Data.version == v1
|
||||
* - inner content variant is enrolmentResponse
|
||||
*
|
||||
* For a non-ok responseCode the function returns normally and `code` holds it;
|
||||
* `certificate` is populated iff the response actually contains one.
|
||||
*
|
||||
* \throws DecodingFailure on structural failure
|
||||
* \throws VerificationFailure on signer-digest or signature mismatch
|
||||
*/
|
||||
EnrolmentResponse parse_enrolment_response(SecurityModule& security, const ByteBuffer& decrypted,
|
||||
const Certificate& ea_certificate);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "ecies.hpp"
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
ByteBuffer calculate_kdf2(SecurityModule& security, const ByteBuffer& shared_secret, const ByteBuffer& kdp,
|
||||
std::size_t dl)
|
||||
{
|
||||
Sha256Function fn = [&security](const ByteBuffer& buffer) {
|
||||
return security.calculate_sha256_hash(buffer.data(), buffer.size());
|
||||
};
|
||||
return calculate_kdf2(fn, shared_secret, kdp, dl);
|
||||
}
|
||||
|
||||
ByteBuffer calculate_kdf2(Sha256Function hash_fn, const ByteBuffer& shared_secret, const ByteBuffer& kdp,
|
||||
std::size_t dl)
|
||||
{
|
||||
ByteBuffer derived;
|
||||
ByteBuffer concat;
|
||||
|
||||
// each iteration adds 32 bytes (SHA-256 hash)
|
||||
const std::uint32_t iterations = dl / Sha256Hash::length + (dl % Sha256Hash::length ? 1 : 0);
|
||||
|
||||
// initialize counter with 1: see IEEE 1363.2a-2004 KDF2
|
||||
for (std::uint32_t counter = 1; counter <= iterations; ++counter) {
|
||||
// start concatenation with shared secret
|
||||
concat = shared_secret;
|
||||
// append 4-byte counter in big-endian order
|
||||
concat.insert(concat.end(), {
|
||||
static_cast<std::uint8_t>(counter >> 24),
|
||||
static_cast<std::uint8_t>(counter >> 16),
|
||||
static_cast<std::uint8_t>(counter >> 8),
|
||||
static_cast<std::uint8_t>(counter)
|
||||
});
|
||||
// finalize with key derivation parameter (P1)
|
||||
concat.insert(concat.end(), kdp.begin(), kdp.end());
|
||||
|
||||
Sha256Hash hash = hash_fn(concat);
|
||||
std::copy(hash.octets.begin(), hash.octets.end(), std::back_inserter(derived));
|
||||
}
|
||||
|
||||
// left most bytes are the derived key
|
||||
assert(derived.size() >= dl);
|
||||
derived.resize(dl);
|
||||
return derived;
|
||||
}
|
||||
|
||||
EncryptedSymmetricKey encrypt_key(SecurityModule& security, const SecurityModule::EciesContext& ecies,
|
||||
const ByteBuffer& key, const Sha256Hash& info)
|
||||
{
|
||||
const std::size_t ke_length = key.size();
|
||||
static constexpr std::size_t km_length = 32;
|
||||
|
||||
// derive ke and km from shared secret
|
||||
ByteBuffer kdp { info.octets.begin(), info.octets.end() };
|
||||
auto hashed_shared_secret = calculate_kdf2(security, ecies.shared_secret(), kdp, ke_length + km_length);
|
||||
assert(hashed_shared_secret.size() == ke_length + km_length);
|
||||
|
||||
EncryptedSymmetricKey result;
|
||||
result.public_key = ecies.ephemeral_public_key();
|
||||
|
||||
// encrypt symmetric key
|
||||
result.wrapped_key.resize(ke_length);
|
||||
std::uint8_t* ke = hashed_shared_secret.data();
|
||||
for (std::size_t i = 0; i < ke_length; ++i) {
|
||||
result.wrapped_key[i] = ke[i] ^ key[i];
|
||||
}
|
||||
|
||||
// generate authentication tag
|
||||
ByteBuffer km { std::next(hashed_shared_secret.begin(), ke_length), hashed_shared_secret.end() };
|
||||
assert(km.size() == km_length);
|
||||
result.authentication_tag = security.calculate_hmac_sha256(km, result.wrapped_key);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "security_module.hpp"
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <cstddef>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
using Sha256Function = std::function<Sha256Hash(const ByteBuffer&)>;
|
||||
|
||||
ByteBuffer calculate_kdf2(SecurityModule&, const ByteBuffer& shared_secret, const ByteBuffer& kdp, std::size_t dl);
|
||||
ByteBuffer calculate_kdf2(Sha256Function, const ByteBuffer& shared_secret, const ByteBuffer& kdp, std::size_t dl);
|
||||
|
||||
struct EncryptedSymmetricKey
|
||||
{
|
||||
PublicKey public_key;
|
||||
ByteBuffer authentication_tag;
|
||||
ByteBuffer wrapped_key;
|
||||
};
|
||||
|
||||
EncryptedSymmetricKey encrypt_key(SecurityModule&, const SecurityModule::EciesContext&, const ByteBuffer& key,
|
||||
const Sha256Hash& info);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,133 @@
|
||||
#include "encrypted_data.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "exception.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "security_module.hpp"
|
||||
#include "signed_builder.hpp"
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
const Vanetza_Security_AesCcmCiphertext_t*
|
||||
get_aes_ccm_ciphertext(const Vanetza_Security_EtsiTs103097Data_Encrypted_85P0_t& dest)
|
||||
{
|
||||
if (dest.content && dest.content->present == Vanetza_Security_Ieee1609Dot2Content_PR_encryptedData) {
|
||||
const auto& enc = dest.content->choice.encryptedData;
|
||||
if (enc.ciphertext.present == Vanetza_Security_SymmetricCiphertext_PR_aes128ccm) {
|
||||
return &enc.ciphertext.choice.aes128ccm;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void EncryptedData::init(asn1c_type& dest)
|
||||
{
|
||||
dest.protocolVersion = ieee1609dot2_protocol_version;
|
||||
dest.content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
dest.content->present = Vanetza_Security_Ieee1609Dot2Content_PR_encryptedData;
|
||||
}
|
||||
|
||||
void EncryptedData::set_aes_ccm_ciphertext(asn1c_type& dest, SecurityModule::EciesContext& ecies,
|
||||
const ByteBuffer& plaintext)
|
||||
{
|
||||
ByteBuffer ciphertext = ecies.encrypt(plaintext);
|
||||
auto& aes = dest.content->choice.encryptedData.ciphertext;
|
||||
aes.present = Vanetza_Security_SymmetricCiphertext_PR_aes128ccm;
|
||||
copy(ecies.nonce(), aes.choice.aes128ccm.nonce);
|
||||
copy(ciphertext, aes.choice.aes128ccm.ccmCiphertext);
|
||||
}
|
||||
|
||||
void EncryptedData::append_recipient_info(asn1c_type& dest, const SecurityModule::EciesContext& ecies,
|
||||
const HashedId8& cert)
|
||||
{
|
||||
auto recipient = asn1::allocate<Vanetza_Security_RecipientInfo_t>();
|
||||
asn_sequence_add(&dest.content->choice.encryptedData.recipients.list, recipient);
|
||||
recipient->present = Vanetza_Security_RecipientInfo_PR_certRecipInfo;
|
||||
OCTET_STRING_fromBuf(&recipient->choice.certRecipInfo.recipientId,
|
||||
reinterpret_cast<const char*>(cert.octets.data()), cert.octets.size());
|
||||
|
||||
Vanetza_Security_EncryptedDataEncryptionKey_t& enckey = recipient->choice.certRecipInfo.encKey;
|
||||
switch (ecies.ephemeral_public_key().type) {
|
||||
case KeyType::NistP256:
|
||||
enckey.present = Vanetza_Security_EncryptedDataEncryptionKey_PR_eciesNistP256;
|
||||
fill_curve_point(ecies.ephemeral_public_key(), enckey.choice.eciesNistP256.v);
|
||||
copy(ecies.authentication_tag(), enckey.choice.eciesNistP256.t);
|
||||
copy(ecies.encrypted_key(), enckey.choice.eciesNistP256.c);
|
||||
break;
|
||||
case KeyType::BrainpoolP256r1:
|
||||
enckey.present = Vanetza_Security_EncryptedDataEncryptionKey_PR_eciesBrainpoolP256r1;
|
||||
fill_curve_point(ecies.ephemeral_public_key(), enckey.choice.eciesBrainpoolP256r1.v);
|
||||
copy(ecies.authentication_tag(), enckey.choice.eciesBrainpoolP256r1.t);
|
||||
copy(ecies.encrypted_key(), enckey.choice.eciesBrainpoolP256r1.c);
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("unsupported encryption key type");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
EncryptedData::EncryptedData(std::shared_ptr<SecurityModule::EciesContext> ecies) :
|
||||
wrapper(asn_DEF_Vanetza_Security_EtsiTs103097Data_Encrypted_85P0), m_ecies(ecies)
|
||||
{
|
||||
init(*m_struct);
|
||||
}
|
||||
|
||||
void EncryptedData::generate_ciphertext(const ByteBuffer& plaintext)
|
||||
{
|
||||
set_aes_ccm_ciphertext(*m_struct, *m_ecies, plaintext);
|
||||
}
|
||||
|
||||
void EncryptedData::add_recipient_info(const HashedId8& cert)
|
||||
{
|
||||
append_recipient_info(*m_struct, *m_ecies, cert);
|
||||
}
|
||||
|
||||
const OCTET_STRING_t* EncryptedData::get_nonce() const
|
||||
{
|
||||
auto aes_ccm = get_aes_ccm_ciphertext(*m_struct);
|
||||
return aes_ccm ? &aes_ccm->nonce : nullptr;
|
||||
}
|
||||
|
||||
const OCTET_STRING_t* EncryptedData::get_ciphertext() const
|
||||
{
|
||||
auto aes_ccm = get_aes_ccm_ciphertext(*m_struct);
|
||||
return aes_ccm ? &aes_ccm->ccmCiphertext : nullptr;
|
||||
}
|
||||
|
||||
bool EncryptedData::has_psk_recipient() const
|
||||
{
|
||||
const auto& recipients = m_struct->content->choice.encryptedData.recipients;
|
||||
for (int i = 0; i < recipients.list.count; ++i) {
|
||||
const Vanetza_Security_RecipientInfo_t* recipient = recipients.list.array[i];
|
||||
if (recipient && recipient->present == Vanetza_Security_RecipientInfo_PR_pskRecipInfo) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ByteBuffer EncryptedData::decrypt()
|
||||
{
|
||||
auto aes_ccm = get_aes_ccm_ciphertext(*m_struct);
|
||||
if (!aes_ccm) {
|
||||
throw DecodingFailure("missing AES CCM ciphertext");
|
||||
}
|
||||
// A PKI response reuses the request's symmetric key, referenced via pskRecipInfo.
|
||||
if (!has_psk_recipient()) {
|
||||
throw DecodingFailure("encrypted PKI response lacks expected pskRecipInfo recipient");
|
||||
}
|
||||
|
||||
m_ecies->nonce(to_buffer(aes_ccm->nonce));
|
||||
return m_ecies->decrypt(aes_ccm->ccmCiphertext.buf, aes_ccm->ccmCiphertext.size);
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include "security_module.hpp"
|
||||
#include <vanetza/asn1/asn1c_wrapper.hpp>
|
||||
#include <vanetza/asn1/security/EtsiTs103097Data-Encrypted.h>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
// forward declarations
|
||||
struct HashedId8;
|
||||
class SecurityModule;
|
||||
|
||||
class EncryptedData : public asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs103097Data_Encrypted_85P0_t>
|
||||
{
|
||||
public:
|
||||
using wrapper = asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs103097Data_Encrypted_85P0_t>;
|
||||
|
||||
EncryptedData(std::shared_ptr<SecurityModule::EciesContext> ecies);
|
||||
void generate_ciphertext(const ByteBuffer& payload);
|
||||
void add_recipient_info(const HashedId8&);
|
||||
const OCTET_STRING_t* get_nonce() const;
|
||||
const OCTET_STRING_t* get_ciphertext() const;
|
||||
|
||||
/**
|
||||
* \brief Decrypt a PKI response that reuses the request's symmetric AES key.
|
||||
*
|
||||
* Call on the same EncryptedData instance that built the request, after
|
||||
* decode()-ing the response body into it. The AA/EA encrypts its response
|
||||
* with the request's AES key referenced via pskRecipInfo
|
||||
* (TS 102 941 §6.2.3.3.2 for AT, §6.2.3.2.2 for EC).
|
||||
* \throws DecodingFailure if the ciphertext or pskRecipInfo is missing
|
||||
*/
|
||||
ByteBuffer decrypt();
|
||||
|
||||
// Lower-level builders that mutate an EtsiTs103097Data-Encrypted value in-place
|
||||
static void init(asn1c_type& dest);
|
||||
static void set_aes_ccm_ciphertext(asn1c_type& dest, SecurityModule::EciesContext& ecies,
|
||||
const ByteBuffer& plaintext);
|
||||
static void append_recipient_info(asn1c_type& dest, const SecurityModule::EciesContext& ecies,
|
||||
const HashedId8& recipient);
|
||||
|
||||
private:
|
||||
bool has_psk_recipient() const;
|
||||
std::shared_ptr<SecurityModule::EciesContext> m_ecies;
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,386 @@
|
||||
#include "enrolment.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "certificate_trust_list.hpp"
|
||||
#include "ea_request.hpp"
|
||||
#include "ea_response.hpp"
|
||||
#include "encrypted_data.hpp"
|
||||
#include "exception.hpp"
|
||||
#include "filesystem.hpp"
|
||||
#include "hex_string_validator.hpp"
|
||||
#include "hexstring.hpp"
|
||||
#include "http.hpp"
|
||||
#include "pem.hpp"
|
||||
#include "response_codes.hpp"
|
||||
#include "time.hpp"
|
||||
#include "validation.hpp"
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
#include <CLI/ExtraValidators.hpp>
|
||||
#include <boost/date_time/posix_time/posix_time_io.hpp>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct Context
|
||||
{
|
||||
Context(const MainConfig& c) : cfg(c)
|
||||
{
|
||||
}
|
||||
|
||||
std::string url(const EnrolmentAuthority& ea) const
|
||||
{
|
||||
// Some PKIs have EA entry which does not have ITS access point
|
||||
const std::string& base = ea.its_access_point.empty() ? ea.aa_access_point : ea.its_access_point;
|
||||
return resolve_url(base, url_override);
|
||||
}
|
||||
|
||||
const MainConfig& cfg;
|
||||
std::string url_override;
|
||||
std::function<void()> action;
|
||||
bool forced = false;
|
||||
KeyType key_type = KeyType::BrainpoolP256r1;
|
||||
HashAlgorithm hash_algo = HashAlgorithm::SHA256;
|
||||
|
||||
// Manual enrolment: EA certificate and endpoint supplied directly instead of CTL lookup
|
||||
std::string ea_cert_file;
|
||||
std::string ea_url;
|
||||
|
||||
// Check if manual enolrment mode is engaged
|
||||
bool manual_enrolment() const { return !ea_cert_file.empty() || !ea_url.empty(); }
|
||||
|
||||
struct Bootstrap
|
||||
{
|
||||
PrivateKey private_key;
|
||||
PublicKey public_key;
|
||||
std::string identifier; // canonical identifier as stored/displayed (text or hex string)
|
||||
bool identifier_hex = false; // interpret identifier as hex, decoded to bytes for itsId
|
||||
|
||||
// itsId bytes for the enrolment request: raw text, or hex decoded to bytes
|
||||
std::string its_id() const
|
||||
{
|
||||
return identifier_hex ? parse_hexstring(identifier) : identifier;
|
||||
}
|
||||
};
|
||||
Bootstrap bootstrap;
|
||||
};
|
||||
|
||||
std::map<std::string, KeyType> key_type_map = { { "NistP256", KeyType::NistP256 },
|
||||
{ "BrainpoolP256r1", KeyType::BrainpoolP256r1 }, { "BrainpoolP384r1", KeyType::BrainpoolP384r1 } };
|
||||
|
||||
std::map<std::string, HashAlgorithm> hash_algo_map = { { "SHA256", HashAlgorithm::SHA256 },
|
||||
{ "SHA384", HashAlgorithm::SHA384 } };
|
||||
|
||||
void check_enrolment_status(Context&);
|
||||
void perform_initial_enrolment(Context&);
|
||||
void perform_enrolment_renewal(Context&);
|
||||
|
||||
} // namespace
|
||||
|
||||
std::shared_ptr<CLI::App> build_enrolment_command(const MainConfig& cfg)
|
||||
{
|
||||
auto ctx = std::make_shared<Context>(cfg);
|
||||
auto app = std::make_shared<CLI::App>("enrolment of ITS station at PKI", "enrolment");
|
||||
app->alias("enrol");
|
||||
app->add_option("--url", ctx->url_override, "override the Enrolment Authority URL");
|
||||
|
||||
CLI::callback_t canonical_keyfile_cb = [ctx](const CLI::results_t& keyfiles) -> bool {
|
||||
if (keyfiles.size() != 1) {
|
||||
return false;
|
||||
}
|
||||
auto key = parse_pem_private_key(read(keyfiles[0]));
|
||||
if (key) {
|
||||
ctx->bootstrap.private_key = *key;
|
||||
ctx->bootstrap.public_key = derive_public_key(*key);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Manual enrolment options, shared by initial and renewal
|
||||
auto add_manual_options = [ctx](CLI::App* sub) {
|
||||
auto ea_cert = sub->add_option("--ea-certificate", ctx->ea_cert_file,
|
||||
"manual enrolment: EA certificate (OER) to encrypt the request to")
|
||||
->check(CLI::ExistingFile);
|
||||
auto ea_url = sub->add_option("--ea-url", ctx->ea_url, "manual enrolment: EA enrolment endpoint URL");
|
||||
// Manual enrolment needs both; let CLI11 reject a lone --ea-certificate / --ea-url.
|
||||
ea_cert->needs(ea_url);
|
||||
ea_url->needs(ea_cert);
|
||||
};
|
||||
|
||||
auto initial = app->add_subcommand("initial", "enrol with initial credentials (bootstrap)");
|
||||
initial->alias("init");
|
||||
initial->fallthrough();
|
||||
initial->add_flag("--force", ctx->forced, "enforce initial enrolment");
|
||||
// Exactly one of --canonical-id (text) or --canonical-id-hex (hex, decoded to bytes for itsId).
|
||||
auto canonical_id_group = initial->add_option_group("canonical-id");
|
||||
canonical_id_group->fallthrough(false); // required because of CLI11 2.6.2 bug
|
||||
canonical_id_group->add_option("--canonical-id", ctx->bootstrap.identifier, "canonical identifier (text)");
|
||||
canonical_id_group->add_option("--canonical-id-hex", ctx->bootstrap.identifier,
|
||||
"canonical identifier (hex, decoded to bytes for itsId)")
|
||||
->check(HexStringValidator())
|
||||
->each([ctx](const std::string&) { ctx->bootstrap.identifier_hex = true; });
|
||||
canonical_id_group->require_option(1);
|
||||
initial->add_option("--canonical-keyfile", canonical_keyfile_cb, "canonical key (PEM encoded file)")
|
||||
->required()
|
||||
->check(CLI::ExistingFile);
|
||||
initial->add_option("--key-type", ctx->key_type, "type of generated verification key")
|
||||
->default_val(KeyType::BrainpoolP256r1)
|
||||
->capture_default_str()
|
||||
->transform(CLI::CheckedTransformer(key_type_map, CLI::ignore_case));
|
||||
initial->add_option("--hash-algorithm", ctx->hash_algo, "hash algorithm for signing")
|
||||
->default_val(HashAlgorithm::SHA256)
|
||||
->capture_default_str()
|
||||
->transform(CLI::CheckedTransformer(hash_algo_map, CLI::ignore_case));
|
||||
add_manual_options(initial);
|
||||
|
||||
initial->callback([ctx]() {
|
||||
ctx->action = [ctx]() {
|
||||
// will initial enrolment overwrite an existing EC identifier?
|
||||
auto ec_id = ctx->cfg.station->get_ec_identifier();
|
||||
if (!ec_id || ctx->forced) {
|
||||
perform_initial_enrolment(*ctx);
|
||||
} else {
|
||||
throw UsageError("EC identifier is already set", "pass --force to overwrite");
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
auto renewal = app->add_subcommand("renewal", "renew enrolment credential (re-keying)");
|
||||
renewal->alias("renew");
|
||||
renewal->fallthrough();
|
||||
renewal->add_option("--key-type", ctx->key_type, "type of generated verification key")
|
||||
->default_val(KeyType::BrainpoolP256r1)
|
||||
->capture_default_str()
|
||||
->transform(CLI::CheckedTransformer(key_type_map, CLI::ignore_case));
|
||||
renewal->add_option("--hash-algorithm", ctx->hash_algo, "hash algorithm for signing")
|
||||
->default_val(HashAlgorithm::SHA256)
|
||||
->capture_default_str()
|
||||
->transform(CLI::CheckedTransformer(hash_algo_map, CLI::ignore_case));
|
||||
add_manual_options(renewal);
|
||||
renewal->callback([ctx]() { ctx->action = [ctx]() { perform_enrolment_renewal(*ctx); }; });
|
||||
|
||||
auto status = app->add_subcommand("status", "show enrolment status");
|
||||
status->callback([ctx]() { ctx->action = [ctx]() { check_enrolment_status(*ctx); }; });
|
||||
|
||||
app->final_callback([ctx]() {
|
||||
if (ctx->action) {
|
||||
ctx->action();
|
||||
} else {
|
||||
check_enrolment_status(*ctx);
|
||||
}
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
void check_enrolment_status(Context& ctx)
|
||||
{
|
||||
// has EC identifier set?
|
||||
boost::optional<HashedId8> ec_id = ctx.cfg.station->get_ec_identifier();
|
||||
|
||||
// has EC certificate stored?
|
||||
boost::optional<Certificate> ec_cert;
|
||||
if (ec_id) {
|
||||
ec_cert = ctx.cfg.enrolment_credentials->fetch(*ec_id);
|
||||
}
|
||||
|
||||
// can station sign with EC certificate (i.e., is the private key available)?
|
||||
bool can_sign = false;
|
||||
if (ec_cert) {
|
||||
can_sign = ctx.cfg.security->can_sign(ec_cert->get_public_key());
|
||||
}
|
||||
|
||||
auto canonical_identifier = ctx.cfg.station->get_canonical_identifier();
|
||||
std::cout << "Canonical identifier: " << (canonical_identifier.empty() ? "[unknown]" : canonical_identifier)
|
||||
<< "\n";
|
||||
std::cout << "EC identifier: " << (ec_id ? hexstring(*ec_id) : "[unknown]") << "\n";
|
||||
std::cout << "EC certificate: " << (ec_cert ? (ec_cert->get_name() + " [available]") : "[unavailable]") << "\n";
|
||||
if (ec_cert) {
|
||||
Clock::time_point start = ec_cert->valid_since();
|
||||
Clock::time_point stop = ec_cert->valid_until();
|
||||
std::cout << "EC certificate validity: " << Clock::at(start) << " until " << Clock::at(stop);
|
||||
Clock::time_point now = current_time();
|
||||
if (now >= start && now <= stop) {
|
||||
std::cout << " [valid now]\n";
|
||||
if (now + std::chrono::hours(24 * 7 * 12) > stop) {
|
||||
std::cout << "!! EC certificate expires in less than 12 weeks, you should renew it !!\n";
|
||||
}
|
||||
} else if (now < start) {
|
||||
std::cout << " [not yet valid]\n";
|
||||
} else {
|
||||
std::cout << " [expired]\n";
|
||||
}
|
||||
}
|
||||
std::cout << "EC suitable for signing: " << (can_sign ? "yes" : "no") << "\n";
|
||||
}
|
||||
|
||||
EnrolmentAuthority lookup_enrolment_authority(Context& ctx)
|
||||
{
|
||||
std::cout << "Root CA " << hexstring(ctx.cfg.root_ca_hid8) << "\n";
|
||||
auto processor = process_stored_ctl(*ctx.cfg.trust_lists, ctx.cfg.security, ctx.cfg.root_ca_hid8);
|
||||
return require_enrolment_authority(processor, ctx.cfg.root_ca_hid8);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Build the EnrolmentAuthority from the manually supplied --ea-certificate / --ea-url.
|
||||
*
|
||||
* Lets a station enrol against a PKI whose DC offers no getctl, where trust comes from an
|
||||
* out-of-band bootstrap bundle rather than a stored CTL. The certificate must carry an encryption
|
||||
* key because the request is ECIES-encrypted to it.
|
||||
*/
|
||||
EnrolmentAuthority build_enrolment_authority(Context& ctx)
|
||||
{
|
||||
Certificate cert;
|
||||
if (!cert.decode(read(ctx.ea_cert_file))) {
|
||||
throw DecodingFailure("could not decode EA certificate");
|
||||
}
|
||||
if (!cert.get_encryption_key()) {
|
||||
throw UsageError("missing encryption key in EA certificate");
|
||||
}
|
||||
EnrolmentAuthority ea;
|
||||
ea.certificate = std::move(cert);
|
||||
ea.its_access_point = ctx.ea_url;
|
||||
std::cout << "Manual EA " << hexstring(ea.certificate.calculate_hashed_id8(*ctx.cfg.security)) << " at "
|
||||
<< ctx.ea_url << "\n";
|
||||
return ea;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Shared request/response round-trip for initial enrolment and renewal.
|
||||
*
|
||||
* Builds the EA-encrypted request, POSTs it, decrypts/parses the response,
|
||||
* stores the returned EC, sets the station's active EC identifier, and commits
|
||||
* the new verification key pair.
|
||||
* \throws on any failure along the way
|
||||
*/
|
||||
void perform_enrolment(Context& ctx, const EnrolmentRequestParameters& params, const EnrolmentAuthority& ea,
|
||||
ScopedKeyPair& scoped_verification_key)
|
||||
{
|
||||
HashedId8 ea_cert_hid8 = ea.certificate.calculate_hashed_id8(*ctx.cfg.security);
|
||||
std::cout << "Enroling against EA certificate: " << hexstring(ea_cert_hid8) << "\n";
|
||||
|
||||
EncryptedData encrypted_data = build_enrolment_request(*ctx.cfg.security, params, ea.certificate);
|
||||
|
||||
const std::string ea_url = ctx.url(ea);
|
||||
std::cout << "Enroling at EA URL: " << ea_url << "\n";
|
||||
auto query = HttpQuery::from_url(ea_url);
|
||||
auto encoded_encrypted_data = encrypted_data.encode();
|
||||
auto sha256_encrypted_data =
|
||||
ctx.cfg.security->calculate_sha256_hash(encoded_encrypted_data.data(), encoded_encrypted_data.size());
|
||||
|
||||
auto response = http_post(query, "application/x-its-request", encoded_encrypted_data);
|
||||
if (response.result() != boost::beast::http::status::ok) {
|
||||
throw HttpException("EA returned an unexpected HTTP status for the enrolment request",
|
||||
std::move(response));
|
||||
} else if (response[boost::beast::http::field::content_type] != "application/x-its-response") {
|
||||
throw HttpException("expected application/x-its-response");
|
||||
}
|
||||
|
||||
if (!encrypted_data.decode(response.body().data(), response.body().size())) {
|
||||
throw DecodingFailure("decoding encrypted response failed");
|
||||
}
|
||||
ByteBuffer dec_resp = encrypted_data.decrypt();
|
||||
|
||||
EnrolmentResponse ec_resp = parse_enrolment_response(*ctx.cfg.security, dec_resp, ea.certificate);
|
||||
|
||||
if (!check_request_hash(sha256_encrypted_data, ec_resp.request_hash)) {
|
||||
throw VerificationFailure("mismatch between request and response hash");
|
||||
}
|
||||
|
||||
if (ec_resp.code != Vanetza_Security_EnrolmentResponseCode_ok) {
|
||||
throw std::runtime_error("inner EC response code: " + vanetza::pki::to_string(ec_resp.code));
|
||||
}
|
||||
|
||||
Certificate& ec = *ec_resp.certificate;
|
||||
auto ec_hid8 = ec.calculate_hashed_id8(*ctx.cfg.security);
|
||||
scoped_verification_key.commit();
|
||||
ctx.cfg.enrolment_credentials->store(ec);
|
||||
ctx.cfg.station->set_ec_identifier(ec_hid8);
|
||||
std::cout << "Stored new EC with HashedId8 = " << hexstring(ec_hid8) << "\n";
|
||||
|
||||
// safety check
|
||||
if (ctx.cfg.security->can_sign(ec.get_public_key())) {
|
||||
std::cout << "Station can sign with received EC\n";
|
||||
} else {
|
||||
std::cerr << "Station cannot sign with received EC\n";
|
||||
}
|
||||
}
|
||||
|
||||
void perform_initial_enrolment(Context& ctx)
|
||||
{
|
||||
EnrolmentAuthority ea = ctx.manual_enrolment() ? build_enrolment_authority(ctx) : lookup_enrolment_authority(ctx);
|
||||
|
||||
// security module stores created private key in its credential storage
|
||||
ScopedKeyPair scoped_verification_key(*ctx.cfg.security, ctx.key_type);
|
||||
|
||||
// set permanent canonical identifier
|
||||
ctx.cfg.station->set_canonical_identifier(ctx.bootstrap.identifier);
|
||||
std::cout << "Canonical identifier: " << ctx.bootstrap.identifier << "\n";
|
||||
|
||||
// load bootstrap keys (only for this scope)
|
||||
ScopedCredential scoped_bootstrap_keys { *ctx.cfg.credentials, ctx.bootstrap.public_key,
|
||||
ctx.bootstrap.private_key };
|
||||
|
||||
std::cout << "Canonical public key X=" << hexstring(ctx.bootstrap.public_key.x);
|
||||
if (!ctx.bootstrap.public_key.y.empty()) {
|
||||
std::cout << " Y=" << hexstring(ctx.bootstrap.public_key.y);
|
||||
}
|
||||
std::cout << "\n";
|
||||
|
||||
EnrolmentRequestParameters params;
|
||||
params.its_id = ctx.bootstrap.its_id();
|
||||
params.verification_key = scoped_verification_key.public_key();
|
||||
params.outer_signer_key = ctx.bootstrap.public_key;
|
||||
params.hash_algo = ctx.hash_algo;
|
||||
|
||||
perform_enrolment(ctx, params, ea, scoped_verification_key);
|
||||
}
|
||||
|
||||
void perform_enrolment_renewal(Context& ctx)
|
||||
{
|
||||
auto ec_id = ctx.cfg.station->get_ec_identifier();
|
||||
if (!ec_id) {
|
||||
throw UsageError("no EC to renew", "run 'enrolment initial' first");
|
||||
}
|
||||
auto ec_cert = ctx.cfg.enrolment_credentials->fetch(*ec_id);
|
||||
if (!ec_cert) {
|
||||
throw UsageError("EC certificate missing from storage", "re-run 'enrolment initial'");
|
||||
}
|
||||
PublicKey ec_key = ec_cert->get_public_key();
|
||||
if (!ctx.cfg.security->can_sign(ec_key)) {
|
||||
throw UsageError("no EC private key available", "re-run 'enrolment initial'");
|
||||
}
|
||||
|
||||
EnrolmentAuthority ea = ctx.manual_enrolment() ? build_enrolment_authority(ctx) : lookup_enrolment_authority(ctx);
|
||||
|
||||
// Fresh verification key for the new EC
|
||||
// TS 102 941 §6.2.3.2.1 mandates a new key pair on every enrolment request).
|
||||
ScopedKeyPair scoped_verification_key(*ctx.cfg.security, ctx.key_type);
|
||||
|
||||
std::cout << "Renewing EC " << hexstring(*ec_id) << "\n";
|
||||
|
||||
EnrolmentRequestParameters params;
|
||||
// TS 102 941 §6.2.3.2.1 itsId carries the raw 8 bytes of HashedId8(current EC).
|
||||
params.its_id.assign(ec_id->octets.begin(), ec_id->octets.end());
|
||||
params.verification_key = scoped_verification_key.public_key();
|
||||
params.outer_signer_key = ec_key;
|
||||
params.outer_signer_certificate = &ec_cert.value();
|
||||
params.hash_algo = ctx.hash_algo;
|
||||
|
||||
perform_enrolment(ctx, params, ea, scoped_verification_key);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "main.hpp"
|
||||
#include <CLI/App.hpp>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
std::shared_ptr<CLI::App> build_enrolment_command(const MainConfig&);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class DecodingFailure : public std::runtime_error
|
||||
{
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
class VerificationFailure : public std::runtime_error
|
||||
{
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
class UsageError : public std::runtime_error
|
||||
{
|
||||
public:
|
||||
UsageError(const std::string& problem) : std::runtime_error(problem) {}
|
||||
UsageError(const std::string& problem, const std::string& remedy) :
|
||||
std::runtime_error(problem), m_remedy(remedy)
|
||||
{
|
||||
}
|
||||
|
||||
const std::string& remedy() const { return m_remedy; }
|
||||
|
||||
private:
|
||||
std::string m_remedy;
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,62 @@
|
||||
#include "filesystem.hpp"
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <system_error>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
void write_file(const std::filesystem::path& path, const ByteBuffer& buffer)
|
||||
{
|
||||
std::ofstream ofs(path, std::ios::binary | std::ios::trunc);
|
||||
if (!ofs) {
|
||||
throw std::runtime_error("could not open " + path.string() + " for writing");
|
||||
}
|
||||
ofs.write(reinterpret_cast<const char*>(buffer.data()), buffer.size());
|
||||
ofs.flush();
|
||||
if (!ofs) {
|
||||
throw std::runtime_error("could not write " + path.string());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void write(const std::filesystem::path& path, const ByteBuffer& buffer)
|
||||
{
|
||||
// Write to temporary file first and atomically rename it later
|
||||
std::filesystem::path tmp = path;
|
||||
tmp += ".tmp";
|
||||
|
||||
try {
|
||||
write_file(tmp, buffer);
|
||||
std::filesystem::rename(tmp, path);
|
||||
} catch (...) {
|
||||
// clean up temporary file
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(tmp, ec);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuffer read(const std::filesystem::path& path)
|
||||
{
|
||||
std::ifstream ifs(path, std::ios::binary);
|
||||
if (!ifs) {
|
||||
throw std::runtime_error("could not open " + path.string() + " for reading");
|
||||
}
|
||||
|
||||
ByteBuffer buffer { std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>() };
|
||||
if (ifs.bad()) {
|
||||
throw std::runtime_error("I/O error while reading " + path.string());
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <filesystem>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
void write(const std::filesystem::path& path, const ByteBuffer&);
|
||||
ByteBuffer read(const std::filesystem::path&);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,93 @@
|
||||
#include "hashed_id8.hpp"
|
||||
#include "hexstring.hpp"
|
||||
#include <vanetza/asn1/security/HashedId8.h>
|
||||
#include <boost/algorithm/hex.hpp>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
HashedId8::HashedId8(const Sha256Hash& hash)
|
||||
{
|
||||
std::copy(hash.octets.end() - 8, hash.octets.end(), octets.data());
|
||||
}
|
||||
|
||||
HashedId8::HashedId8(const Sha384Hash& hash)
|
||||
{
|
||||
std::copy(hash.octets.end() - 8, hash.octets.end(), octets.data());
|
||||
}
|
||||
|
||||
boost::optional<HashedId8> HashedId8::from_hexstring(const std::string& hex)
|
||||
{
|
||||
if (hex.size() == 16) {
|
||||
try {
|
||||
HashedId8 result;
|
||||
boost::algorithm::unhex(hex, result.octets.data());
|
||||
return result;
|
||||
} catch (boost::algorithm::hex_decode_error&) {
|
||||
}
|
||||
}
|
||||
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
boost::optional<HashedId8> HashedId8::from_buffer(const OCTET_STRING_t& os)
|
||||
{
|
||||
HashedId8 result;
|
||||
if (os.buf != nullptr && os.size == result.octets.size()) {
|
||||
std::copy_n(os.buf, result.octets.size(), result.octets.data());
|
||||
return result;
|
||||
}
|
||||
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
std::string hexstring(const HashedId8& id)
|
||||
{
|
||||
return hexstring(id.octets.data(), id.octets.size());
|
||||
}
|
||||
|
||||
bool valid_hashed_id8(const std::string& hex)
|
||||
{
|
||||
auto all_hex = [](const std::string& input) {
|
||||
return std::all_of(input.begin(), input.end(), [](unsigned char c) { return std::isxdigit(c); });
|
||||
};
|
||||
|
||||
return (hex.size() == 16 && all_hex(hex));
|
||||
}
|
||||
|
||||
bool operator==(const HashedId8& a, const HashedId8& b)
|
||||
{
|
||||
return a.octets == b.octets;
|
||||
}
|
||||
|
||||
bool operator!=(const HashedId8& a, const HashedId8& b)
|
||||
{
|
||||
return a.octets != b.octets;
|
||||
}
|
||||
|
||||
bool operator<(const HashedId8& a, const HashedId8& b)
|
||||
{
|
||||
return a.octets < b.octets;
|
||||
}
|
||||
|
||||
bool lexical_cast(const std::string& input, HashedId8& hid8)
|
||||
{
|
||||
auto staging = HashedId8::from_hexstring(input);
|
||||
if (staging) {
|
||||
hid8 = *staging;
|
||||
}
|
||||
return staging.has_value();
|
||||
}
|
||||
|
||||
bool equals(const Vanetza_Security_HashedId8_t& asn, const HashedId8& own)
|
||||
{
|
||||
return asn.size == own.octets.size() && std::memcmp(asn.buf, own.octets.data(), own.octets.size()) == 0;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include "sha.hpp"
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
// forward declarations
|
||||
typedef struct OCTET_STRING OCTET_STRING_t;
|
||||
typedef OCTET_STRING_t Vanetza_Security_HashedId8_t;
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
struct HashedId8
|
||||
{
|
||||
HashedId8() = default;
|
||||
explicit HashedId8(const Sha256Hash&);
|
||||
explicit HashedId8(const Sha384Hash&);
|
||||
static boost::optional<HashedId8> from_hexstring(const std::string&);
|
||||
static boost::optional<HashedId8> from_buffer(const OCTET_STRING_t&);
|
||||
|
||||
std::array<std::uint8_t, 8> octets;
|
||||
};
|
||||
|
||||
bool operator==(const HashedId8&, const HashedId8&);
|
||||
bool operator!=(const HashedId8&, const HashedId8&);
|
||||
bool operator<(const HashedId8&, const HashedId8&);
|
||||
|
||||
std::string hexstring(const HashedId8&);
|
||||
bool valid_hashed_id8(const std::string& hex);
|
||||
|
||||
bool lexical_cast(const std::string&, HashedId8&);
|
||||
|
||||
bool equals(const Vanetza_Security_HashedId8_t&, const HashedId8&);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "CLI/Validators.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class HashedId8Validator : public CLI::Validator
|
||||
{
|
||||
public:
|
||||
HashedId8Validator() : Validator("HashedId8")
|
||||
{
|
||||
func_ = [](const std::string& input) {
|
||||
if (valid_hashed_id8(input)) {
|
||||
return "";
|
||||
} else {
|
||||
return "HashedId8 must be given as 16 hex-digits";
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "CLI/Validators.hpp"
|
||||
#include "hexstring.hpp"
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class HexStringValidator : public CLI::Validator
|
||||
{
|
||||
public:
|
||||
HexStringValidator() : Validator("HEX")
|
||||
{
|
||||
func_ = [](const std::string& input) {
|
||||
if (is_valid_hexstring(input)) {
|
||||
return "";
|
||||
} else {
|
||||
return "value must be an even, non-zero number of hex digits";
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,44 @@
|
||||
#include "hexstring.hpp"
|
||||
#include <boost/algorithm/hex.hpp>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <iterator>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
std::string hexstring(const std::uint8_t* buf, std::size_t len)
|
||||
{
|
||||
std::string out;
|
||||
boost::algorithm::hex(buf, buf + len, std::back_inserter(out));
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string hexstring(const std::string& input)
|
||||
{
|
||||
static_assert(sizeof(std::string::value_type) == sizeof(std::uint8_t));
|
||||
return hexstring(reinterpret_cast<const std::uint8_t*>(input.data()), input.size());
|
||||
}
|
||||
|
||||
std::string hexstring(const ByteBuffer& buffer)
|
||||
{
|
||||
return hexstring(buffer.data(), buffer.size());
|
||||
}
|
||||
|
||||
bool is_valid_hexstring(const std::string& input)
|
||||
{
|
||||
return !input.empty() && input.size() % 2 == 0 &&
|
||||
std::all_of(input.begin(), input.end(), [](unsigned char c) { return std::isxdigit(c) != 0; });
|
||||
}
|
||||
|
||||
std::string parse_hexstring(const std::string& input)
|
||||
{
|
||||
std::string out;
|
||||
boost::algorithm::unhex(input, std::back_inserter(out));
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include "vanetza/common/byte_buffer.hpp"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
|
||||
namespace pki
|
||||
{
|
||||
|
||||
std::string hexstring(const std::uint8_t* buf, std::size_t len);
|
||||
std::string hexstring(const std::string& input);
|
||||
std::string hexstring(const ByteBuffer& buffer);
|
||||
|
||||
template<size_t N> std::string hexstring(const std::array<std::uint8_t, N>& array)
|
||||
{
|
||||
return hexstring(array.data(), array.size());
|
||||
}
|
||||
|
||||
// true iff input is a non-empty, even-length run of hex digits
|
||||
bool is_valid_hexstring(const std::string& input);
|
||||
|
||||
// decode hex digits to raw bytes
|
||||
std::string parse_hexstring(const std::string& input);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,363 @@
|
||||
#include "http.hpp"
|
||||
#include <boost/asio/connect.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <boost/beast/ssl.hpp>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <ostream>
|
||||
#include <regex>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
namespace asio = boost::asio;
|
||||
namespace beast = boost::beast;
|
||||
namespace ssl = boost::asio::ssl;
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const HttpException& e)
|
||||
{
|
||||
os << e.what();
|
||||
if (const auto& response = e.response()) {
|
||||
os << " (HTTP " << response->result_int();
|
||||
if (!response->body().empty()) {
|
||||
os << ": " << response->body();
|
||||
}
|
||||
os << ")";
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
HttpQuery HttpQuery::from_url(const std::string& url)
|
||||
{
|
||||
std::regex url_regex { "^(https?)://([^:/]+)(?::([[:digit:]]+))?(/?|/.*)$" };
|
||||
std::smatch match;
|
||||
if (std::regex_match(url, match, url_regex) && match.size() == 5) {
|
||||
HttpQuery query;
|
||||
query.secure = (match[1] == "https");
|
||||
query.host = match[2];
|
||||
query.path = match[4];
|
||||
if (match[3].matched) {
|
||||
query.service = match[3];
|
||||
} else {
|
||||
query.service = match[1];
|
||||
}
|
||||
return query;
|
||||
} else {
|
||||
throw HttpException("URL is not acceptable");
|
||||
}
|
||||
}
|
||||
|
||||
std::string resolve_url(const std::string& base, const std::string& reference)
|
||||
{
|
||||
if (reference.empty()) {
|
||||
return base;
|
||||
}
|
||||
// A reference with its own scheme is already absolute (RFC 3986 §5.2.2).
|
||||
static const std::regex scheme_re { "^[a-zA-Z][a-zA-Z0-9+.-]*://" };
|
||||
if (std::regex_search(reference, scheme_re)) {
|
||||
return reference;
|
||||
}
|
||||
|
||||
// Split the base into scheme, authority and path (RFC 3986 §3); drop any query/fragment.
|
||||
static const std::regex base_re { "^([a-zA-Z][a-zA-Z0-9+.-]*)://([^/?#]*)(/[^?#]*)?.*$" };
|
||||
std::smatch m;
|
||||
if (!std::regex_match(base, m, base_re)) {
|
||||
throw HttpException("base URL is not acceptable");
|
||||
}
|
||||
const std::string scheme = m[1];
|
||||
const std::string origin = scheme + "://" + std::string(m[2]); // scheme + authority
|
||||
const std::string base_path = m[3].matched ? std::string(m[3]) : "/";
|
||||
|
||||
if (reference.compare(0, 2, "//") == 0) {
|
||||
// network-path reference: keep the base scheme, replace authority and path
|
||||
return scheme + ":" + reference;
|
||||
} else if (reference.front() == '/') {
|
||||
// absolute-path reference: replace the whole path
|
||||
return origin + reference;
|
||||
} else {
|
||||
// relative-path reference: merge onto the base's directory
|
||||
const std::string dir = base_path.substr(0, base_path.find_last_of('/') + 1);
|
||||
return origin + dir + reference;
|
||||
}
|
||||
}
|
||||
|
||||
const std::string& HttpQuery::which_service() const
|
||||
{
|
||||
if (service.empty()) {
|
||||
static const std::string http_service = "http";
|
||||
static const std::string https_service = "https";
|
||||
return secure ? https_service : http_service;
|
||||
} else {
|
||||
return service;
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// resolve and connect lifecycle of one address family taking part in a Happy Eyeballs race
|
||||
class Family
|
||||
{
|
||||
public:
|
||||
explicit Family(asio::io_context& ctx) : m_socket(ctx) {}
|
||||
|
||||
bool resolved() const { return static_cast<bool>(m_addresses); }
|
||||
bool connected() const { return m_error && !*m_error; }
|
||||
bool failed() const { return m_error && *m_error; }
|
||||
boost::system::error_code error() const { return m_error.value_or(boost::system::error_code {}); }
|
||||
|
||||
void resolve(asio::ip::tcp::resolver& resolver, const asio::ip::tcp& protocol,
|
||||
const std::string& host, const std::string& service, const std::function<void()>& notify)
|
||||
{
|
||||
resolver.async_resolve(protocol, host, service,
|
||||
[this, notify](boost::system::error_code ec, asio::ip::tcp::resolver::results_type results) {
|
||||
if (ec) {
|
||||
m_error = ec;
|
||||
} else {
|
||||
m_addresses = results;
|
||||
}
|
||||
notify();
|
||||
});
|
||||
}
|
||||
|
||||
void connect(const std::function<void()>& notify)
|
||||
{
|
||||
if (resolved() && !m_connecting && !m_error) {
|
||||
m_connecting = true;
|
||||
asio::async_connect(m_socket, *m_addresses,
|
||||
[this, notify](boost::system::error_code ec, const asio::ip::tcp::endpoint&) {
|
||||
m_connecting = false;
|
||||
m_error = ec;
|
||||
notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// hand the connected socket over to another io_context
|
||||
asio::ip::tcp::socket release(asio::io_context& io)
|
||||
{
|
||||
return asio::ip::tcp::socket { io, m_socket.local_endpoint().protocol(), m_socket.release() };
|
||||
}
|
||||
|
||||
private:
|
||||
asio::ip::tcp::socket m_socket;
|
||||
boost::optional<asio::ip::tcp::resolver::results_type> m_addresses;
|
||||
boost::optional<boost::system::error_code> m_error;
|
||||
bool m_connecting = false; // a connect attempt is in flight
|
||||
};
|
||||
|
||||
// Happy Eyeballs (RFC 8305): race AAAA/A lookups and per-family connect attempts, preferring IPv6
|
||||
class HappyEyeballsRace
|
||||
{
|
||||
public:
|
||||
// winning socket is adopted by the caller's io_context
|
||||
static asio::ip::tcp::socket connect(asio::io_context& io, const std::string& host,
|
||||
const std::string& service, std::chrono::milliseconds deadline)
|
||||
{
|
||||
HappyEyeballsRace race;
|
||||
return race.run(host, service, deadline).release(io);
|
||||
}
|
||||
|
||||
private:
|
||||
// run the race and return the winning family, throw when nobody wins
|
||||
Family& run(const std::string& host, const std::string& service, std::chrono::milliseconds deadline)
|
||||
{
|
||||
timeout.expires_after(deadline);
|
||||
timeout.async_wait([this](boost::system::error_code ec) {
|
||||
if (!ec) {
|
||||
race.stop();
|
||||
}
|
||||
});
|
||||
v6.resolve(resolver, asio::ip::tcp::v6(), host, service, [this]() { on_v6_resolve_done(); });
|
||||
v4.resolve(resolver, asio::ip::tcp::v4(), host, service, [this]() { on_v4_resolve_done(); });
|
||||
race.run();
|
||||
|
||||
if (v6.connected() || v4.connected()) {
|
||||
return v6.connected() ? v6 : v4;
|
||||
} else if (!v6.failed() || !v4.failed()) {
|
||||
throw HttpException("connect timed out");
|
||||
} else if (v6.resolved() || v4.resolved()) {
|
||||
throw HttpException("connect failed: " + (v6.resolved() ? v6.error() : v4.error()).message());
|
||||
} else {
|
||||
throw HttpException("resolve failed: " + (v6.error() ? v6.error() : v4.error()).message());
|
||||
}
|
||||
}
|
||||
|
||||
void on_v6_resolve_done()
|
||||
{
|
||||
if (v6.resolved()) {
|
||||
v6.connect([this]() { on_v6_connect_done(); });
|
||||
open_v4_gate(stagger_delay); // IPv4 may compete once the attempt delay elapsed
|
||||
} else {
|
||||
open_v4_gate(std::chrono::milliseconds::zero()); // no AAAA answer, IPv4 goes at once
|
||||
}
|
||||
settle();
|
||||
}
|
||||
|
||||
void on_v4_resolve_done()
|
||||
{
|
||||
if (is_v4_gate_open()) {
|
||||
v4.connect([this]() { settle(); });
|
||||
} else if (v4.resolved() && !v6.resolved()) {
|
||||
open_v4_gate(resolution_delay); // pending AAAA answer gets a short head start
|
||||
}
|
||||
settle();
|
||||
}
|
||||
|
||||
void on_v6_connect_done()
|
||||
{
|
||||
if (v6.failed()) {
|
||||
open_v4_gate(std::chrono::milliseconds::zero()); // IPv6 lost, IPv4 takes over
|
||||
}
|
||||
settle();
|
||||
}
|
||||
|
||||
void open_v4_gate(std::chrono::milliseconds delay)
|
||||
{
|
||||
v4_gate.expires_after(delay);
|
||||
v4_gate.async_wait([this](boost::system::error_code ec) {
|
||||
if (!ec) {
|
||||
v4.connect([this]() { settle(); });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool is_v4_gate_open() const
|
||||
{
|
||||
return v4_gate.expiry() <= asio::steady_timer::clock_type::now();
|
||||
}
|
||||
|
||||
void settle()
|
||||
{
|
||||
// stop the race once a connection is established or both families have failed
|
||||
if (v6.connected() || v4.connected() || (v6.failed() && v4.failed())) {
|
||||
race.stop();
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr std::chrono::milliseconds resolution_delay { 50 }; // RFC 8305 head start for the AAAA answer
|
||||
static constexpr std::chrono::milliseconds stagger_delay { 200 }; // connection attempt delay
|
||||
|
||||
asio::io_context race;
|
||||
asio::ip::tcp::resolver resolver { race };
|
||||
Family v6 { race };
|
||||
Family v4 { race };
|
||||
asio::steady_timer v4_gate { race, asio::steady_timer::time_point::max() }; // gate starts closed
|
||||
asio::steady_timer timeout { race };
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
static bool should_follow(int status)
|
||||
{
|
||||
switch (status) {
|
||||
case 301:
|
||||
case 302:
|
||||
case 303:
|
||||
case 307:
|
||||
case 308:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
HttpResponse query_http(const HttpQuery& query, const beast::http::request<T>& request, asio::io_context& io,
|
||||
ssl::context& ssl)
|
||||
{
|
||||
constexpr std::chrono::seconds connect_deadline { 10 };
|
||||
HttpResponse response;
|
||||
beast::flat_buffer buffer;
|
||||
asio::ip::tcp::socket socket = HappyEyeballsRace::connect(io, query.host, query.which_service(), connect_deadline);
|
||||
|
||||
if (query.secure) {
|
||||
ssl::stream<asio::ip::tcp::socket> stream(std::move(socket), ssl);
|
||||
SSL_set_tlsext_host_name(stream.native_handle(), query.host.c_str());
|
||||
|
||||
stream.lowest_layer().set_option(asio::ip::tcp::no_delay(true));
|
||||
stream.handshake(ssl::stream_base::client);
|
||||
|
||||
beast::http::write(stream, request);
|
||||
beast::http::read(stream, buffer, response);
|
||||
beast::error_code ec;
|
||||
if (stream.shutdown(ec)) {
|
||||
// could log error in verbose mode
|
||||
}
|
||||
} else {
|
||||
beast::http::write(socket, request);
|
||||
beast::http::read(socket, buffer, response);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
HttpResponse query_http_redirections(HttpQuery query, const beast::http::request<T>& request, asio::io_context& io,
|
||||
ssl::context& ssl)
|
||||
{
|
||||
constexpr unsigned max_hops = 5;
|
||||
for (unsigned hop = 0; hop < max_hops; ++hop) {
|
||||
auto resp = query_http(query, request, io, ssl);
|
||||
if (should_follow(resp.result_int())) {
|
||||
std::string location { resp[beast::http::field::location] };
|
||||
query = HttpQuery::from_url(location);
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
|
||||
throw HttpException("too many redirects");
|
||||
}
|
||||
|
||||
template<typename T> HttpResponse query_http(const HttpQuery& query, const beast::http::request<T>& request)
|
||||
{
|
||||
asio::io_context io;
|
||||
|
||||
ssl::context ssl(ssl::context::tlsv13_client);
|
||||
ssl.set_default_verify_paths();
|
||||
ssl.set_verify_mode(ssl::context::verify_peer);
|
||||
ssl.set_verify_callback(ssl::host_name_verification(query.host));
|
||||
|
||||
auto resp = query_http(query, request, io, ssl);
|
||||
if (should_follow(resp.result_int())) {
|
||||
std::string location { resp[beast::http::field::location] };
|
||||
auto redirection = HttpQuery::from_url(location);
|
||||
return query_http_redirections(std::move(redirection), request, io, ssl);
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse http_get(const HttpQuery& query)
|
||||
{
|
||||
beast::http::request<beast::http::empty_body> request;
|
||||
request.method(beast::http::verb::get);
|
||||
request.target(query.path);
|
||||
request.set(beast::http::field::host, query.host);
|
||||
|
||||
return query_http(query, request);
|
||||
}
|
||||
|
||||
HttpResponse http_post(const HttpQuery& query, const std::string& content_type, const ByteBuffer& data)
|
||||
{
|
||||
beast::http::request<beast::http::string_body> request;
|
||||
request.method(beast::http::verb::post);
|
||||
request.target(query.path);
|
||||
request.set(beast::http::field::host, query.host);
|
||||
request.set(beast::http::field::content_type, content_type);
|
||||
request.body().assign(data.begin(), data.end());
|
||||
request.prepare_payload();
|
||||
|
||||
return query_http(query, request);
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <iosfwd>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
using HttpResponse = boost::beast::http::response<boost::beast::http::string_body>;
|
||||
|
||||
struct HttpException : public std::runtime_error
|
||||
{
|
||||
using std::runtime_error::runtime_error;
|
||||
|
||||
HttpException(const std::string& what, HttpResponse response) :
|
||||
std::runtime_error(what), m_response(std::move(response))
|
||||
{
|
||||
}
|
||||
|
||||
// Server response associated with the failure, if any.
|
||||
const boost::optional<HttpResponse>& response() const { return m_response; }
|
||||
|
||||
private:
|
||||
boost::optional<HttpResponse> m_response;
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const HttpException& e);
|
||||
|
||||
struct HttpQuery
|
||||
{
|
||||
static HttpQuery from_url(const std::string&);
|
||||
|
||||
bool secure = false;
|
||||
std::string host;
|
||||
std::string service;
|
||||
std::string path;
|
||||
|
||||
const std::string& which_service() const;
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Resolve a (possibly relative) URL reference against a base URL per RFC 3986 §5.
|
||||
*
|
||||
* An empty reference yields the base unchanged. A reference carrying its own scheme
|
||||
* (e.g. "https://...") is absolute and returned as-is. Otherwise it is resolved against
|
||||
* the base: "//authority/..." replaces the authority, a leading-slash path replaces the
|
||||
* base path, and any other path is merged onto the base's directory.
|
||||
* \throws HttpException if the base URL cannot be parsed
|
||||
*/
|
||||
std::string resolve_url(const std::string& base, const std::string& reference);
|
||||
|
||||
HttpResponse http_get(const HttpQuery& query);
|
||||
HttpResponse http_post(const HttpQuery& query, const std::string& content_type, const ByteBuffer& body);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,104 @@
|
||||
#include "key_command.hpp"
|
||||
#include "exception.hpp"
|
||||
#include "filesystem.hpp"
|
||||
#include "keys.hpp"
|
||||
#include "pem.hpp"
|
||||
#include <vanetza/security/public_key.hpp>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
const std::map<std::string, KeyType> key_type_map = {
|
||||
{ "NistP256", KeyType::NistP256 },
|
||||
{ "BrainpoolP256r1", KeyType::BrainpoolP256r1 },
|
||||
{ "BrainpoolP384r1", KeyType::BrainpoolP384r1 }
|
||||
};
|
||||
|
||||
struct Context
|
||||
{
|
||||
std::function<void()> action;
|
||||
std::filesystem::path path; // --out for generate, --in for print
|
||||
KeyType type = KeyType::BrainpoolP256r1;
|
||||
bool force = false;
|
||||
};
|
||||
|
||||
std::string key_type_name(KeyType type)
|
||||
{
|
||||
for (const auto& entry : key_type_map) {
|
||||
if (entry.second == type) {
|
||||
return entry.first;
|
||||
}
|
||||
}
|
||||
return "Unspecified";
|
||||
}
|
||||
|
||||
void print_public_key(const PublicKey& pub)
|
||||
{
|
||||
std::cout << "Key type: " << key_type_name(pub.type) << "\n";
|
||||
std::cout << "Canonical public key: " << security::canonical_hexstring(pub) << "\n";
|
||||
}
|
||||
|
||||
void generate_keyfile(Context& ctx)
|
||||
{
|
||||
if (std::filesystem::exists(ctx.path) && !ctx.force) {
|
||||
throw UsageError("key file already exists: " + ctx.path.string(), "pass --force to overwrite");
|
||||
}
|
||||
|
||||
PrivateKey priv = generate_private_key(ctx.type);
|
||||
write_pem_private_key(priv, ctx.path);
|
||||
|
||||
std::cout << "Wrote " << ctx.path.string() << "\n";
|
||||
print_public_key(derive_public_key(priv));
|
||||
}
|
||||
|
||||
void print_keyfile(Context& ctx)
|
||||
{
|
||||
auto priv = parse_pem_private_key(read(ctx.path));
|
||||
if (!priv) {
|
||||
throw UsageError("could not read an EC private key from " + ctx.path.string(), "expecting a PEM-encoded EC key");
|
||||
}
|
||||
print_public_key(derive_public_key(*priv));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::shared_ptr<CLI::App> build_key_command()
|
||||
{
|
||||
auto ctx = std::make_shared<Context>();
|
||||
auto app = std::make_shared<CLI::App>("EC key generation", "key");
|
||||
|
||||
auto generate = app->add_subcommand("generate", "generate an EC private key (PEM) for enrolment");
|
||||
generate->alias("gen");
|
||||
generate->add_option("--out,-o", ctx->path, "output PEM file")->required();
|
||||
generate->add_flag("--force", ctx->force, "overwrite an existing key file");
|
||||
generate->add_option("--key-type", ctx->type, "type of generated key")
|
||||
->default_val(KeyType::BrainpoolP256r1)
|
||||
->capture_default_str()
|
||||
->transform(CLI::CheckedTransformer(key_type_map, CLI::ignore_case));
|
||||
generate->callback([ctx]() { ctx->action = [ctx]() { generate_keyfile(*ctx); }; });
|
||||
|
||||
auto print = app->add_subcommand("print", "print the canonical public key of an existing PEM key file");
|
||||
print->add_option("--in,-i", ctx->path, "input PEM file")->required()->check(CLI::ExistingFile);
|
||||
print->callback([ctx]() { ctx->action = [ctx]() { print_keyfile(*ctx); }; });
|
||||
|
||||
app->require_subcommand(1);
|
||||
app->final_callback([ctx]() {
|
||||
if (ctx->action) {
|
||||
ctx->action();
|
||||
}
|
||||
});
|
||||
return app;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <CLI/CLI.hpp>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
std::shared_ptr<CLI::App> build_key_command();
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,132 @@
|
||||
#include "keys.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "openssl.hpp"
|
||||
#include <vanetza/asn1/security/PublicEncryptionKey.h>
|
||||
#include <vanetza/asn1/security/PublicVerificationKey.h>
|
||||
#include <vanetza/asn1/security/Signature.h>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
static ByteBuffer get_x_coordinate(const Vanetza_Security_EccP256CurvePoint_t& point)
|
||||
{
|
||||
switch (point.present) {
|
||||
case Vanetza_Security_EccP256CurvePoint_PR_compressed_y_0:
|
||||
return copy(point.choice.compressed_y_0);
|
||||
case Vanetza_Security_EccP256CurvePoint_PR_compressed_y_1:
|
||||
return copy(point.choice.compressed_y_1);
|
||||
case Vanetza_Security_EccP256CurvePoint_PR_x_only:
|
||||
return copy(point.choice.x_only);
|
||||
case Vanetza_Security_EccP256CurvePoint_PR_uncompressedP256:
|
||||
return copy(point.choice.uncompressedP256.x);
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
static ByteBuffer get_x_coordinate(const Vanetza_Security_EccP384CurvePoint_t& point)
|
||||
{
|
||||
switch (point.present) {
|
||||
case Vanetza_Security_EccP384CurvePoint_PR_compressed_y_0:
|
||||
return copy(point.choice.compressed_y_0);
|
||||
case Vanetza_Security_EccP384CurvePoint_PR_compressed_y_1:
|
||||
return copy(point.choice.compressed_y_1);
|
||||
case Vanetza_Security_EccP384CurvePoint_PR_x_only:
|
||||
return copy(point.choice.x_only);
|
||||
case Vanetza_Security_EccP384CurvePoint_PR_uncompressedP384:
|
||||
return copy(point.choice.uncompressedP384.x);
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Signature make_signature(const struct Vanetza_Security_Signature& asn)
|
||||
{
|
||||
Signature sig;
|
||||
switch (asn.present) {
|
||||
case Vanetza_Security_Signature_PR_ecdsaNistP256Signature:
|
||||
sig.type = KeyType::NistP256;
|
||||
sig.r = get_x_coordinate(asn.choice.ecdsaNistP256Signature.rSig);
|
||||
sig.s = copy(asn.choice.ecdsaNistP256Signature.sSig);
|
||||
break;
|
||||
case Vanetza_Security_Signature_PR_ecdsaBrainpoolP256r1Signature:
|
||||
sig.type = KeyType::BrainpoolP256r1;
|
||||
sig.r = get_x_coordinate(asn.choice.ecdsaBrainpoolP256r1Signature.rSig);
|
||||
sig.s = copy(asn.choice.ecdsaBrainpoolP256r1Signature.sSig);
|
||||
break;
|
||||
case Vanetza_Security_Signature_PR_ecdsaBrainpoolP384r1Signature:
|
||||
sig.type = KeyType::BrainpoolP384r1;
|
||||
sig.r = get_x_coordinate(asn.choice.ecdsaBrainpoolP384r1Signature.rSig);
|
||||
sig.s = copy(asn.choice.ecdsaBrainpoolP384r1Signature.sSig);
|
||||
break;
|
||||
default:
|
||||
sig.type = KeyType::Unspecified;
|
||||
break;
|
||||
}
|
||||
|
||||
return sig;
|
||||
}
|
||||
|
||||
PublicKey derive_public_key(const PrivateKey& priv)
|
||||
{
|
||||
auto ec_key = make_ec_key(priv);
|
||||
return make_public_key(ec_key.raw());
|
||||
}
|
||||
|
||||
PrivateKey generate_private_key(KeyType type)
|
||||
{
|
||||
OpenSslPointer<EC_KEY> ec_key { EC_KEY_new_by_curve_name(openssl_nid(type)) };
|
||||
openssl_result(EC_KEY_generate_key(ec_key.raw()), "EC_KEY_generate_key");
|
||||
openssl_result(EC_KEY_check_key(ec_key.raw()), "generated EC_KEY is invalid");
|
||||
|
||||
const EC_GROUP* group = EC_KEY_get0_group(ec_key.raw());
|
||||
const std::size_t key_len = BN_num_bytes(EC_GROUP_get0_order(group));
|
||||
|
||||
PrivateKey priv;
|
||||
priv.type = type;
|
||||
priv.key = make_buffer(EC_KEY_get0_private_key(ec_key.raw()), key_len);
|
||||
return priv;
|
||||
}
|
||||
|
||||
void set_verification_key(Vanetza_Security_PublicVerificationKey& dst, const PublicKey& key)
|
||||
{
|
||||
switch (key.type) {
|
||||
case KeyType::NistP256:
|
||||
dst.present = Vanetza_Security_PublicVerificationKey_PR_ecdsaNistP256;
|
||||
fill_curve_point(key, dst.choice.ecdsaNistP256);
|
||||
break;
|
||||
case KeyType::BrainpoolP256r1:
|
||||
dst.present = Vanetza_Security_PublicVerificationKey_PR_ecdsaBrainpoolP256r1;
|
||||
fill_curve_point(key, dst.choice.ecdsaBrainpoolP256r1);
|
||||
break;
|
||||
case KeyType::BrainpoolP384r1:
|
||||
dst.present = Vanetza_Security_PublicVerificationKey_PR_ecdsaBrainpoolP384r1;
|
||||
fill_curve_point(key, dst.choice.ecdsaBrainpoolP384r1);
|
||||
break;
|
||||
default:
|
||||
throw std::invalid_argument("unsupported verification key type");
|
||||
}
|
||||
}
|
||||
|
||||
void set_encryption_key(Vanetza_Security_PublicEncryptionKey& dst, const PublicKey& key)
|
||||
{
|
||||
dst.supportedSymmAlg = Vanetza_Security_SymmAlgorithm_aes128Ccm;
|
||||
switch (key.type) {
|
||||
case KeyType::NistP256:
|
||||
dst.publicKey.present = Vanetza_Security_BasePublicEncryptionKey_PR_eciesNistP256;
|
||||
fill_curve_point(key, dst.publicKey.choice.eciesNistP256);
|
||||
break;
|
||||
case KeyType::BrainpoolP256r1:
|
||||
dst.publicKey.present = Vanetza_Security_BasePublicEncryptionKey_PR_eciesBrainpoolP256r1;
|
||||
fill_curve_point(key, dst.publicKey.choice.eciesBrainpoolP256r1);
|
||||
break;
|
||||
default:
|
||||
throw std::invalid_argument("unsupported encryption key type");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <vanetza/security/key_type.hpp>
|
||||
#include <vanetza/security/private_key.hpp>
|
||||
#include <vanetza/security/public_key.hpp>
|
||||
#include <vanetza/security/signature.hpp>
|
||||
|
||||
// forward declarations
|
||||
struct Vanetza_Security_Signature;
|
||||
struct Vanetza_Security_PublicVerificationKey;
|
||||
struct Vanetza_Security_PublicEncryptionKey;
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
using security::KeyCompression;
|
||||
using security::KeyType;
|
||||
using security::PrivateKey;
|
||||
using security::PublicKey;
|
||||
using security::Signature;
|
||||
|
||||
PublicKey derive_public_key(const PrivateKey&);
|
||||
Signature make_signature(const struct Vanetza_Security_Signature&);
|
||||
|
||||
/**
|
||||
* Generate a fresh random EC private key on the curve matching `type`.
|
||||
*
|
||||
* \throws OpenSslException on generation failure
|
||||
*/
|
||||
PrivateKey generate_private_key(KeyType type);
|
||||
|
||||
/**
|
||||
* Populate an ASN.1 PublicVerificationKey CHOICE from a PublicKey.
|
||||
*
|
||||
* \throws std::invalid_argument on unsupported key types
|
||||
*/
|
||||
void set_verification_key(struct Vanetza_Security_PublicVerificationKey&, const PublicKey&);
|
||||
|
||||
/**
|
||||
* Populate an ASN.1 PublicEncryptionKey from a PublicKey.
|
||||
* Sets supportedSymmAlg to AES-128-CCM.
|
||||
*
|
||||
* \throws std::invalid_argument on unsupported key types
|
||||
*/
|
||||
void set_encryption_key(struct Vanetza_Security_PublicEncryptionKey&, const PublicKey&);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,118 @@
|
||||
#include "main.hpp"
|
||||
#include "authorization.hpp"
|
||||
#include "certificate_filesystem_storage.hpp"
|
||||
#include "cpoc.hpp"
|
||||
#include "credential_filesystem_storage.hpp"
|
||||
#include "dc_command.hpp"
|
||||
#include "enrolment.hpp"
|
||||
#include "exception.hpp"
|
||||
#include "http.hpp"
|
||||
#include "key_command.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include "printing.hpp"
|
||||
#include "prune_command.hpp"
|
||||
#include "station.hpp"
|
||||
#include "station_config_filesystem.hpp"
|
||||
#include "trust_list_filesystem_storage.hpp"
|
||||
#include "xdg.hpp"
|
||||
#include <CLI/CLI.hpp>
|
||||
#include <boost/date_time/posix_time/posix_time_io.hpp>
|
||||
#include <iostream>
|
||||
#include <locale>
|
||||
|
||||
using namespace vanetza::pki;
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
CLI::App app("Vanetza PKI tool");
|
||||
MainConfig config;
|
||||
config.config_path = xdg_config_home() / "vanetza";
|
||||
config.data_path = xdg_data_home() / "vanetza" / "pki";
|
||||
|
||||
auto initialize_storage = [&config]() {
|
||||
config.credentials = std::make_shared<CredentialFilesystemStorage>(config.data_path / "keys");
|
||||
config.security = std::make_shared<OpenSslSecurityModule>(config.credentials);
|
||||
const std::filesystem::path certs_dir = config.data_path / "certificates";
|
||||
config.root_ca = std::make_shared<CertificateFilesystemStorage>(config.security, certs_dir, ".rca");
|
||||
config.enrolment_credentials =
|
||||
std::make_shared<CertificateFilesystemStorage>(config.security, certs_dir, ".ec");
|
||||
config.tickets = std::make_shared<CertificateFilesystemStorage>(config.security, certs_dir, ".at");
|
||||
config.tlm = std::make_shared<CertificateFilesystemStorage>(config.security, certs_dir, ".tlm");
|
||||
config.authorization_authorities =
|
||||
std::make_shared<CertificateFilesystemStorage>(config.security, certs_dir, ".aa");
|
||||
config.enrolment_authorities =
|
||||
std::make_shared<CertificateFilesystemStorage>(config.security, certs_dir, ".ea");
|
||||
config.trust_lists = std::make_shared<TrustListFilesystemStorage>(config.security, config.data_path / "ctls");
|
||||
config.crl_store = std::make_shared<CrlFilesystemStore>(config.security, config.data_path / "crls");
|
||||
config.station = std::make_shared<StationConfigurationFilesystem>(config.data_path);
|
||||
if (auto root_ca = config.station->get_root_ca()) {
|
||||
config.root_ca_hid8 = *root_ca;
|
||||
config.dc_url = lookup_dc_url(config.data_path / "ectl.ctl", *root_ca).value_or("");
|
||||
} else {
|
||||
config.root_ca_hid8 = HashedId8 {};
|
||||
config.dc_url.clear();
|
||||
}
|
||||
};
|
||||
|
||||
const auto default_config_file = config.config_path / "pki.cfg";
|
||||
auto config_option = app.set_config("--config", default_config_file.string());
|
||||
config_option->description("PKI configuration file");
|
||||
config_option->envname("VANETZA_PKI_CONFIG");
|
||||
|
||||
auto data_option = app.add_option("--data", config.data_path);
|
||||
data_option->description("PKI data directory");
|
||||
data_option->default_str(config.data_path.string());
|
||||
data_option->envname("VANETZA_PKI_DATA");
|
||||
data_option->type_name("");
|
||||
data_option->check([](const std::string& path) -> std::string {
|
||||
auto status = std::filesystem::status(path);
|
||||
if (std::filesystem::exists(status) && !std::filesystem::is_directory(status)) {
|
||||
return "PKI data path exists but is not a directory";
|
||||
}
|
||||
return {};
|
||||
});
|
||||
// Storage derives from config.data_path, which CLI11 binds during parse
|
||||
// (from --data, env, or config file). Build it once that value is final.
|
||||
app.parse_complete_callback(initialize_storage);
|
||||
|
||||
app.require_subcommand(1);
|
||||
app.add_subcommand(build_authorization_command(config));
|
||||
app.add_subcommand(build_cpoc_command(config));
|
||||
app.add_subcommand(build_dc_command(config));
|
||||
app.add_subcommand(build_enrolment_command(config));
|
||||
app.add_subcommand(build_key_command());
|
||||
app.add_subcommand(build_print_command());
|
||||
app.add_subcommand(build_prune_command(config));
|
||||
app.add_subcommand(build_station_command(config));
|
||||
|
||||
std::cout.imbue(std::locale(std::cout.getloc(), new boost::posix_time::time_facet("%Y-%m-%d %H:%M:%S")));
|
||||
|
||||
try {
|
||||
app.parse(argc, argv);
|
||||
} catch (const CLI::ParseError& e) {
|
||||
return app.exit(e);
|
||||
} catch (const UsageError& e) {
|
||||
std::cerr << "Usage: " << e.what() << "\n";
|
||||
if (!e.remedy().empty()) {
|
||||
std::cerr << "Hint: " << e.remedy() << "\n";
|
||||
}
|
||||
return 2;
|
||||
} catch (const VerificationFailure& e) {
|
||||
std::cerr << "Verification failed: " << e.what() << "\n";
|
||||
return 3;
|
||||
} catch (const DecodingFailure& e) {
|
||||
std::cerr << "Decoding failed: " << e.what() << "\n";
|
||||
return 1;
|
||||
} catch (const HttpException& e) {
|
||||
std::cerr << "HTTP error: " << e << "\n";
|
||||
return 1;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error: " << e.what() << "\n";
|
||||
return 1;
|
||||
} catch (...) {
|
||||
std::cerr << "Unknown error occurred\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "certificate_storage.hpp"
|
||||
#include "credential_storage.hpp"
|
||||
#include "crl_store.hpp"
|
||||
#include "filesystem.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "security_module.hpp"
|
||||
#include "station_config.hpp"
|
||||
#include "trust_list_storage.hpp"
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
struct MainConfig
|
||||
{
|
||||
std::filesystem::path config_path;
|
||||
std::filesystem::path data_path;
|
||||
std::string dc_url;
|
||||
HashedId8 root_ca_hid8;
|
||||
std::shared_ptr<StationConfiguration> station;
|
||||
std::shared_ptr<SecurityModule> security;
|
||||
std::shared_ptr<CertificateStorage> root_ca; // root CA certificates
|
||||
std::shared_ptr<CertificateStorage> enrolment_credentials; // station's EC certificates
|
||||
std::shared_ptr<CertificateStorage> tickets; // station's authorization tickets
|
||||
std::shared_ptr<CertificateStorage> tlm; // trust list manager certificates
|
||||
std::shared_ptr<CertificateStorage> authorization_authorities; // AA certs extracted from CTLs
|
||||
std::shared_ptr<CertificateStorage> enrolment_authorities; // EA certs extracted from CTLs
|
||||
std::shared_ptr<CredentialStorage> credentials; // private keys
|
||||
std::shared_ptr<TrustListStorage> trust_lists;
|
||||
std::shared_ptr<CrlFilesystemStore> crl_store; // certificate revocation lists
|
||||
};
|
||||
|
||||
boost::optional<std::string> lookup_dc_url(const std::filesystem::path& ectl_file, const HashedId8& root_ca);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,238 @@
|
||||
#include "openssl.hpp"
|
||||
#include <vanetza/security/openssl_wrapper.hpp>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
template<> std::function<void(BIGNUM*)> openssl_deleter<BIGNUM>()
|
||||
{
|
||||
return [](BIGNUM* ptr) { BN_clear_free(ptr); };
|
||||
}
|
||||
|
||||
template<> std::function<void(BIO*)> openssl_deleter<BIO>()
|
||||
{
|
||||
return [](BIO* ptr) { BIO_free_all(ptr); };
|
||||
}
|
||||
|
||||
template<> std::function<void(BN_CTX*)> openssl_deleter<BN_CTX>()
|
||||
{
|
||||
return [](BN_CTX* ptr) { BN_CTX_free(ptr); };
|
||||
}
|
||||
|
||||
template<> std::function<void(EC_GROUP*)> openssl_deleter<EC_GROUP>()
|
||||
{
|
||||
return [](EC_GROUP* ptr) { EC_GROUP_clear_free(ptr); };
|
||||
}
|
||||
|
||||
template<> std::function<void(EC_KEY*)> openssl_deleter<EC_KEY>()
|
||||
{
|
||||
return [](EC_KEY* ptr) { EC_KEY_free(ptr); };
|
||||
}
|
||||
|
||||
template<> std::function<void(EC_POINT*)> openssl_deleter<EC_POINT>()
|
||||
{
|
||||
return [](EC_POINT* ptr) { EC_POINT_clear_free(ptr); };
|
||||
}
|
||||
|
||||
template<> std::function<void(ECDSA_SIG*)> openssl_deleter<ECDSA_SIG>()
|
||||
{
|
||||
return [](ECDSA_SIG* ptr) { ECDSA_SIG_free(ptr); };
|
||||
}
|
||||
|
||||
template<> std::function<void(EVP_PKEY*)> openssl_deleter<EVP_PKEY>()
|
||||
{
|
||||
return [](EVP_PKEY* ptr) { EVP_PKEY_free(ptr); };
|
||||
}
|
||||
|
||||
template<> std::function<void(EVP_CIPHER_CTX*)> openssl_deleter<EVP_CIPHER_CTX>()
|
||||
{
|
||||
return [](EVP_CIPHER_CTX* ptr) { EVP_CIPHER_CTX_free(ptr); };
|
||||
}
|
||||
|
||||
int openssl_nid(KeyType key)
|
||||
{
|
||||
int nid = 0;
|
||||
switch (key) {
|
||||
case KeyType::NistP256:
|
||||
nid = NID_X9_62_prime256v1;
|
||||
break;
|
||||
case KeyType::BrainpoolP256r1:
|
||||
nid = NID_brainpoolP256r1;
|
||||
break;
|
||||
case KeyType::BrainpoolP384r1:
|
||||
nid = NID_brainpoolP384r1;
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("unknown key type");
|
||||
break;
|
||||
}
|
||||
return nid;
|
||||
}
|
||||
|
||||
KeyType openssl_nid2key(int nid)
|
||||
{
|
||||
switch (nid) {
|
||||
case NID_X9_62_prime256v1:
|
||||
return KeyType::NistP256;
|
||||
break;
|
||||
case NID_brainpoolP256r1:
|
||||
return KeyType::BrainpoolP256r1;
|
||||
break;
|
||||
case NID_brainpoolP384r1:
|
||||
return KeyType::BrainpoolP384r1;
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("unsupported curve type");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
KeyType openssl_key_type_from_group_name(const char* name)
|
||||
{
|
||||
if (std::strcmp(name, SN_X9_62_prime256v1) == 0) {
|
||||
return KeyType::NistP256;
|
||||
} else if (std::strcmp(name, SN_brainpoolP256r1) == 0) {
|
||||
return KeyType::BrainpoolP256r1;
|
||||
} else if (std::strcmp(name, SN_brainpoolP384r1) == 0) {
|
||||
return KeyType::BrainpoolP384r1;
|
||||
} else {
|
||||
return KeyType::Unspecified;
|
||||
}
|
||||
}
|
||||
|
||||
void openssl_result(int rc, const char* msg)
|
||||
{
|
||||
if (rc != 1) {
|
||||
throw OpenSslException(ERR_get_error(), msg);
|
||||
}
|
||||
}
|
||||
|
||||
OpenSslPointer<BIGNUM> make_bignum(const ByteBuffer& buffer)
|
||||
{
|
||||
OpenSslPointer<BIGNUM> bn { BN_new() };
|
||||
if (!BN_bin2bn(buffer.data(), buffer.size(), bn.raw())) {
|
||||
throw OpenSslException(ERR_get_error());
|
||||
}
|
||||
return bn;
|
||||
}
|
||||
|
||||
OpenSslPointer<EC_POINT> make_ec_point(const PublicKey& pub)
|
||||
{
|
||||
int nid = openssl_nid(pub.type);
|
||||
OpenSslPointer<EC_GROUP> group { EC_GROUP_new_by_curve_name(nid) };
|
||||
OpenSslPointer<EC_POINT> point { EC_POINT_new(group.raw()) };
|
||||
OpenSslPointer<BN_CTX> bn_ctx { BN_CTX_new() };
|
||||
|
||||
int rc = 0;
|
||||
if (pub.compression == KeyCompression::NoCompression) {
|
||||
auto x = make_bignum(pub.x);
|
||||
auto y = make_bignum(pub.y);
|
||||
rc = EC_POINT_set_affine_coordinates(group.raw(), point.raw(), x.raw(), y.raw(), bn_ctx.raw());
|
||||
} else if (pub.compression == KeyCompression::Y0 || pub.compression == KeyCompression::Y1) {
|
||||
auto x = make_bignum(pub.x);
|
||||
int ybit = pub.compression == KeyCompression::Y1 ? 1 : 0;
|
||||
rc = EC_POINT_set_compressed_coordinates(group.raw(), point.raw(), x.raw(), ybit, bn_ctx.raw());
|
||||
} else {
|
||||
throw std::invalid_argument("invalid key compression type");
|
||||
}
|
||||
|
||||
return point;
|
||||
}
|
||||
|
||||
ByteBuffer make_buffer(const BIGNUM* bn)
|
||||
{
|
||||
ByteBuffer buffer;
|
||||
buffer.resize(BN_num_bytes(bn));
|
||||
BN_bn2bin(bn, buffer.data());
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// Variant that emits the BIGNUM as exactly `length` bytes, left-padded with
|
||||
// zeros. Use this when downstream consumers expect a fixed canonical width
|
||||
// (e.g. EC field sizes for round-trip-safe key storage).
|
||||
ByteBuffer make_buffer(const BIGNUM* bn, std::size_t length)
|
||||
{
|
||||
ByteBuffer buffer;
|
||||
buffer.resize(length);
|
||||
if (BN_bn2binpad(bn, buffer.data(), length) != static_cast<int>(length)) {
|
||||
throw OpenSslException(ERR_get_error(), "BN_bn2binpad");
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
OpenSslPointer<EC_KEY> make_ec_key(const PublicKey& pub)
|
||||
{
|
||||
auto ec_point = make_ec_point(pub);
|
||||
OpenSslPointer<EC_KEY> ec_key { EC_KEY_new_by_curve_name(openssl_nid(pub.type)) };
|
||||
openssl_result(EC_KEY_set_public_key(ec_key.raw(), ec_point.raw()), "set public key");
|
||||
openssl_result(EC_KEY_check_key(ec_key.raw()), "check key");
|
||||
return ec_key;
|
||||
}
|
||||
|
||||
OpenSslPointer<EC_KEY> make_ec_key(const PrivateKey& priv)
|
||||
{
|
||||
// create EC_KEY with private key data
|
||||
OpenSslPointer<BIGNUM> bn_priv { BN_bin2bn(priv.key.data(), priv.key.size(), nullptr) };
|
||||
OpenSslPointer<EC_KEY> ec_key { EC_KEY_new_by_curve_name(openssl_nid(priv.type)) };
|
||||
openssl_result(EC_KEY_set_private_key(ec_key.raw(), bn_priv.raw()), "setting private key failed");
|
||||
|
||||
// calculate and assign public key
|
||||
const EC_GROUP* ec_group = EC_KEY_get0_group(ec_key.raw());
|
||||
OpenSslPointer<EC_POINT> ec_point { EC_POINT_new(ec_group) };
|
||||
OpenSslPointer<BN_CTX> bn_ctx { BN_CTX_new() };
|
||||
openssl_result(EC_POINT_mul(ec_group, ec_point.raw(), bn_priv.raw(), nullptr, nullptr, bn_ctx.raw()),
|
||||
"EC point multiplation failed");
|
||||
openssl_result(EC_KEY_set_public_key(ec_key.raw(), ec_point.raw()), "setting public key failed");
|
||||
|
||||
// check key integrity
|
||||
openssl_result(EC_KEY_check_key(ec_key.raw()), "calculated EC_KEY is invalid");
|
||||
|
||||
return ec_key;
|
||||
}
|
||||
|
||||
PublicKey make_public_key(const EC_KEY* ec_key)
|
||||
{
|
||||
PublicKey pub;
|
||||
const EC_GROUP* group = EC_KEY_get0_group(ec_key);
|
||||
pub.type = openssl_nid2key(EC_GROUP_get_curve_name(group));
|
||||
const EC_POINT* point = EC_KEY_get0_public_key(ec_key);
|
||||
if (point) {
|
||||
const std::size_t coord_len = (EC_GROUP_get_degree(group) + 7) / 8;
|
||||
OpenSslPointer<BIGNUM> x { BN_new() };
|
||||
OpenSslPointer<BIGNUM> y { BN_new() };
|
||||
openssl_result(EC_POINT_get_affine_coordinates(group, point, x.raw(), y.raw(), nullptr),
|
||||
"get affine coordinates");
|
||||
pub.compression = BN_is_bit_set(y.raw(), 0) ? security::KeyCompression::Y1 : security::KeyCompression::Y0;
|
||||
pub.x = make_buffer(x.raw(), coord_len);
|
||||
} else {
|
||||
throw OpenSslException(ERR_get_error(), "EC_KEY_get0_public_key");
|
||||
}
|
||||
|
||||
return pub;
|
||||
}
|
||||
|
||||
OpenSslPointer<BIO> make_owner_only_bio(const std::filesystem::path& path)
|
||||
{
|
||||
int fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
|
||||
if (fd < 0) {
|
||||
throw std::runtime_error("could not create file at " + path.string());
|
||||
}
|
||||
::fchmod(fd, S_IRUSR | S_IWUSR); // enforce 0600 even if the file already existed
|
||||
BIO* bio = BIO_new_fd(fd, BIO_CLOSE);
|
||||
if (!bio) {
|
||||
::close(fd);
|
||||
throw OpenSslException(ERR_get_error(), "BIO_new_fd");
|
||||
}
|
||||
return OpenSslPointer<BIO>(bio);
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
#include "keys.hpp"
|
||||
#include <vanetza/security/openssl_wrapper.hpp>
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <cassert>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
template<typename T> std::function<void(T*)> openssl_deleter();
|
||||
|
||||
template<> std::function<void(BIGNUM*)> openssl_deleter<BIGNUM>();
|
||||
|
||||
template<> std::function<void(BIO*)> openssl_deleter<BIO>();
|
||||
|
||||
template<> std::function<void(BN_CTX*)> openssl_deleter<BN_CTX>();
|
||||
|
||||
template<> std::function<void(EC_GROUP*)> openssl_deleter<EC_GROUP>();
|
||||
|
||||
template<> std::function<void(EC_KEY*)> openssl_deleter<EC_KEY>();
|
||||
|
||||
template<> std::function<void(EC_POINT*)> openssl_deleter<EC_POINT>();
|
||||
|
||||
template<> std::function<void(ECDSA_SIG*)> openssl_deleter<ECDSA_SIG>();
|
||||
|
||||
template<> std::function<void(EVP_PKEY*)> openssl_deleter<EVP_PKEY>();
|
||||
|
||||
template<> std::function<void(EVP_CIPHER_CTX*)> openssl_deleter<EVP_CIPHER_CTX>();
|
||||
|
||||
using OpenSslException = vanetza::security::openssl::Exception;
|
||||
|
||||
template<typename T> class OpenSslPointer
|
||||
{
|
||||
public:
|
||||
explicit OpenSslPointer(T* raw) : m_ptr(raw, openssl_deleter<T>())
|
||||
{
|
||||
if (raw == nullptr) {
|
||||
throw OpenSslException(ERR_get_error());
|
||||
}
|
||||
}
|
||||
|
||||
OpenSslPointer(T* raw, const char* errmsg) : m_ptr(raw, openssl_deleter<T>())
|
||||
{
|
||||
if (raw == nullptr) {
|
||||
throw OpenSslException(ERR_get_error(), errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
OpenSslPointer(T* raw, const std::string& errmsg) : OpenSslPointer(raw, errmsg.c_str())
|
||||
{
|
||||
}
|
||||
|
||||
T* raw()
|
||||
{
|
||||
assert(m_ptr);
|
||||
return m_ptr.get();
|
||||
}
|
||||
|
||||
const T* raw() const
|
||||
{
|
||||
assert(m_ptr);
|
||||
return m_ptr.get();
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<T, std::function<void(T*)>> m_ptr;
|
||||
};
|
||||
|
||||
int openssl_nid(KeyType);
|
||||
KeyType openssl_nid2key(int);
|
||||
KeyType openssl_key_type_from_group_name(const char*);
|
||||
void openssl_result(int rc, const char*);
|
||||
|
||||
OpenSslPointer<EC_POINT> make_ec_point(const PublicKey&);
|
||||
OpenSslPointer<EC_KEY> make_ec_key(const PublicKey&);
|
||||
OpenSslPointer<EC_KEY> make_ec_key(const PrivateKey&);
|
||||
OpenSslPointer<BIGNUM> make_bignum(const ByteBuffer&);
|
||||
PublicKey make_public_key(const EC_KEY*);
|
||||
ByteBuffer make_buffer(const BIGNUM*);
|
||||
ByteBuffer make_buffer(const BIGNUM*, std::size_t length);
|
||||
|
||||
/**
|
||||
* \brief Open (truncate) a file as owner-only and wrap its descriptor in a BIO.
|
||||
*
|
||||
* Prevents that the file is ever momentarily readable by other users.
|
||||
* \param path file to create or truncate
|
||||
* \return BIO writing to the file; closing the BIO closes the descriptor
|
||||
* \throws std::runtime_error if the file cannot be created
|
||||
* \throws OpenSslException if the BIO cannot be allocated
|
||||
*/
|
||||
OpenSslPointer<BIO> make_owner_only_bio(const std::filesystem::path& path);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,338 @@
|
||||
#include "openssl_security_module.hpp"
|
||||
#include "credential_storage.hpp"
|
||||
#include "ecies.hpp"
|
||||
#include "openssl.hpp"
|
||||
#include <vanetza/security/key_type.hpp>
|
||||
#include <openssl/hmac.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/sha.h>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr std::size_t aes128_ccm_key_length = 16;
|
||||
constexpr std::size_t nonce_length = 12;
|
||||
constexpr std::size_t auth_tag_length = 16;
|
||||
|
||||
ByteBuffer calculate_hmac_sha256(const ByteBuffer& key, const ByteBuffer& data)
|
||||
{
|
||||
ByteBuffer hmac;
|
||||
hmac.resize(Sha256Hash::length);
|
||||
unsigned hmac_len = 0;
|
||||
if (!HMAC(EVP_sha256(), key.data(), key.size(), data.data(), data.size(), hmac.data(), &hmac_len)) {
|
||||
throw OpenSslException(ERR_get_error(), "HMAC");
|
||||
} else if (hmac_len != hmac.size()) {
|
||||
throw std::runtime_error("invalid length of HMAC");
|
||||
}
|
||||
|
||||
static_assert(Sha256Hash::length >= auth_tag_length, "length of authentication tag exceeds SHA-256 hash");
|
||||
hmac.resize(auth_tag_length);
|
||||
return hmac;
|
||||
}
|
||||
|
||||
Sha256Hash calculate_sha256_hash(const ByteBuffer& data)
|
||||
{
|
||||
Sha256Hash hash;
|
||||
SHA256(data.data(), data.size(), hash.octets.data());
|
||||
return hash;
|
||||
}
|
||||
|
||||
class OpenSslEciesContext : public SecurityModule::EciesContext
|
||||
{
|
||||
public:
|
||||
OpenSslEciesContext(const PublicKey& receiver, const Sha256Hash& info) :
|
||||
m_ephemeral(EC_KEY_new_by_curve_name(openssl_nid(receiver.type))), m_recipient(receiver)
|
||||
{
|
||||
openssl_result(EC_KEY_generate_key(m_ephemeral.raw()), "EC_KEY_generate_key");
|
||||
openssl_result(EC_KEY_check_key(m_ephemeral.raw()), "EC_KEY_check_key");
|
||||
calculate_shared_secret(receiver);
|
||||
|
||||
m_aes_key.resize(aes128_ccm_key_length);
|
||||
openssl_result(RAND_bytes(m_aes_key.data(), m_aes_key.size()), "RAND_bytes");
|
||||
m_nonce.resize(nonce_length);
|
||||
openssl_result(RAND_bytes(m_nonce.data(), m_nonce.size()), "RAND_bytes");
|
||||
encrypt_key(info);
|
||||
}
|
||||
|
||||
PublicKey ephemeral_public_key() const override
|
||||
{
|
||||
return make_public_key(m_ephemeral.raw());
|
||||
}
|
||||
|
||||
PublicKey recipient_public_key() const override
|
||||
{
|
||||
return m_recipient;
|
||||
}
|
||||
|
||||
ByteBuffer shared_secret() const override
|
||||
{
|
||||
return m_shared_secret;
|
||||
}
|
||||
|
||||
ByteBuffer encrypted_key() const override
|
||||
{
|
||||
return m_encrypted_key;
|
||||
}
|
||||
|
||||
ByteBuffer authentication_tag() const override
|
||||
{
|
||||
return m_authentication_tag;
|
||||
}
|
||||
|
||||
ByteBuffer nonce() const override
|
||||
{
|
||||
return m_nonce;
|
||||
}
|
||||
|
||||
void nonce(const ByteBuffer& nonce) override
|
||||
{
|
||||
if (nonce.size() != nonce_length) {
|
||||
throw std::runtime_error("size of given nonce is invalid");
|
||||
}
|
||||
m_nonce = nonce;
|
||||
}
|
||||
|
||||
ByteBuffer encrypt(const ByteBuffer& plaintext) override
|
||||
{
|
||||
OpenSslPointer<EVP_CIPHER_CTX> ctx { EVP_CIPHER_CTX_new() };
|
||||
openssl_result(EVP_CIPHER_CTX_init(ctx.raw()), "EVP_CIPHER_CTX_init");
|
||||
openssl_result(EVP_EncryptInit(ctx.raw(), EVP_aes_128_ccm(), nullptr, nullptr), "EVP_EncryptInit(CCM)");
|
||||
openssl_result(EVP_CIPHER_CTX_ctrl(ctx.raw(), EVP_CTRL_CCM_SET_TAG, auth_tag_length, nullptr),
|
||||
"EVP_CIPHER_CTX_ctrl(CCM_SET_TAG)");
|
||||
openssl_result(EVP_CIPHER_CTX_ctrl(ctx.raw(), EVP_CTRL_CCM_SET_IVLEN, nonce_length, nullptr),
|
||||
"EVP_CIPHER_CTX_ctrl(CCM_SET_IVLEN)");
|
||||
openssl_result(EVP_EncryptInit(ctx.raw(), nullptr, m_aes_key.data(), m_nonce.data()), "EVP_EncryptInit(key)");
|
||||
|
||||
ByteBuffer ciphertext;
|
||||
ciphertext.resize(plaintext.size() + auth_tag_length);
|
||||
int ciphertext_length = 0;
|
||||
openssl_result(EVP_EncryptUpdate(ctx.raw(), ciphertext.data(), &ciphertext_length, plaintext.data(),
|
||||
plaintext.size()),
|
||||
"EVP_EncryptUpdate");
|
||||
assert(ciphertext_length == ciphertext.size() - auth_tag_length);
|
||||
int final_length = 0;
|
||||
openssl_result(EVP_EncryptFinal(ctx.raw(), ciphertext.data() + ciphertext_length, &final_length),
|
||||
"EVP_EncryptFinal");
|
||||
assert(final_length == 0);
|
||||
openssl_result(EVP_CIPHER_CTX_ctrl(ctx.raw(), EVP_CTRL_CCM_GET_TAG, auth_tag_length,
|
||||
ciphertext.data() + ciphertext_length),
|
||||
"EVP_CIPHER_CTX_ctrl(CCM_GET_TAG)");
|
||||
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
ByteBuffer decrypt(const std::uint8_t* ciphertext, std::size_t ciphertext_length) override
|
||||
{
|
||||
if (ciphertext_length < auth_tag_length) {
|
||||
throw std::runtime_error("cipher text is too short");
|
||||
}
|
||||
int plaintext_length = ciphertext_length - auth_tag_length;
|
||||
const std::uint8_t* auth_tag = ciphertext + plaintext_length;
|
||||
|
||||
OpenSslPointer<EVP_CIPHER_CTX> ctx { EVP_CIPHER_CTX_new() };
|
||||
openssl_result(EVP_CIPHER_CTX_init(ctx.raw()), "EVP_CIPHER_CTX_init");
|
||||
openssl_result(EVP_DecryptInit(ctx.raw(), EVP_aes_128_ccm(), nullptr, nullptr), "EVP_DecryptInit(CCM)");
|
||||
openssl_result(EVP_CIPHER_CTX_ctrl(ctx.raw(), EVP_CTRL_CCM_SET_TAG, auth_tag_length,
|
||||
const_cast<std::uint8_t*>(auth_tag)),
|
||||
"EVP_CIPHER_CTX_ctrl(CCM_SET_TAG)");
|
||||
openssl_result(EVP_CIPHER_CTX_ctrl(ctx.raw(), EVP_CTRL_CCM_SET_IVLEN, nonce_length, nullptr),
|
||||
"EVP_CIPHER_CTX_ctrl(CCM_SET_IVLEN)");
|
||||
openssl_result(EVP_DecryptInit(ctx.raw(), nullptr, m_aes_key.data(), m_nonce.data()), "EVP_DecryptInit(key)");
|
||||
|
||||
ByteBuffer plaintext;
|
||||
plaintext.resize(plaintext_length);
|
||||
openssl_result(EVP_DecryptUpdate(ctx.raw(), plaintext.data(), &plaintext_length, ciphertext, plaintext_length),
|
||||
"EVP_DecryptUpdate");
|
||||
assert(plaintext_length == plaintext.size());
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
void calculate_shared_secret(const PublicKey& receiver)
|
||||
{
|
||||
const EC_GROUP* group = EC_KEY_get0_group(m_ephemeral.raw());
|
||||
int degree = EC_GROUP_get_degree(group);
|
||||
m_shared_secret.resize((degree + 7) / 8);
|
||||
int computed = ECDH_compute_key(m_shared_secret.data(), m_shared_secret.size(), make_ec_point(receiver).raw(),
|
||||
m_ephemeral.raw(), nullptr);
|
||||
if (computed != m_shared_secret.size()) {
|
||||
throw OpenSslException(ERR_get_error(), "ECDH_compute_key");
|
||||
}
|
||||
}
|
||||
|
||||
void encrypt_key(const Sha256Hash& info)
|
||||
{
|
||||
const std::size_t ke_length = m_aes_key.size();
|
||||
static constexpr std::size_t km_length = 32;
|
||||
|
||||
// derive ke and km from shared secret
|
||||
ByteBuffer kdp { info.octets.begin(), info.octets.end() };
|
||||
auto hashed_shared_secret = calculate_kdf2(&calculate_sha256_hash, shared_secret(), kdp, ke_length + km_length);
|
||||
assert(hashed_shared_secret.size() == ke_length + km_length);
|
||||
|
||||
// encrypt symmetric key
|
||||
m_encrypted_key.resize(ke_length);
|
||||
std::uint8_t* ke = hashed_shared_secret.data();
|
||||
for (std::size_t i = 0; i < ke_length; ++i) {
|
||||
m_encrypted_key[i] = ke[i] ^ m_aes_key[i];
|
||||
}
|
||||
|
||||
// generate authentication tag
|
||||
ByteBuffer km { std::next(hashed_shared_secret.begin(), ke_length), hashed_shared_secret.end() };
|
||||
assert(km.size() == km_length);
|
||||
m_authentication_tag = calculate_hmac_sha256(km, m_encrypted_key);
|
||||
}
|
||||
|
||||
private:
|
||||
OpenSslPointer<EC_KEY> m_ephemeral;
|
||||
PublicKey m_recipient;
|
||||
ByteBuffer m_shared_secret;
|
||||
ByteBuffer m_aes_key;
|
||||
ByteBuffer m_nonce;
|
||||
ByteBuffer m_encrypted_key;
|
||||
ByteBuffer m_authentication_tag;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
OpenSslSecurityModule::OpenSslSecurityModule(std::shared_ptr<CredentialStorage> cred) : m_credential_storage(cred)
|
||||
{
|
||||
}
|
||||
|
||||
Sha256Hash OpenSslSecurityModule::calculate_sha256_hash(const std::uint8_t* buffer, std::size_t length)
|
||||
{
|
||||
Sha256Hash hash;
|
||||
SHA256(buffer, length, hash.octets.data());
|
||||
return hash;
|
||||
}
|
||||
|
||||
Sha384Hash OpenSslSecurityModule::calculate_sha384_hash(const std::uint8_t* buffer, std::size_t length)
|
||||
{
|
||||
Sha384Hash hash;
|
||||
SHA384(buffer, length, hash.octets.data());
|
||||
return hash;
|
||||
}
|
||||
|
||||
bool OpenSslSecurityModule::verify(const Sha256Hash& digest, const Signature& signature, const PublicKey& pubkey)
|
||||
{
|
||||
return verify(digest.octets.data(), digest.octets.size(), signature, pubkey);
|
||||
}
|
||||
|
||||
bool OpenSslSecurityModule::verify(const Sha384Hash& digest, const Signature& signature, const PublicKey& pubkey)
|
||||
{
|
||||
return verify(digest.octets.data(), digest.octets.size(), signature, pubkey);
|
||||
}
|
||||
|
||||
bool OpenSslSecurityModule::verify(const std::uint8_t* dbuf, std::size_t dlen, const Signature& signature,
|
||||
const PublicKey& pubkey)
|
||||
{
|
||||
if (signature.type == KeyType::Unspecified || pubkey.type == KeyType::Unspecified) {
|
||||
return false;
|
||||
} else if (signature.type != pubkey.type) {
|
||||
return false;
|
||||
} else if (signature.s.empty() || signature.r.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OpenSslPointer<EC_KEY> osl_pubkey = make_ec_key(pubkey);
|
||||
OpenSslPointer<ECDSA_SIG> osl_sig { ECDSA_SIG_new() };
|
||||
BIGNUM* osl_r = BN_bin2bn(signature.r.data(), signature.r.size(), nullptr);
|
||||
BIGNUM* osl_s = BN_bin2bn(signature.s.data(), signature.s.size(), nullptr);
|
||||
if (!osl_r || !osl_s) {
|
||||
BN_free(osl_r); // BN_free(nullptr) is a no-op
|
||||
BN_free(osl_s);
|
||||
return false;
|
||||
}
|
||||
// ownership of big numbers is transfered by calling ECDSA_SIG_set0!
|
||||
openssl_result(ECDSA_SIG_set0(osl_sig.raw(), osl_r, osl_s), "set signature");
|
||||
return (ECDSA_do_verify(dbuf, dlen, osl_sig.raw(), osl_pubkey.raw()) == 1);
|
||||
}
|
||||
|
||||
std::unique_ptr<SecurityModule::EciesContext> OpenSslSecurityModule::create_ecies_context(const PublicKey& receiver,
|
||||
const Sha256Hash& info)
|
||||
{
|
||||
return std::unique_ptr<SecurityModule::EciesContext> { new OpenSslEciesContext { receiver, info } };
|
||||
}
|
||||
|
||||
ByteBuffer OpenSslSecurityModule::generate_nonce(std::size_t length)
|
||||
{
|
||||
ByteBuffer nonce;
|
||||
nonce.resize(length);
|
||||
openssl_result(RAND_bytes(nonce.data(), nonce.size()), "RAND_bytes");
|
||||
return nonce;
|
||||
}
|
||||
|
||||
ByteBuffer OpenSslSecurityModule::calculate_hmac_sha256(const ByteBuffer& key, const ByteBuffer& data)
|
||||
{
|
||||
return pki::calculate_hmac_sha256(key, data);
|
||||
}
|
||||
|
||||
boost::optional<Signature> OpenSslSecurityModule::sign(const ByteBuffer& digest, const PublicKey& pubkey)
|
||||
{
|
||||
boost::optional<PrivateKey> privkey = m_credential_storage->fetch(pubkey);
|
||||
if (privkey) {
|
||||
return sign(digest, *privkey);
|
||||
} else {
|
||||
return boost::none;
|
||||
}
|
||||
}
|
||||
|
||||
boost::optional<Signature> OpenSslSecurityModule::sign(const ByteBuffer& digest, const PrivateKey& privkey)
|
||||
{
|
||||
OpenSslPointer<EC_KEY> osl_key = make_ec_key(privkey);
|
||||
OpenSslPointer<ECDSA_SIG> osl_sig { ECDSA_do_sign(digest.data(), digest.size(), osl_key.raw()) };
|
||||
if (osl_sig.raw()) {
|
||||
const BIGNUM* osl_r = nullptr;
|
||||
const BIGNUM* osl_s = nullptr;
|
||||
ECDSA_SIG_get0(osl_sig.raw(), &osl_r, &osl_s);
|
||||
Signature sig;
|
||||
sig.type = privkey.type;
|
||||
sig.r.resize(BN_num_bytes(osl_r));
|
||||
BN_bn2bin(osl_r, sig.r.data());
|
||||
sig.s.resize(BN_num_bytes(osl_s));
|
||||
BN_bn2bin(osl_s, sig.s.data());
|
||||
return sig;
|
||||
} else {
|
||||
throw OpenSslException(ERR_get_error(), "ECDSA_do_sign");
|
||||
}
|
||||
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
PublicKey OpenSslSecurityModule::create_key(KeyType type)
|
||||
{
|
||||
// create key according to type
|
||||
OpenSslPointer<EC_KEY> ec_key { EC_KEY_new_by_curve_name(openssl_nid(type)) };
|
||||
openssl_result(EC_KEY_generate_key(ec_key.raw()), "EC_KEY_generate_key");
|
||||
openssl_result(EC_KEY_check_key(ec_key.raw()), "EC_KEY_check_key");
|
||||
|
||||
PublicKey pub_key = make_public_key(ec_key.raw());
|
||||
|
||||
// bn_priv must not be freed, returns pointer to internal structure of key
|
||||
const BIGNUM* bn_priv = EC_KEY_get0_private_key(ec_key.raw());
|
||||
if (!bn_priv) {
|
||||
throw security::openssl::Exception();
|
||||
}
|
||||
|
||||
PrivateKey priv_key;
|
||||
priv_key.type = type;
|
||||
priv_key.key.resize(BN_num_bytes(bn_priv));
|
||||
BN_bn2bin(bn_priv, priv_key.key.data());
|
||||
|
||||
// store key pair in credential store
|
||||
m_credential_storage->store(pub_key, priv_key);
|
||||
return pub_key;
|
||||
}
|
||||
|
||||
bool OpenSslSecurityModule::discard_key(const PublicKey& key)
|
||||
{
|
||||
return m_credential_storage->discard(key);
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "security_module.hpp"
|
||||
#include <boost/optional/optional.hpp>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class CredentialStorage;
|
||||
|
||||
class OpenSslSecurityModule : public SecurityModule
|
||||
{
|
||||
public:
|
||||
OpenSslSecurityModule(std::shared_ptr<CredentialStorage>);
|
||||
|
||||
Sha256Hash calculate_sha256_hash(const std::uint8_t* buffer, std::size_t length) override;
|
||||
Sha384Hash calculate_sha384_hash(const std::uint8_t* buffer, std::size_t length) override;
|
||||
bool verify(const Sha256Hash&, const Signature&, const PublicKey&) override;
|
||||
bool verify(const Sha384Hash&, const Signature&, const PublicKey&) override;
|
||||
ByteBuffer generate_nonce(std::size_t length) override;
|
||||
std::unique_ptr<EciesContext> create_ecies_context(const PublicKey& receiver, const Sha256Hash& info) override;
|
||||
ByteBuffer calculate_hmac_sha256(const ByteBuffer& key, const ByteBuffer& data) override;
|
||||
|
||||
boost::optional<Signature> sign(const ByteBuffer& data, const PublicKey&) override;
|
||||
|
||||
/**
|
||||
* Creates a new public and private key pair.
|
||||
* The private key is intentionally not returned but only stored in the credential storage.
|
||||
*/
|
||||
PublicKey create_key(KeyType) override;
|
||||
|
||||
bool discard_key(const PublicKey&) override;
|
||||
|
||||
private:
|
||||
bool verify(const uint8_t* digest, std::size_t dlen, const Signature&, const PublicKey&);
|
||||
boost::optional<Signature> sign(const ByteBuffer&, const PrivateKey&);
|
||||
|
||||
std::shared_ptr<CredentialStorage> m_credential_storage;
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,85 @@
|
||||
#include "pem.hpp"
|
||||
#include "openssl.hpp"
|
||||
#include <openssl/core_names.h>
|
||||
#include <openssl/pem.h>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
boost::optional<PrivateKey> parse_pem_private_key(const void* data, std::size_t size)
|
||||
{
|
||||
OpenSslPointer<BIO> bio { BIO_new(BIO_s_mem()) };
|
||||
int rc = BIO_write(bio.raw(), data, size);
|
||||
if (rc < 0) {
|
||||
throw security::openssl::Exception();
|
||||
}
|
||||
|
||||
EVP_PKEY* evp_key_raw = PEM_read_bio_PrivateKey(bio.raw(), nullptr, nullptr, nullptr);
|
||||
if (evp_key_raw == nullptr) {
|
||||
return boost::none;
|
||||
} else if (EVP_PKEY_get_base_id(evp_key_raw) != EVP_PKEY_EC) {
|
||||
return boost::none;
|
||||
}
|
||||
OpenSslPointer<EVP_PKEY> evp_key { evp_key_raw }; /*< safe deallocation*/
|
||||
|
||||
BIGNUM* bn_priv_raw = nullptr;
|
||||
EVP_PKEY_get_bn_param(evp_key.raw(), OSSL_PKEY_PARAM_PRIV_KEY, &bn_priv_raw);
|
||||
OpenSslPointer<BIGNUM> bn_priv { bn_priv_raw }; /*< safe deallocation */
|
||||
|
||||
char group[80];
|
||||
if (!EVP_PKEY_get_group_name(evp_key.raw(), group, sizeof(group), nullptr)) {
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
PrivateKey priv_key;
|
||||
priv_key.type = openssl_key_type_from_group_name(group);
|
||||
priv_key.key.resize(BN_num_bytes(bn_priv_raw));
|
||||
BN_bn2bin(bn_priv_raw, priv_key.key.data());
|
||||
|
||||
return priv_key;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
boost::optional<PrivateKey> parse_pem_private_key(const std::string& pem)
|
||||
{
|
||||
return parse_pem_private_key(pem.data(), pem.size());
|
||||
}
|
||||
|
||||
boost::optional<PrivateKey> parse_pem_private_key(const ByteBuffer& pem)
|
||||
{
|
||||
return parse_pem_private_key(pem.data(), pem.size());
|
||||
}
|
||||
|
||||
std::string make_pem(const PrivateKey& priv)
|
||||
{
|
||||
OpenSslPointer<EC_KEY> ec_key = make_ec_key(priv);
|
||||
OpenSslPointer<EVP_PKEY> evp_key { EVP_PKEY_new() };
|
||||
openssl_result(EVP_PKEY_set1_EC_KEY(evp_key.raw(), ec_key.raw()), "EVP_PKEY_set1_EC_KEY");
|
||||
|
||||
// unencrypted PKCS#8 PrivateKeyInfo, read back by parse_pem_private_key
|
||||
OpenSslPointer<BIO> bio { BIO_new(BIO_s_mem()) };
|
||||
openssl_result(PEM_write_bio_PrivateKey(bio.raw(), evp_key.raw(), nullptr, nullptr, 0, nullptr, nullptr),
|
||||
"PEM_write_bio_PrivateKey");
|
||||
|
||||
char* data = nullptr;
|
||||
long len = BIO_get_mem_data(bio.raw(), &data);
|
||||
return std::string(data, len);
|
||||
}
|
||||
|
||||
void write_pem_private_key(const PrivateKey& priv, const std::filesystem::path& path)
|
||||
{
|
||||
std::string pem = make_pem(priv);
|
||||
auto bio = make_owner_only_bio(path);
|
||||
if (BIO_write(bio.raw(), pem.data(), pem.size()) != static_cast<int>(pem.size())) {
|
||||
throw OpenSslException(ERR_get_error(), "writing PEM to key file failed");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "keys.hpp"
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
boost::optional<PrivateKey> parse_pem_private_key(const std::string&);
|
||||
boost::optional<PrivateKey> parse_pem_private_key(const ByteBuffer&);
|
||||
|
||||
/**
|
||||
* Encode an EC private key as unencrypted PKCS#8 PEM.
|
||||
* Round-trips through parse_pem_private_key.
|
||||
*/
|
||||
std::string make_pem(const PrivateKey&);
|
||||
|
||||
/**
|
||||
* Encode `priv` as PEM and write it to `path`.
|
||||
*
|
||||
* \throws std::runtime_error if the file cannot be created
|
||||
* \throws OpenSslException on write failure
|
||||
*/
|
||||
void write_pem_private_key(const PrivateKey& priv, const std::filesystem::path& path);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,154 @@
|
||||
#include "printing.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "exception.hpp"
|
||||
#include <vanetza/asn1/asn1c_wrapper.hpp>
|
||||
#include <vanetza/asn1/security/EtsiTs102941Data.h>
|
||||
#include <vanetza/asn1/security/EtsiTs103097Certificate.h>
|
||||
#include <vanetza/asn1/security/EtsiTs103097Data.h>
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct Context
|
||||
{
|
||||
std::string input_file;
|
||||
bool input_stdin = false;
|
||||
bool print_role = false; // certificate only
|
||||
};
|
||||
|
||||
const char* role_token(CertificateRole role)
|
||||
{
|
||||
switch (role) {
|
||||
case CertificateRole::RootCa:
|
||||
return "root-ca";
|
||||
case CertificateRole::EnrolmentAuthority:
|
||||
return "enrolment-authority";
|
||||
case CertificateRole::AuthorizationAuthority:
|
||||
return "authorization-authority";
|
||||
case CertificateRole::EnrolmentCredential:
|
||||
return "enrolment-credential";
|
||||
case CertificateRole::AuthorizationTicket:
|
||||
return "authorization-ticket";
|
||||
case CertificateRole::Tlm:
|
||||
return "tlm";
|
||||
case CertificateRole::Unknown:
|
||||
return "unknown";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
// Read the OER bytes from the chosen input source, or boost::none if neither
|
||||
// was given (should not happen: the input-source group requires exactly one).
|
||||
boost::optional<ByteBuffer> read_input(const Context& ctx)
|
||||
{
|
||||
ByteBuffer input;
|
||||
if (!ctx.input_file.empty()) {
|
||||
std::ifstream ifs(ctx.input_file, std::ios::binary);
|
||||
std::istreambuf_iterator<char> begin(ifs), end;
|
||||
input.assign(begin, end);
|
||||
} else if (ctx.input_stdin) {
|
||||
std::istreambuf_iterator<char> begin(std::cin), end;
|
||||
input.assign(begin, end);
|
||||
} else {
|
||||
return boost::none;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
void add_input_source(CLI::App& app, Context& ctx)
|
||||
{
|
||||
auto group = app.add_option_group("input source", "read data from the given source");
|
||||
group->add_option("-f,--file", ctx.input_file, "filename")->check(CLI::ExistingFile);
|
||||
group->add_flag("--stdin", ctx.input_stdin, "read from stdin");
|
||||
group->require_option(1);
|
||||
}
|
||||
|
||||
void print_certificate(const ByteBuffer& input, bool role)
|
||||
{
|
||||
if (role) {
|
||||
Certificate cert;
|
||||
if (cert.decode(input)) {
|
||||
std::cout << role_token(certificate_role(cert)) << "\n";
|
||||
} else {
|
||||
throw DecodingFailure("EtsiTs103097Certificate");
|
||||
}
|
||||
return;
|
||||
}
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs103097Certificate_t>
|
||||
cert(asn_DEF_Vanetza_Security_EtsiTs103097Certificate);
|
||||
if (cert.decode(input)) {
|
||||
xer_fprint(stdout, &asn_DEF_Vanetza_Security_EtsiTs103097Certificate, &*cert);
|
||||
} else {
|
||||
throw DecodingFailure("EtsiTs103097Certificate");
|
||||
}
|
||||
}
|
||||
|
||||
void print_data(const ByteBuffer& input)
|
||||
{
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs103097Data_t> data(asn_DEF_Vanetza_Security_EtsiTs103097Data);
|
||||
if (data.decode(input)) {
|
||||
xer_fprint(stdout, &asn_DEF_Vanetza_Security_EtsiTs103097Data, &*data);
|
||||
} else {
|
||||
throw DecodingFailure("EtsiTs103097Data");
|
||||
}
|
||||
}
|
||||
|
||||
void print_mgmt_data(const ByteBuffer& input)
|
||||
{
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> data(asn_DEF_Vanetza_Security_EtsiTs102941Data);
|
||||
if (data.decode(input)) {
|
||||
xer_fprint(stdout, &asn_DEF_Vanetza_Security_EtsiTs102941Data, &*data);
|
||||
} else {
|
||||
throw DecodingFailure("EtsiTs102941Data");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::shared_ptr<CLI::App> build_print_command()
|
||||
{
|
||||
auto ctx = std::make_shared<Context>();
|
||||
auto app = std::make_shared<CLI::App>("printing ASN.1 OER encoded certificates, trust lists and alike", "print");
|
||||
|
||||
auto certificate = app->add_subcommand("certificate", "EtsiTs103097Certificate");
|
||||
certificate->alias("cert");
|
||||
add_input_source(*certificate, *ctx);
|
||||
certificate->add_flag("--role", ctx->print_role, "print the certificate's PKI role instead of its content");
|
||||
certificate->callback([ctx]() {
|
||||
if (auto input = read_input(*ctx)) {
|
||||
print_certificate(*input, ctx->print_role);
|
||||
}
|
||||
});
|
||||
|
||||
auto data = app->add_subcommand("data", "EtsiTs103097Data");
|
||||
add_input_source(*data, *ctx);
|
||||
data->callback([ctx]() {
|
||||
if (auto input = read_input(*ctx)) {
|
||||
print_data(*input);
|
||||
}
|
||||
});
|
||||
|
||||
auto mgmt = app->add_subcommand("mgmt-data", "EtsiTs102941Data");
|
||||
add_input_source(*mgmt, *ctx);
|
||||
mgmt->callback([ctx]() {
|
||||
if (auto input = read_input(*ctx)) {
|
||||
print_mgmt_data(*input);
|
||||
}
|
||||
});
|
||||
|
||||
app->require_subcommand(1);
|
||||
return app;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "CLI/App.hpp"
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
std::shared_ptr<CLI::App> build_print_command();
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,384 @@
|
||||
#include "prune_command.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "certificate_storage.hpp"
|
||||
#include "certificate_trust_list.hpp"
|
||||
#include "credential_storage.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "security_module.hpp"
|
||||
#include "station_config.hpp"
|
||||
#include "time.hpp"
|
||||
#include "trust_list_storage.hpp"
|
||||
#include <vanetza/security/public_key.hpp>
|
||||
#include <boost/date_time/posix_time/posix_time_io.hpp>
|
||||
#include <boost/range/algorithm_ext/push_back.hpp>
|
||||
#include <iostream>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Stream expired certs from one store to the visitor. Constant memory.
|
||||
void scan_store_for_expired(CertificateStorage& store, const std::string& label, Clock::time_point now,
|
||||
const StationConfiguration& station, bool force, PruneExpiredVisitor& visitor)
|
||||
{
|
||||
auto station_ec = station.get_ec_identifier();
|
||||
auto station_root_ca = station.get_root_ca();
|
||||
|
||||
for (const auto& id : store.list()) {
|
||||
auto cert = store.fetch(id);
|
||||
if (!cert) {
|
||||
std::cerr << "warning: " << label << " " << hexstring(id) << " could not be decoded; ignoring for prune\n";
|
||||
continue;
|
||||
} else if (cert->valid_until() >= now) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool station_ref = (station_ec && *station_ec == id) || (station_root_ca && *station_root_ca == id);
|
||||
|
||||
if (station_ref && !force) {
|
||||
visitor.on_skipped(label, id, *cert);
|
||||
} else {
|
||||
std::string pubkey_hex = security::canonical_hexstring(cert->get_public_key());
|
||||
visitor.on_deletable(store, label, id, *cert, pubkey_hex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect canonical-hex public keys from one store into the set.
|
||||
void collect_pubkey_hexes_from_store(const CertificateStorage& store, const char* label,
|
||||
std::unordered_set<std::string>& out)
|
||||
{
|
||||
for (const auto& id : store.list()) {
|
||||
auto cert = store.fetch(id);
|
||||
if (!cert) {
|
||||
std::cerr << "warning: " << label << " " << hexstring(id) << " could not be decoded; ignoring for prune\n";
|
||||
continue;
|
||||
}
|
||||
std::string hex = security::canonical_hexstring(cert->get_public_key());
|
||||
if (!hex.empty()) {
|
||||
out.insert(std::move(hex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Format a cert entry for printing in expired-related output.
|
||||
std::string format_expired_entry(const std::string& label, const HashedId8& id, const Certificate& cert)
|
||||
{
|
||||
std::ostringstream os;
|
||||
os.imbue(std::cout.getloc());
|
||||
os << "[" << label << "] " << hexstring(id);
|
||||
const std::string name = cert.get_name();
|
||||
if (!name.empty()) {
|
||||
os << " \"" << name << "\"";
|
||||
}
|
||||
os << " expired " << Clock::at(cert.valid_until());
|
||||
return os.str();
|
||||
}
|
||||
|
||||
class DryRunPrintExpiredVisitor : public PruneExpiredVisitor
|
||||
{
|
||||
public:
|
||||
explicit DryRunPrintExpiredVisitor(const CredentialStorage* credentials) : m_credentials(credentials)
|
||||
{
|
||||
}
|
||||
|
||||
void on_deletable(CertificateStorage&, const std::string& label, const HashedId8& id, const Certificate& cert,
|
||||
const std::string& pubkey_hex) override
|
||||
{
|
||||
std::cout << " " << format_expired_entry(label, id, cert) << "\n";
|
||||
if (m_credentials && m_credentials->contains(pubkey_hex)) {
|
||||
std::cout << " + would discard credential " << pubkey_hex << "\n";
|
||||
}
|
||||
++m_deletable;
|
||||
}
|
||||
|
||||
void on_skipped(const std::string& label, const HashedId8& id, const Certificate& cert) override
|
||||
{
|
||||
std::cout << " " << format_expired_entry(label, id, cert) << " [station-referenced, skipped — use --force]\n";
|
||||
++m_skipped;
|
||||
}
|
||||
|
||||
void on_summary() override
|
||||
{
|
||||
std::cout << "Would prune " << m_deletable << " certificate(s)";
|
||||
if (m_skipped) {
|
||||
std::cout << "; " << m_skipped << " skipped";
|
||||
}
|
||||
std::cout << ".\n";
|
||||
}
|
||||
|
||||
private:
|
||||
const CredentialStorage* m_credentials;
|
||||
std::size_t m_deletable = 0;
|
||||
std::size_t m_skipped = 0;
|
||||
};
|
||||
|
||||
class ApplyExpiredVisitor : public PruneExpiredVisitor
|
||||
{
|
||||
public:
|
||||
explicit ApplyExpiredVisitor(CredentialStorage* credentials) : m_credentials(credentials)
|
||||
{
|
||||
}
|
||||
|
||||
void on_deletable(CertificateStorage& store, const std::string& label, const HashedId8& id, const Certificate&,
|
||||
const std::string& pubkey_hex) override
|
||||
{
|
||||
if (store.erase(id)) {
|
||||
std::cout << "Removed " << label << " " << hexstring(id) << "\n";
|
||||
++m_removed;
|
||||
if (m_credentials && m_credentials->discard(pubkey_hex)) {
|
||||
std::cout << "Removed credential " << pubkey_hex << "\n";
|
||||
++m_removed_creds;
|
||||
}
|
||||
} else {
|
||||
std::cerr << "warning: failed to erase " << label << " " << hexstring(id) << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
void on_skipped(const std::string& label, const HashedId8& id, const Certificate&) override
|
||||
{
|
||||
std::cout << "Skipped " << label << " " << hexstring(id)
|
||||
<< " — referenced by station config (--force to remove)\n";
|
||||
}
|
||||
|
||||
void on_summary() override
|
||||
{
|
||||
std::cout << "Pruned " << m_removed << " certificate(s)";
|
||||
if (m_credentials) {
|
||||
std::cout << " and " << m_removed_creds << " credential(s)";
|
||||
}
|
||||
std::cout << ".\n";
|
||||
}
|
||||
|
||||
private:
|
||||
CredentialStorage* m_credentials;
|
||||
std::size_t m_removed = 0;
|
||||
std::size_t m_removed_creds = 0;
|
||||
};
|
||||
|
||||
class DryRunPrintOrphansVisitor : public PruneOrphansVisitor
|
||||
{
|
||||
public:
|
||||
void on_orphan(const std::string& canonical_hex) override
|
||||
{
|
||||
std::cout << " " << canonical_hex << "\n";
|
||||
++m_count;
|
||||
}
|
||||
|
||||
void on_keep(const std::string& canonical_hex) override
|
||||
{
|
||||
++m_keep;
|
||||
}
|
||||
|
||||
void on_summary() override
|
||||
{
|
||||
std::cout << "Would prune " << m_count << " orphan and keep " << m_keep << " credential(s).\n";
|
||||
}
|
||||
|
||||
private:
|
||||
std::size_t m_count = 0;
|
||||
std::size_t m_keep = 0;
|
||||
};
|
||||
|
||||
class ApplyOrphansVisitor : public PruneOrphansVisitor
|
||||
{
|
||||
public:
|
||||
explicit ApplyOrphansVisitor(const MainConfig& cfg) : m_cfg(cfg)
|
||||
{
|
||||
}
|
||||
|
||||
void on_orphan(const std::string& canonical_hex) override
|
||||
{
|
||||
if (m_cfg.credentials->discard(canonical_hex)) {
|
||||
std::cout << "Removed orphan credential " << canonical_hex << "\n";
|
||||
++m_removed;
|
||||
} else {
|
||||
std::cerr << "warning: failed to discard orphan credential " << canonical_hex << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
void on_summary() override
|
||||
{
|
||||
std::cout << "Pruned " << m_removed << " orphan credential(s).\n";
|
||||
}
|
||||
|
||||
private:
|
||||
const MainConfig& m_cfg;
|
||||
std::size_t m_removed = 0;
|
||||
};
|
||||
|
||||
// Collects the HashedId8 of every AA/EA certificate encountered while visiting RCA CTLs.
|
||||
class IssuerCollector : public CtlVisitor
|
||||
{
|
||||
public:
|
||||
explicit IssuerCollector(SecurityModule& security) : m_security(security)
|
||||
{
|
||||
}
|
||||
|
||||
void add_authorization_authority(const Vanetza_Security_AaEntry_t& entry) override
|
||||
{
|
||||
m_aa.insert(Certificate(entry.aaCertificate).calculate_hashed_id8(m_security));
|
||||
}
|
||||
|
||||
void add_enrolment_authority(const Vanetza_Security_EaEntry_t& entry) override
|
||||
{
|
||||
m_ea.insert(Certificate(entry.eaCertificate).calculate_hashed_id8(m_security));
|
||||
}
|
||||
|
||||
const std::set<HashedId8>& authorization_authorities() const { return m_aa; }
|
||||
const std::set<HashedId8>& enrolment_authorities() const { return m_ea; }
|
||||
|
||||
private:
|
||||
SecurityModule& m_security;
|
||||
std::set<HashedId8> m_aa;
|
||||
std::set<HashedId8> m_ea;
|
||||
};
|
||||
|
||||
// Remove every cert in `store` whose HashedId8 is not in `keep`. Returns the number pruned.
|
||||
std::size_t reconcile_issuer_store(CertificateStorage& store, const std::set<HashedId8>& keep, const char* label,
|
||||
bool dry_run)
|
||||
{
|
||||
std::vector<HashedId8> ids;
|
||||
boost::push_back(ids, store.list());
|
||||
std::size_t count = 0;
|
||||
for (const auto& id : ids) {
|
||||
if (keep.find(id) != keep.end()) {
|
||||
continue;
|
||||
}
|
||||
if (dry_run) {
|
||||
std::cout << " would remove " << label << " " << hexstring(id) << "\n";
|
||||
++count;
|
||||
} else if (store.erase(id)) {
|
||||
std::cout << "Removed " << label << " " << hexstring(id) << "\n";
|
||||
++count;
|
||||
} else {
|
||||
std::cerr << "warning: failed to erase " << label << " " << hexstring(id) << "\n";
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void prune_ctl(const MainConfig& cfg, bool dry_run)
|
||||
{
|
||||
// Build the current set of trusted issuer certs from the CTLs stored per Root CA.
|
||||
IssuerCollector collector(*cfg.security);
|
||||
for (const auto& rca : cfg.root_ca->list()) {
|
||||
auto ctl = cfg.trust_lists->fetch(rca);
|
||||
if (!ctl) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
ctl->visit_rca_ctl(collector);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "warning: CTL for " << hexstring(rca) << " could not be parsed: " << e.what() << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t count = 0;
|
||||
count += reconcile_issuer_store(*cfg.authorization_authorities, collector.authorization_authorities(), "AA", dry_run);
|
||||
count += reconcile_issuer_store(*cfg.enrolment_authorities, collector.enrolment_authorities(), "EA", dry_run);
|
||||
std::cout << (dry_run ? "Would prune " : "Pruned ") << count << " issuer certificate(s).\n";
|
||||
}
|
||||
|
||||
void prune_expired(const MainConfig& cfg, Clock::time_point now, bool force, PruneExpiredVisitor& visitor)
|
||||
{
|
||||
scan_store_for_expired(*cfg.root_ca, "Root CA", now, *cfg.station, force, visitor);
|
||||
scan_store_for_expired(*cfg.enrolment_credentials, "EC", now, *cfg.station, force, visitor);
|
||||
scan_store_for_expired(*cfg.tlm, "TLM", now, *cfg.station, force, visitor);
|
||||
scan_store_for_expired(*cfg.tickets, "AT", now, *cfg.station, force, visitor);
|
||||
visitor.on_summary();
|
||||
}
|
||||
|
||||
void prune_expired_tickets(const MainConfig& cfg, Clock::time_point now, bool dry_run)
|
||||
{
|
||||
CredentialStorage* credentials = cfg.credentials.get();
|
||||
if (dry_run) {
|
||||
std::cout << "Expired authorization tickets:\n";
|
||||
DryRunPrintExpiredVisitor visitor(credentials);
|
||||
scan_store_for_expired(*cfg.tickets, "AT", now, *cfg.station, true, visitor);
|
||||
visitor.on_summary();
|
||||
} else {
|
||||
ApplyExpiredVisitor visitor(credentials);
|
||||
scan_store_for_expired(*cfg.tickets, "AT", now, *cfg.station, true, visitor);
|
||||
visitor.on_summary();
|
||||
}
|
||||
}
|
||||
|
||||
void prune_orphans(const MainConfig& cfg, PruneOrphansVisitor& visitor)
|
||||
{
|
||||
std::unordered_set<std::string> referenced;
|
||||
collect_pubkey_hexes_from_store(*cfg.enrolment_credentials, "EC", referenced);
|
||||
collect_pubkey_hexes_from_store(*cfg.tickets, "AT", referenced);
|
||||
|
||||
for (const auto& name : cfg.credentials->list()) {
|
||||
if (referenced.find(name) == referenced.end()) {
|
||||
visitor.on_orphan(name);
|
||||
} else {
|
||||
visitor.on_keep(name);
|
||||
}
|
||||
}
|
||||
visitor.on_summary();
|
||||
}
|
||||
|
||||
std::shared_ptr<CLI::App> build_prune_command(const MainConfig& cfg)
|
||||
{
|
||||
auto app = std::make_shared<CLI::App>("housekeeping for stored credentials and certificates", "prune");
|
||||
|
||||
auto orphans = app->add_subcommand("orphans", "remove credential files not referenced by any certificate");
|
||||
auto orphans_dry = orphans->add_flag("--dry-run,-n", "list only; do not delete");
|
||||
orphans->callback([&cfg, orphans_dry]() {
|
||||
if (orphans_dry->as<bool>()) {
|
||||
std::cout << "Orphan credentials:\n";
|
||||
DryRunPrintOrphansVisitor v;
|
||||
prune_orphans(cfg, v);
|
||||
} else {
|
||||
ApplyOrphansVisitor v(cfg);
|
||||
prune_orphans(cfg, v);
|
||||
}
|
||||
});
|
||||
|
||||
auto expired = app->add_subcommand("expired", "remove expired certificates");
|
||||
auto expired_dry = expired->add_flag("--dry-run,-n", "list only; do not delete");
|
||||
auto expired_force = expired->add_flag("--force", "also remove certs referenced by station config (EC, Root CA)");
|
||||
auto expired_keep =
|
||||
expired->add_flag("--keep-credentials", "keep corresponding credential when removing certificate");
|
||||
expired->callback([&cfg, expired_dry, expired_force, expired_keep]() {
|
||||
// Null pointer signals "do not touch credentials" (--keep-credentials).
|
||||
CredentialStorage* credentials = expired_keep->as<bool>() ? nullptr : cfg.credentials.get();
|
||||
const bool force = expired_force->as<bool>();
|
||||
if (expired_dry->as<bool>()) {
|
||||
std::cout << "Expired certificates:\n";
|
||||
DryRunPrintExpiredVisitor v(credentials);
|
||||
prune_expired(cfg, current_time(), force, v);
|
||||
} else {
|
||||
ApplyExpiredVisitor v(credentials);
|
||||
prune_expired(cfg, current_time(), force, v);
|
||||
}
|
||||
});
|
||||
|
||||
auto ctl = app->add_subcommand("ctl", "remove exported AA/EA issuer certs no longer present in stored CTLs");
|
||||
auto ctl_dry = ctl->add_flag("--dry-run,-n", "list only; do not delete");
|
||||
ctl->callback([&cfg, ctl_dry]() {
|
||||
if (ctl_dry->as<bool>()) {
|
||||
std::cout << "Stale issuer certificates:\n";
|
||||
}
|
||||
prune_ctl(cfg, ctl_dry->as<bool>());
|
||||
});
|
||||
|
||||
app->require_subcommand(1);
|
||||
return app;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,110 @@
|
||||
#pragma once
|
||||
|
||||
#include "certificate.hpp"
|
||||
#include "certificate_storage.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "main.hpp"
|
||||
#include <vanetza/common/clock.hpp>
|
||||
#include <CLI/CLI.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
/**
|
||||
* Visitor that observes expired certificates streamed by `prune_expired`.
|
||||
* Methods are called once per certificate as it is encountered. The driver
|
||||
* performs no per-cert buffering.
|
||||
*/
|
||||
class PruneExpiredVisitor
|
||||
{
|
||||
public:
|
||||
virtual ~PruneExpiredVisitor() = default;
|
||||
|
||||
/**
|
||||
* An expired certificate eligible for deletion.
|
||||
*
|
||||
* \param store backing store holding the cert; the visitor erases from it if needed
|
||||
* \param label short role tag ("Root CA", "EC", "AT", "TLM") for output
|
||||
* \param pubkey_hex canonical-hex name of the corresponding credential
|
||||
*/
|
||||
virtual void on_deletable(CertificateStorage& store, const std::string& label, const HashedId8&, const Certificate&,
|
||||
const std::string& pubkey_hex)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* An expired certificate that is being skipped because it is referenced
|
||||
* by station configuration and `--force` was not specified.
|
||||
*/
|
||||
virtual void on_skipped(const std::string& label, const HashedId8&, const Certificate&)
|
||||
{
|
||||
}
|
||||
|
||||
/// Called once after all stores have been fully iterated.
|
||||
virtual void on_summary()
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stream every expired certificate in the cert and ticket stores through the
|
||||
* visitor. Memory is constant — no per-cert state is retained.
|
||||
*
|
||||
* \param now cutoff for expiry: `cert.valid_until() < now` ⇒ expired
|
||||
* \param force if true, station-referenced certs go to `on_deletable` instead
|
||||
* of `on_skipped`
|
||||
*/
|
||||
void prune_expired(const MainConfig& cfg, Clock::time_point now, bool force, PruneExpiredVisitor& visitor);
|
||||
|
||||
/**
|
||||
* Prune expired authorization tickets and their credentials.
|
||||
* `dry_run` lists without deleting.
|
||||
*/
|
||||
void prune_expired_tickets(const MainConfig& cfg, Clock::time_point now, bool dry_run);
|
||||
|
||||
/// Visitor for `prune_orphans`. Same streaming pattern.
|
||||
class PruneOrphansVisitor
|
||||
{
|
||||
public:
|
||||
virtual ~PruneOrphansVisitor() = default;
|
||||
|
||||
/// An orphan credential eligible for deletion, by canonical-hex name.
|
||||
virtual void on_orphan(const std::string& canonical_hex)
|
||||
{
|
||||
}
|
||||
|
||||
/// A credential retained because a certificate still references it.
|
||||
virtual void on_keep(const std::string& canonical_hex)
|
||||
{
|
||||
}
|
||||
|
||||
/// Called once after all credentials have been iterated.
|
||||
virtual void on_summary()
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stream every orphan credential through the visitor.
|
||||
* A credential is orphan when its canonical-hex name does not match any public key
|
||||
* referenced by a certificate in either store.
|
||||
*/
|
||||
void prune_orphans(const MainConfig& cfg, PruneOrphansVisitor& visitor);
|
||||
|
||||
/**
|
||||
* Reconcile the exported AA/EA issuer certificates against the currently stored
|
||||
* CTLs: remove any `.aa`/`.ea` cert whose HashedId8 is not present in the latest
|
||||
* stored CTL of any trusted Root CA. Prevents issuer certs orphaned by CTL
|
||||
* rotation from accumulating. `dry_run` lists without deleting.
|
||||
*/
|
||||
void prune_ctl(const MainConfig& cfg, bool dry_run);
|
||||
|
||||
// CLI subcommand tree.
|
||||
std::shared_ptr<CLI::App> build_prune_command(const MainConfig&);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,40 @@
|
||||
#include "psid_ssp.hpp"
|
||||
#include <boost/algorithm/hex.hpp>
|
||||
#include <charconv>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
bool lexical_cast(const std::string& input, PsidSsp& permission)
|
||||
{
|
||||
// split into the decimal PSID and the optional ':'-separated hex SSP.
|
||||
const std::size_t colon = input.find(':');
|
||||
const char* first = input.data();
|
||||
const char* last = input.data() + (colon == std::string::npos ? input.size() : colon);
|
||||
|
||||
decltype(permission.psid) psid = 0;
|
||||
auto [ptr, ec] = std::from_chars(first, last, psid);
|
||||
if (ec != std::errc {} || ptr != last) {
|
||||
// empty, contains a non-digit, or out of range
|
||||
return false;
|
||||
}
|
||||
|
||||
// found SSP part
|
||||
if (colon != std::string::npos) {
|
||||
try {
|
||||
ByteBuffer ssp;
|
||||
boost::algorithm::unhex(input.begin() + colon + 1, input.end(), std::back_inserter(ssp));
|
||||
permission.ssp = std::move(ssp);
|
||||
} catch (boost::algorithm::hex_decode_error&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
permission.psid = psid;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
struct PsidSsp
|
||||
{
|
||||
std::uintmax_t psid;
|
||||
ByteBuffer ssp;
|
||||
};
|
||||
|
||||
bool lexical_cast(const std::string&, PsidSsp&);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,106 @@
|
||||
#include "response_codes.hpp"
|
||||
#include <string>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
std::string to_string(EnrolmentResponseCode code)
|
||||
{
|
||||
switch (code.value) {
|
||||
case Vanetza_Security_EnrolmentResponseCode_ok:
|
||||
return "ok";
|
||||
case Vanetza_Security_EnrolmentResponseCode_badcontenttype:
|
||||
return "bad content type";
|
||||
case Vanetza_Security_EnrolmentResponseCode_baditsstatus:
|
||||
return "bad ITS status";
|
||||
case Vanetza_Security_EnrolmentResponseCode_cantparse:
|
||||
return "cannot parse";
|
||||
case Vanetza_Security_EnrolmentResponseCode_decryptionfailed:
|
||||
return "decryption failed";
|
||||
case Vanetza_Security_EnrolmentResponseCode_deniedpermissions:
|
||||
return "denied permissions";
|
||||
case Vanetza_Security_EnrolmentResponseCode_deniedrequest:
|
||||
return "denied request";
|
||||
case Vanetza_Security_EnrolmentResponseCode_imnottherecipient:
|
||||
return "I am not the recipient";
|
||||
case Vanetza_Security_EnrolmentResponseCode_invalidencryptionkey:
|
||||
return "invalid encryption key";
|
||||
case Vanetza_Security_EnrolmentResponseCode_invalidkeys:
|
||||
return "invalid keys";
|
||||
case Vanetza_Security_EnrolmentResponseCode_invalidsignature:
|
||||
return "invalid signature";
|
||||
case Vanetza_Security_EnrolmentResponseCode_unknownencryptionalgorithm:
|
||||
return "unknown encryption algorithm";
|
||||
case Vanetza_Security_EnrolmentResponseCode_unknownits:
|
||||
return "unknown ITS";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::string to_string(AuthorizationResponseCode code)
|
||||
{
|
||||
switch (code.value) {
|
||||
case Vanetza_Security_AuthorizationResponseCode_ok:
|
||||
return "ok";
|
||||
case Vanetza_Security_AuthorizationResponseCode_its_aa_cantparse:
|
||||
return "its-aa: cannot parse";
|
||||
case Vanetza_Security_AuthorizationResponseCode_its_aa_badcontenttype:
|
||||
return "its-aa: bad content type";
|
||||
case Vanetza_Security_AuthorizationResponseCode_its_aa_imnottherecipient:
|
||||
return "its-aa: not the recipient";
|
||||
case Vanetza_Security_AuthorizationResponseCode_its_aa_unknownencryptionalgorithm:
|
||||
return "its-aa: unknown encryption algorithm";
|
||||
case Vanetza_Security_AuthorizationResponseCode_its_aa_decryptionfailed:
|
||||
return "its-aa: decryption failed";
|
||||
case Vanetza_Security_AuthorizationResponseCode_its_aa_keysdontmatch:
|
||||
return "its-aa: keys don't match";
|
||||
case Vanetza_Security_AuthorizationResponseCode_its_aa_incompleterequest:
|
||||
return "its-aa: incomplete request";
|
||||
case Vanetza_Security_AuthorizationResponseCode_its_aa_invalidencryptionkey:
|
||||
return "its-aa: invalid encryption key";
|
||||
case Vanetza_Security_AuthorizationResponseCode_its_aa_outofsyncrequest:
|
||||
return "its-aa: out-of-sync request";
|
||||
case Vanetza_Security_AuthorizationResponseCode_its_aa_unknownea:
|
||||
return "its-aa: unknown EA";
|
||||
case Vanetza_Security_AuthorizationResponseCode_its_aa_invalidea:
|
||||
return "its-aa: invalid EA";
|
||||
case Vanetza_Security_AuthorizationResponseCode_its_aa_deniedpermissions:
|
||||
return "its-aa: denied permissions";
|
||||
case Vanetza_Security_AuthorizationResponseCode_aa_ea_cantreachea:
|
||||
return "aa-ea: cannot reach EA";
|
||||
case Vanetza_Security_AuthorizationResponseCode_ea_aa_cantparse:
|
||||
return "ea-aa: cannot parse";
|
||||
case Vanetza_Security_AuthorizationResponseCode_ea_aa_badcontenttype:
|
||||
return "ea-aa: bad content type";
|
||||
case Vanetza_Security_AuthorizationResponseCode_ea_aa_imnottherecipient:
|
||||
return "ea-aa: not the recipient";
|
||||
case Vanetza_Security_AuthorizationResponseCode_ea_aa_unknownencryptionalgorithm:
|
||||
return "ea-aa: unknown encryption algorithm";
|
||||
case Vanetza_Security_AuthorizationResponseCode_ea_aa_decryptionfailed:
|
||||
return "ea-aa: decryption failed";
|
||||
case Vanetza_Security_AuthorizationResponseCode_invalidaa:
|
||||
return "invalid AA";
|
||||
case Vanetza_Security_AuthorizationResponseCode_invalidaasignature:
|
||||
return "invalid AA signature";
|
||||
case Vanetza_Security_AuthorizationResponseCode_wrongea:
|
||||
return "wrong EA";
|
||||
case Vanetza_Security_AuthorizationResponseCode_unknownits:
|
||||
return "unknown ITS";
|
||||
case Vanetza_Security_AuthorizationResponseCode_invalidsignature:
|
||||
return "invalid signature";
|
||||
case Vanetza_Security_AuthorizationResponseCode_invalidencryptionkey:
|
||||
return "invalid encryption key";
|
||||
case Vanetza_Security_AuthorizationResponseCode_deniedpermissions:
|
||||
return "denied permissions";
|
||||
case Vanetza_Security_AuthorizationResponseCode_deniedtoomanycerts:
|
||||
return "denied: too many certificates";
|
||||
default:
|
||||
return "unknown(" + std::to_string(static_cast<long>(code.value)) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "asn1.hpp"
|
||||
#include <vanetza/asn1/security/AuthorizationResponseCode.h>
|
||||
#include <vanetza/asn1/security/EnrolmentResponseCode.h>
|
||||
#include <string>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
using EnrolmentResponseCode = asn1c_enum<enum Vanetza_Security_EnrolmentResponseCode>;
|
||||
using AuthorizationResponseCode = asn1c_enum<enum Vanetza_Security_AuthorizationResponseCode>;
|
||||
|
||||
std::string to_string(EnrolmentResponseCode);
|
||||
std::string to_string(AuthorizationResponseCode);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,15 @@
|
||||
#include "security_module.hpp"
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
bool SecurityModule::can_sign(const PublicKey& key)
|
||||
{
|
||||
static const ByteBuffer probe { 0x12, 0x34, 0x56, 0x78 };
|
||||
return sign(probe, key).has_value();
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,90 @@
|
||||
#pragma once
|
||||
|
||||
#include "keys.hpp"
|
||||
#include "sha.hpp"
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class SecurityModule
|
||||
{
|
||||
public:
|
||||
class EciesContext
|
||||
{
|
||||
public:
|
||||
virtual ~EciesContext() = default;
|
||||
virtual PublicKey ephemeral_public_key() const = 0;
|
||||
virtual PublicKey recipient_public_key() const = 0;
|
||||
virtual ByteBuffer shared_secret() const = 0;
|
||||
virtual ByteBuffer encrypted_key() const = 0;
|
||||
virtual ByteBuffer authentication_tag() const = 0;
|
||||
virtual ByteBuffer nonce() const = 0;
|
||||
virtual void nonce(const ByteBuffer&) = 0;
|
||||
|
||||
virtual ByteBuffer encrypt(const ByteBuffer& plaintext) = 0;
|
||||
virtual ByteBuffer decrypt(const std::uint8_t* buffer, std::size_t length) = 0;
|
||||
};
|
||||
|
||||
virtual ~SecurityModule() = default;
|
||||
virtual Sha256Hash calculate_sha256_hash(const std::uint8_t* buffer, std::size_t length) = 0;
|
||||
virtual Sha384Hash calculate_sha384_hash(const std::uint8_t* buffer, std::size_t length) = 0;
|
||||
virtual bool verify(const Sha256Hash&, const Signature&, const PublicKey&) = 0;
|
||||
virtual bool verify(const Sha384Hash&, const Signature&, const PublicKey&) = 0;
|
||||
|
||||
virtual boost::optional<Signature> sign(const ByteBuffer& digest, const PublicKey&) = 0;
|
||||
|
||||
// True if the private key matching `key` is available for signing.
|
||||
virtual bool can_sign(const PublicKey& key);
|
||||
|
||||
virtual PublicKey create_key(KeyType) = 0;
|
||||
virtual bool discard_key(const PublicKey&) = 0;
|
||||
|
||||
virtual ByteBuffer generate_nonce(std::size_t length) = 0;
|
||||
virtual std::unique_ptr<EciesContext> create_ecies_context(const PublicKey& receiver, const Sha256Hash& info) = 0;
|
||||
virtual ByteBuffer calculate_hmac_sha256(const ByteBuffer& key, const ByteBuffer& data) = 0;
|
||||
};
|
||||
|
||||
class ScopedKeyPair
|
||||
{
|
||||
public:
|
||||
ScopedKeyPair(SecurityModule& sm, KeyType type) : m_security(&sm), m_public_key(sm.create_key(type))
|
||||
{
|
||||
}
|
||||
|
||||
// no copy
|
||||
ScopedKeyPair(const ScopedKeyPair&) = delete;
|
||||
ScopedKeyPair& operator=(const ScopedKeyPair&) = delete;
|
||||
// no move
|
||||
ScopedKeyPair(ScopedKeyPair&&) = delete;
|
||||
ScopedKeyPair& operator=(ScopedKeyPair&&) = delete;
|
||||
|
||||
~ScopedKeyPair()
|
||||
{
|
||||
if (m_security) {
|
||||
m_security->discard_key(m_public_key);
|
||||
}
|
||||
}
|
||||
|
||||
const PublicKey& public_key() const
|
||||
{
|
||||
return m_public_key;
|
||||
}
|
||||
|
||||
void commit()
|
||||
{
|
||||
// will no longer discard created key
|
||||
m_security = nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
SecurityModule* m_security = nullptr;
|
||||
PublicKey m_public_key;
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <vanetza/security/hash_algorithm.hpp>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
using security::HashAlgorithm;
|
||||
|
||||
struct Sha256Hash
|
||||
{
|
||||
static constexpr std::size_t length = 32;
|
||||
std::array<std::uint8_t, length> octets;
|
||||
};
|
||||
|
||||
struct Sha384Hash
|
||||
{
|
||||
static constexpr std::size_t length = 48;
|
||||
std::array<std::uint8_t, length> octets;
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,157 @@
|
||||
#include "signed_builder.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "security_module.hpp"
|
||||
#include "time.hpp"
|
||||
#include "validation.hpp"
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
#include <vanetza/security/v3/basic_elements.hpp>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Map a Signature into the asn1c Vanetza_Security_Signature_t representation.
|
||||
void copy_signature(const Signature& from, Vanetza_Security_Signature_t& to)
|
||||
{
|
||||
switch (from.type) {
|
||||
case KeyType::NistP256:
|
||||
to.present = Vanetza_Security_Signature_PR_ecdsaNistP256Signature;
|
||||
to.choice.ecdsaNistP256Signature.rSig.present = Vanetza_Security_EccP256CurvePoint_PR_x_only;
|
||||
copy_left_padded(from.r, to.choice.ecdsaNistP256Signature.rSig.choice.x_only, 32);
|
||||
copy_left_padded(from.s, to.choice.ecdsaNistP256Signature.sSig, 32);
|
||||
break;
|
||||
case KeyType::BrainpoolP256r1:
|
||||
to.present = Vanetza_Security_Signature_PR_ecdsaBrainpoolP256r1Signature;
|
||||
to.choice.ecdsaBrainpoolP256r1Signature.rSig.present = Vanetza_Security_EccP256CurvePoint_PR_x_only;
|
||||
copy_left_padded(from.r, to.choice.ecdsaBrainpoolP256r1Signature.rSig.choice.x_only, 32);
|
||||
copy_left_padded(from.s, to.choice.ecdsaBrainpoolP256r1Signature.sSig, 32);
|
||||
break;
|
||||
case KeyType::BrainpoolP384r1:
|
||||
to.present = Vanetza_Security_Signature_PR_ecdsaBrainpoolP384r1Signature;
|
||||
to.choice.ecdsaBrainpoolP384r1Signature.rSig.present = Vanetza_Security_EccP384CurvePoint_PR_x_only;
|
||||
copy_left_padded(from.r, to.choice.ecdsaBrainpoolP384r1Signature.rSig.choice.x_only, 48);
|
||||
copy_left_padded(from.s, to.choice.ecdsaBrainpoolP384r1Signature.sSig, 48);
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("unknown key type");
|
||||
}
|
||||
}
|
||||
|
||||
// Build the EtsiTs103097Data-Signed envelope with its inner SignedData skeleton
|
||||
SignedData init_signed_envelope(SecurityModule& security, HashAlgorithm hash_algo, const Certificate* signer_cert)
|
||||
{
|
||||
SignedData envelope;
|
||||
envelope->protocolVersion = ieee1609dot2_protocol_version;
|
||||
envelope->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
envelope->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_signedData;
|
||||
envelope->content->choice.signedData = asn1::allocate<Vanetza_Security_SignedData_t>();
|
||||
|
||||
Vanetza_Security_SignedData_t* signed_data = envelope->content->choice.signedData;
|
||||
signed_data->hashId = convert(hash_algo);
|
||||
|
||||
if (signer_cert) {
|
||||
signed_data->signer.present = Vanetza_Security_SignerIdentifier_PR_digest;
|
||||
HashedId8 hid8 = signer_cert->calculate_hashed_id8(security);
|
||||
if (OCTET_STRING_fromBuf(&signed_data->signer.choice.digest,
|
||||
reinterpret_cast<const char*>(hid8.octets.data()), hid8.octets.size()) != 0) {
|
||||
throw std::runtime_error("setting signer digest failed");
|
||||
}
|
||||
} else {
|
||||
signed_data->signer.present = Vanetza_Security_SignerIdentifier_PR_self;
|
||||
}
|
||||
|
||||
signed_data->tbsData = asn1::allocate<Vanetza_Security_ToBeSignedData_t>();
|
||||
signed_data->tbsData->headerInfo.psid = aid::SCR;
|
||||
signed_data->tbsData->headerInfo.generationTime = asn1::allocate<Vanetza_Security_Time64_t>();
|
||||
if (asn_uint642INTEGER(signed_data->tbsData->headerInfo.generationTime,
|
||||
security::v3::convert_time64(current_time())) != 0) {
|
||||
throw std::runtime_error("setting generationTime failed");
|
||||
}
|
||||
|
||||
signed_data->tbsData->payload = asn1::allocate<Vanetza_Security_SignedDataPayload_t>();
|
||||
return envelope;
|
||||
}
|
||||
|
||||
// Sign the tbsData (already populated by the caller) and copy the resulting
|
||||
// signature into signed_data->signature.
|
||||
void sign_and_finalize(Vanetza_Security_SignedData_t* signed_data, SecurityModule& security,
|
||||
const PublicKey& signing_key, HashAlgorithm hash_algo, const Certificate* signer_cert)
|
||||
{
|
||||
const Vanetza_Security_Certificate_t* raw_cert = signer_cert ? &signer_cert->raw() : nullptr;
|
||||
ByteBuffer sign_input;
|
||||
switch (hash_algo) {
|
||||
case HashAlgorithm::SHA256: {
|
||||
Sha256Hash d = calculate_digest<Sha256Hash>(security, *signed_data->tbsData, raw_cert);
|
||||
sign_input.assign(d.octets.begin(), d.octets.end());
|
||||
break;
|
||||
}
|
||||
case HashAlgorithm::SHA384: {
|
||||
Sha384Hash d = calculate_digest<Sha384Hash>(security, *signed_data->tbsData, raw_cert);
|
||||
sign_input.assign(d.octets.begin(), d.octets.end());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw std::runtime_error("unknown hash algorithm");
|
||||
}
|
||||
|
||||
boost::optional<Signature> signature = security.sign(sign_input, signing_key);
|
||||
if (!signature) {
|
||||
throw std::runtime_error("signing failed: private key unknown to security module");
|
||||
}
|
||||
copy_signature(*signature, signed_data->signature);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SignedData create_signed(const ByteBuffer& payload, SecurityModule& security,
|
||||
const PublicKey& signing_key, HashAlgorithm hash_algo, const Certificate* signer_cert)
|
||||
{
|
||||
SignedData envelope = init_signed_envelope(security, hash_algo, signer_cert);
|
||||
Vanetza_Security_SignedData_t* signed_data = envelope->content->choice.signedData;
|
||||
|
||||
auto* sdp = signed_data->tbsData->payload;
|
||||
sdp->data = asn1::allocate<Vanetza_Security_EtsiTs103097Data_t>();
|
||||
sdp->data->protocolVersion = ieee1609dot2_protocol_version;
|
||||
sdp->data->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
sdp->data->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_unsecuredData;
|
||||
copy(payload, sdp->data->content->choice.unsecuredData);
|
||||
|
||||
sign_and_finalize(signed_data, security, signing_key, hash_algo, signer_cert);
|
||||
return envelope;
|
||||
}
|
||||
|
||||
SignedData create_external_signed(const ByteBuffer& external_payload, SecurityModule& security,
|
||||
const PublicKey& signing_key, HashAlgorithm hash_algo, const Certificate* signer_cert)
|
||||
{
|
||||
// The asn1c HashedData CHOICE only carries sha256HashedData; SHA-384
|
||||
// would need a newer 1609.2 schema. SharedAtRequest in TS 102 941
|
||||
// is hashed with SHA-256 in practice, so this restriction is fine.
|
||||
if (hash_algo != HashAlgorithm::SHA256) {
|
||||
throw std::invalid_argument("create_external_signed: only SHA-256 is supported for extDataHash");
|
||||
}
|
||||
|
||||
SignedData envelope = init_signed_envelope(security, hash_algo, signer_cert);
|
||||
Vanetza_Security_SignedData_t* signed_data = envelope->content->choice.signedData;
|
||||
|
||||
auto* sdp = signed_data->tbsData->payload;
|
||||
sdp->extDataHash = asn1::allocate<Vanetza_Security_HashedData_t>();
|
||||
Sha256Hash h = security.calculate_sha256_hash(external_payload.data(), external_payload.size());
|
||||
sdp->extDataHash->present = Vanetza_Security_HashedData_PR_sha256HashedData;
|
||||
if (OCTET_STRING_fromBuf(&sdp->extDataHash->choice.sha256HashedData,
|
||||
reinterpret_cast<const char*>(h.octets.data()), h.octets.size()) != 0) {
|
||||
throw std::runtime_error("setting extDataHash failed");
|
||||
}
|
||||
|
||||
sign_and_finalize(signed_data, security, signing_key, hash_algo, signer_cert);
|
||||
return envelope;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "keys.hpp"
|
||||
#include "signed_data.hpp"
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <vanetza/security/hash_algorithm.hpp>
|
||||
#include <cstdint>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class Certificate;
|
||||
class SecurityModule;
|
||||
|
||||
using security::HashAlgorithm;
|
||||
|
||||
// TS 103 097 protocol version that we emit on every constructed EtsiTs103097Data.
|
||||
inline constexpr std::uint8_t ieee1609dot2_protocol_version = 3;
|
||||
|
||||
/**
|
||||
* Build an EtsiTs103097Data-Signed envelope carrying `payload` as inline
|
||||
* `unsecuredData`, signed with `signing_key`. PSID is fixed to SCR.
|
||||
*
|
||||
* \param signer_cert selects the SignerIdentifier variant per IEEE 1609.2 §5.3.1:
|
||||
* nullptr → signer = self (hash of signer is hash of empty string)
|
||||
* non-null → signer = digest with HashedId8(cert); hash of signer is hash
|
||||
* of the OER-encoded signer certificate.
|
||||
*/
|
||||
SignedData create_signed(const ByteBuffer& payload, SecurityModule& security,
|
||||
const PublicKey& signing_key, HashAlgorithm hash_algo, const Certificate* signer_cert);
|
||||
|
||||
/**
|
||||
* Build an EtsiTs103097Data-SignedExternalPayload (TS 102 941 §6.2.3.3.1) used
|
||||
* for the EC-signed proof inside an AT request: SignedData with
|
||||
* `extDataHash = hash_algo(external_payload)` and no inline data.
|
||||
*
|
||||
* The same `hash_algo` covers both the external-payload hash placed in
|
||||
* extDataHash and the signature hash chain. Other parameters mirror
|
||||
* create_signed(); PSID is fixed to SCR.
|
||||
*/
|
||||
SignedData create_external_signed(const ByteBuffer& external_payload, SecurityModule& security,
|
||||
const PublicKey& signing_key, HashAlgorithm hash_algo, const Certificate* signer_cert);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "signed_data.hpp"
|
||||
#include <vanetza/asn1/security/Ieee1609Dot2Content.h>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
void SignedData::move_into(Vanetza_Security_Ieee1609Dot2Data_t& dest) &&
|
||||
{
|
||||
auto* self = content();
|
||||
// Release anything dest already held (normally none) to avoid leaks.
|
||||
ASN_STRUCT_FREE(asn_DEF_Vanetza_Security_Ieee1609Dot2Content, dest.content);
|
||||
dest.protocolVersion = self->protocolVersion;
|
||||
dest.content = self->content;
|
||||
self->content = nullptr;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <vanetza/asn1/asn1c_wrapper.hpp>
|
||||
#include <vanetza/asn1/security/EtsiTs103097Data-Signed.h>
|
||||
#include <vanetza/asn1/security/Ieee1609Dot2Data.h>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class SignedData : public asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs103097Data_Signed_55P0_t>
|
||||
{
|
||||
public:
|
||||
using wrapper = asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs103097Data_Signed_55P0_t>;
|
||||
|
||||
SignedData() : wrapper(asn_DEF_Vanetza_Security_EtsiTs103097Data_Signed_55P0)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Graft the envelope into a parent that takes over ownership.
|
||||
* Consuming: only callable on an rvalue, leaving the source empty.
|
||||
*
|
||||
* \param dest Ieee1609Dot2Data field of the parent to move the envelope into
|
||||
*/
|
||||
void move_into(Vanetza_Security_Ieee1609Dot2Data_t& dest) &&;
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,101 @@
|
||||
#include "station.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "hashed_id8_validator.hpp"
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
const HashedId8Validator hid8_validator;
|
||||
|
||||
struct Context
|
||||
{
|
||||
Context(const MainConfig& c) : cfg(c)
|
||||
{
|
||||
}
|
||||
|
||||
const MainConfig& cfg;
|
||||
std::function<void()> action;
|
||||
std::string canonical;
|
||||
HashedId8 root_ca;
|
||||
};
|
||||
|
||||
void print_station_summary(Context& ctx)
|
||||
{
|
||||
const std::string canonical = ctx.cfg.station->get_canonical_identifier();
|
||||
const boost::optional<HashedId8> ec_id = ctx.cfg.station->get_ec_identifier();
|
||||
const boost::optional<HashedId8> root_ca = ctx.cfg.station->get_root_ca();
|
||||
|
||||
std::cout << "Canonical identifier: " << (canonical.empty() ? "[unknown]" : canonical) << "\n";
|
||||
std::cout << "Enrolled: " << (ec_id ? "yes" : "no") << "\n";
|
||||
if (ec_id) {
|
||||
std::cout << "EC digest: " << hexstring(*ec_id) << "\n";
|
||||
boost::optional<Certificate> ec_cert = ctx.cfg.enrolment_credentials->fetch(*ec_id);
|
||||
std::cout << "EC certificate: " << (ec_cert ? "[available]" : "[missing]") << "\n";
|
||||
}
|
||||
|
||||
if (root_ca) {
|
||||
std::cout << "Root CA: " << hexstring(*root_ca) << "\n";
|
||||
boost::optional<std::string> dc_url = lookup_dc_url(ctx.cfg.data_path / "ectl.ctl", *root_ca);
|
||||
if (dc_url) {
|
||||
std::cout << "DC URL: " << *dc_url << "\n";
|
||||
} else {
|
||||
std::cout << "DC URL: [not found in ECTL]\n";
|
||||
}
|
||||
} else {
|
||||
std::cout << "Root CA: [not set]\n";
|
||||
}
|
||||
}
|
||||
|
||||
void set_root_ca(Context& ctx)
|
||||
{
|
||||
if (auto cert = ctx.cfg.root_ca->fetch(ctx.root_ca)) {
|
||||
if (is_root_ca(*cert)) {
|
||||
ctx.cfg.station->set_root_ca(ctx.root_ca);
|
||||
std::cout << "Found Root CA certificate in cache.\n";
|
||||
} else {
|
||||
std::cout << "Found certificate matching " << hexstring(ctx.root_ca)
|
||||
<< ", but it is not a Root CA certificate!\n";
|
||||
}
|
||||
} else {
|
||||
std::cout << "Cannot set Root CA because the full certificate is missing.\n";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::shared_ptr<CLI::App> build_station_command(const MainConfig& cfg)
|
||||
{
|
||||
auto ctx = std::make_shared<Context>(cfg);
|
||||
auto app = std::make_shared<CLI::App>("summary of station configuration", "station");
|
||||
|
||||
auto set_canonical = app->add_subcommand("set-canonical-id", "set the canonical identifier");
|
||||
set_canonical->add_option("id", ctx->canonical, "canonical identifier")->required();
|
||||
set_canonical->callback([ctx]() {
|
||||
ctx->action = [ctx]() { ctx->cfg.station->set_canonical_identifier(ctx->canonical); };
|
||||
});
|
||||
|
||||
auto set_root = app->add_subcommand("set-root-ca", "set the station's Root CA");
|
||||
set_root->add_option("hid8", ctx->root_ca, "HashedId8 of the Root CA")->required()->check(hid8_validator);
|
||||
set_root->callback([ctx]() { ctx->action = [ctx]() { set_root_ca(*ctx); }; });
|
||||
|
||||
app->require_subcommand(0, 1);
|
||||
app->final_callback([ctx]() {
|
||||
if (!ctx->action) {
|
||||
ctx->action = [ctx]() { print_station_summary(*ctx); };
|
||||
}
|
||||
ctx->action();
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "main.hpp"
|
||||
#include <CLI/App.hpp>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
std::shared_ptr<CLI::App> build_station_command(const MainConfig&);
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "hashed_id8.hpp"
|
||||
#include <boost/optional/optional.hpp>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class StationConfiguration
|
||||
{
|
||||
public:
|
||||
virtual ~StationConfiguration() = default;
|
||||
|
||||
virtual std::string get_canonical_identifier() const = 0;
|
||||
virtual void set_canonical_identifier(const std::string&) = 0;
|
||||
|
||||
virtual boost::optional<HashedId8> get_ec_identifier() const = 0;
|
||||
virtual void set_ec_identifier(const HashedId8&) = 0;
|
||||
|
||||
virtual boost::optional<HashedId8> get_root_ca() const = 0;
|
||||
virtual void set_root_ca(const HashedId8&) = 0;
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "station_config_filesystem.hpp"
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <system_error>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Write text to a config file; on a write error remove the half-written file so a later
|
||||
// read sees a clean "unset" state rather than a corrupt one.
|
||||
void write_config(const std::filesystem::path& path, const std::string& content)
|
||||
{
|
||||
std::ofstream ofs(path);
|
||||
ofs << content;
|
||||
ofs.flush();
|
||||
if (!ofs) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(path, ec);
|
||||
throw std::runtime_error("could not write station config file " + path.string());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
StationConfigurationFilesystem::StationConfigurationFilesystem(const std::filesystem::path& root) : m_root(root)
|
||||
{
|
||||
std::filesystem::create_directories(m_root);
|
||||
}
|
||||
|
||||
std::string StationConfigurationFilesystem::get_canonical_identifier() const
|
||||
{
|
||||
std::ifstream ifs(m_root / "identifier");
|
||||
std::string identifier;
|
||||
std::getline(ifs, identifier);
|
||||
return identifier;
|
||||
}
|
||||
|
||||
void StationConfigurationFilesystem::set_canonical_identifier(const std::string& identifier)
|
||||
{
|
||||
write_config(m_root / "identifier", identifier);
|
||||
}
|
||||
|
||||
boost::optional<HashedId8> StationConfigurationFilesystem::get_ec_identifier() const
|
||||
{
|
||||
return get_hid8(m_root / "ec.hid8");
|
||||
}
|
||||
|
||||
void StationConfigurationFilesystem::set_ec_identifier(const HashedId8& id)
|
||||
{
|
||||
set_hid8(m_root / "ec.hid8", id);
|
||||
}
|
||||
|
||||
boost::optional<HashedId8> StationConfigurationFilesystem::get_root_ca() const
|
||||
{
|
||||
return get_hid8(m_root / "rootca.hid8");
|
||||
}
|
||||
|
||||
void StationConfigurationFilesystem::set_root_ca(const HashedId8& id)
|
||||
{
|
||||
set_hid8(m_root / "rootca.hid8", id);
|
||||
}
|
||||
|
||||
boost::optional<HashedId8> StationConfigurationFilesystem::get_hid8(const std::filesystem::path& path) const
|
||||
{
|
||||
std::ifstream ifs(path);
|
||||
std::string hex;
|
||||
std::getline(ifs, hex);
|
||||
return HashedId8::from_hexstring(hex);
|
||||
}
|
||||
|
||||
void StationConfigurationFilesystem::set_hid8(const std::filesystem::path& path, const HashedId8& id)
|
||||
{
|
||||
write_config(path, hexstring(id));
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "filesystem.hpp"
|
||||
#include "station_config.hpp"
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class StationConfigurationFilesystem : public StationConfiguration
|
||||
{
|
||||
public:
|
||||
StationConfigurationFilesystem(const std::filesystem::path&);
|
||||
|
||||
std::string get_canonical_identifier() const override;
|
||||
void set_canonical_identifier(const std::string&) override;
|
||||
|
||||
boost::optional<HashedId8> get_ec_identifier() const override;
|
||||
void set_ec_identifier(const HashedId8&) override;
|
||||
|
||||
boost::optional<HashedId8> get_root_ca() const override;
|
||||
void set_root_ca(const HashedId8&) override;
|
||||
|
||||
protected:
|
||||
boost::optional<HashedId8> get_hid8(const std::filesystem::path&) const;
|
||||
void set_hid8(const std::filesystem::path&, const HashedId8&);
|
||||
|
||||
private:
|
||||
std::filesystem::path m_root;
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,24 @@
|
||||
include(UseGTest)
|
||||
|
||||
configure_gtest_directory(
|
||||
INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/..
|
||||
COMPILE_DEFINITIONS OPENSSL_API_COMPAT=0x10101000L
|
||||
LINK_LIBRARIES pki_library)
|
||||
add_gtest(CertificateRevocationList certificate_revocation_list.cpp)
|
||||
add_gtest(CertificateRole certificate_role.cpp)
|
||||
add_gtest(CertificateTrustList certificate_trust_list.cpp)
|
||||
add_gtest(CredentialFilesystemStorage credential_filesystem_storage.cpp)
|
||||
add_gtest(CrlFilesystemStore crl_store.cpp)
|
||||
add_gtest(AtRequest at_request.cpp)
|
||||
add_gtest(AtResponse at_response.cpp)
|
||||
add_gtest(EaRequest ea_request.cpp)
|
||||
add_gtest(EaResponse ea_response.cpp)
|
||||
add_gtest(Ecies ecies.cpp)
|
||||
add_gtest(HexString hexstring.cpp)
|
||||
add_gtest(Http http.cpp)
|
||||
add_gtest(Keys keys.cpp)
|
||||
add_gtest(Pem pem.cpp)
|
||||
add_gtest(PkiTime time.cpp)
|
||||
add_gtest(SecurityModuleNG security_module.cpp)
|
||||
add_gtest(SignedBuilder signed_builder.cpp)
|
||||
add_gtest(PruneCommand prune_command.cpp)
|
||||
@@ -0,0 +1,281 @@
|
||||
#include "at_request.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "encrypted_data.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include "signed_data.hpp"
|
||||
#include "stub_certificate.hpp"
|
||||
#include "validation.hpp"
|
||||
#include <vanetza/asn1/asn1c_wrapper.hpp>
|
||||
#include <vanetza/asn1/security/EtsiTs102941Data.h>
|
||||
#include <vanetza/asn1/security/InnerAtRequest.h>
|
||||
#include <vanetza/asn1/security/SharedAtRequest.h>
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <vanetza/common/clock.hpp>
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
#include <vanetza/security/v3/basic_elements.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class AuthorizationRequestTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
AuthorizationRequestTest() : m_credentials(std::make_shared<MockCredentialStorage>()), m_security(m_credentials)
|
||||
{
|
||||
// EA / AA need both verification and encryption keys; EC needs only verification.
|
||||
PublicKey ea_verify = m_security.create_key(KeyType::NistP256);
|
||||
PublicKey ea_encrypt = m_security.create_key(KeyType::NistP256);
|
||||
m_ea = build_stub_certificate(ea_verify, &ea_encrypt);
|
||||
|
||||
PublicKey aa_verify = m_security.create_key(KeyType::NistP256);
|
||||
PublicKey aa_encrypt = m_security.create_key(KeyType::NistP256);
|
||||
m_aa = build_stub_certificate(aa_verify, &aa_encrypt);
|
||||
|
||||
m_ec_key = m_security.create_key(KeyType::NistP256);
|
||||
m_ec = build_stub_certificate(m_ec_key);
|
||||
}
|
||||
|
||||
AuthorizationRequestParameters make_params()
|
||||
{
|
||||
AuthorizationRequestParameters p;
|
||||
p.ec = &m_ec;
|
||||
p.ea_certificate = &m_ea;
|
||||
p.aa_certificate = &m_aa;
|
||||
p.verification_key = m_security.create_key(KeyType::NistP256);
|
||||
p.permissions = { PsidSsp { aid::CA, { 0x01, 0x00, 0x00 } } };
|
||||
return p;
|
||||
}
|
||||
|
||||
// Decode `bytes` as an EtsiTs102941Data, transparently unwrapping a POP
|
||||
// EtsiTs103097Data-Signed envelope when present (the default for AT
|
||||
// requests; TS 102 941 §6.2.3.3.1).
|
||||
bool decode_inner_management_data(const ByteBuffer& bytes,
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t>& mgmt)
|
||||
{
|
||||
SignedData pop;
|
||||
if (pop.decode(bytes) && pop->content->present == Vanetza_Security_Ieee1609Dot2Content_PR_signedData) {
|
||||
const auto* sd = pop->content->choice.signedData;
|
||||
const auto& payload = sd->tbsData->payload->data->content->choice.unsecuredData;
|
||||
return mgmt.decode(payload.buf, payload.size);
|
||||
}
|
||||
return mgmt.decode(bytes.data(), bytes.size());
|
||||
}
|
||||
|
||||
std::shared_ptr<MockCredentialStorage> m_credentials;
|
||||
OpenSslSecurityModule m_security;
|
||||
Certificate m_ea;
|
||||
Certificate m_aa;
|
||||
Certificate m_ec;
|
||||
PublicKey m_ec_key;
|
||||
};
|
||||
|
||||
// Empty permission list violates CertificateSubjectAttributes invariants and
|
||||
// makes the request meaningless. The builder rejects it early.
|
||||
TEST_F(AuthorizationRequestTest, rejects_empty_permissions)
|
||||
{
|
||||
auto params = make_params();
|
||||
params.permissions.clear();
|
||||
EXPECT_THROW(build_signed_authorization_request(m_security, params), std::invalid_argument);
|
||||
}
|
||||
|
||||
// Required certificates: ec, ea, aa. Missing any → throw.
|
||||
TEST_F(AuthorizationRequestTest, rejects_missing_certificates)
|
||||
{
|
||||
{
|
||||
auto params = make_params();
|
||||
params.ec = nullptr;
|
||||
EXPECT_THROW(build_signed_authorization_request(m_security, params), std::invalid_argument);
|
||||
}
|
||||
{
|
||||
auto params = make_params();
|
||||
params.ea_certificate = nullptr;
|
||||
EXPECT_THROW(build_signed_authorization_request(m_security, params), std::invalid_argument);
|
||||
}
|
||||
{
|
||||
auto params = make_params();
|
||||
params.aa_certificate = nullptr;
|
||||
EXPECT_THROW(build_authorization_request(m_security, params), std::invalid_argument);
|
||||
}
|
||||
}
|
||||
|
||||
// With include_pop = true (default), the bytes returned are an
|
||||
// EtsiTs103097Data-Signed envelope (signer = self) carrying the
|
||||
// EtsiTs102941Data{authorizationRequest} as unsecuredData payload, and the
|
||||
// signature verifies under the new AT verification key.
|
||||
// (TS 102 941 §6.2.3.3.1 AuthorizationRequestMessageWithPop.)
|
||||
TEST_F(AuthorizationRequestTest, default_includes_pop_signed_envelope)
|
||||
{
|
||||
auto params = make_params();
|
||||
ByteBuffer encoded = build_signed_authorization_request(m_security, params);
|
||||
|
||||
SignedData pop;
|
||||
ASSERT_TRUE(pop.decode(encoded));
|
||||
ASSERT_EQ(Vanetza_Security_Ieee1609Dot2Content_PR_signedData, pop->content->present);
|
||||
const auto& sd = *pop->content->choice.signedData;
|
||||
EXPECT_EQ(Vanetza_Security_SignerIdentifier_PR_self, sd.signer.present);
|
||||
EXPECT_EQ(aid::SCR, sd.tbsData->headerInfo.psid);
|
||||
|
||||
// Signature verifies under the verification key (signer = self → empty signer cert)
|
||||
Sha256Hash digest = calculate_digest<Sha256Hash>(m_security, *sd.tbsData, nullptr);
|
||||
EXPECT_TRUE(m_security.verify(digest, make_signature(sd.signature), params.verification_key));
|
||||
}
|
||||
|
||||
// With include_pop = false, the bytes returned are the bare EtsiTs102941Data
|
||||
// (AuthorizationRequestMessage variant — no POP envelope).
|
||||
TEST_F(AuthorizationRequestTest, opt_out_of_pop_returns_bare_management_data)
|
||||
{
|
||||
auto params = make_params();
|
||||
params.include_pop = false;
|
||||
ByteBuffer encoded = build_signed_authorization_request(m_security, params);
|
||||
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
|
||||
ASSERT_TRUE(mgmt.decode(encoded.data(), encoded.size()));
|
||||
EXPECT_EQ(Vanetza_Security_Version_v1, mgmt->version);
|
||||
EXPECT_EQ(Vanetza_Security_EtsiTs102941DataContent_PR_authorizationRequest, mgmt->content.present);
|
||||
}
|
||||
|
||||
// SharedAtRequest fields per TS 102 941 §6.2.3.3.1: eaId = HashedId8(EA),
|
||||
// keyTag (16 bytes), certificateFormat = ts103097v131. With no AT
|
||||
// encryption key, keyTag = first 16 bytes of HMAC-SHA256(hmacKey, verifyKey).
|
||||
TEST_F(AuthorizationRequestTest, shared_at_request_fields_match_spec)
|
||||
{
|
||||
auto params = make_params();
|
||||
ByteBuffer encoded = build_signed_authorization_request(m_security, params);
|
||||
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
|
||||
ASSERT_TRUE(decode_inner_management_data(encoded, mgmt));
|
||||
const auto& iar = mgmt->content.choice.authorizationRequest;
|
||||
const auto& sar = iar.sharedAtRequest;
|
||||
|
||||
HashedId8 ea_hid8 = m_ea.calculate_hashed_id8(m_security);
|
||||
EXPECT_TRUE(sar.eaId == ea_hid8.octets);
|
||||
EXPECT_EQ(Vanetza_Security_CertificateFormat_ts103097v131, sar.certificateFormat);
|
||||
EXPECT_EQ(16, sar.keyTag.size);
|
||||
|
||||
// Default params have no at_encryption_key; encryptionKey must be absent
|
||||
// and keyTag is HMAC over verifyKey only.
|
||||
EXPECT_EQ(nullptr, iar.publicKeys.encryptionKey);
|
||||
|
||||
ByteBuffer verify_oer =
|
||||
asn1::encode_oer(asn_DEF_Vanetza_Security_PublicVerificationKey, &iar.publicKeys.verificationKey);
|
||||
ByteBuffer hmac_key(iar.hmacKey.buf, iar.hmacKey.buf + iar.hmacKey.size);
|
||||
EXPECT_EQ(32, hmac_key.size());
|
||||
ByteBuffer expected_tag = m_security.calculate_hmac_sha256(hmac_key, verify_oer);
|
||||
EXPECT_EQ(0, std::memcmp(sar.keyTag.buf, expected_tag.data(), 16));
|
||||
|
||||
ASSERT_NE(nullptr, sar.requestedSubjectAttributes.appPermissions);
|
||||
ASSERT_EQ(1, sar.requestedSubjectAttributes.appPermissions->list.count);
|
||||
EXPECT_EQ(static_cast<long>(aid::CA), sar.requestedSubjectAttributes.appPermissions->list.array[0]->psid);
|
||||
}
|
||||
|
||||
// When at_encryption_key is provided, it appears in publicKeys and is folded
|
||||
// into the keyTag HMAC input (verifyKey || encKey).
|
||||
TEST_F(AuthorizationRequestTest, optional_at_encryption_key_extends_keytag)
|
||||
{
|
||||
auto params = make_params();
|
||||
params.at_encryption_key = m_security.create_key(KeyType::NistP256);
|
||||
ByteBuffer encoded = build_signed_authorization_request(m_security, params);
|
||||
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
|
||||
ASSERT_TRUE(decode_inner_management_data(encoded, mgmt));
|
||||
const auto& iar = mgmt->content.choice.authorizationRequest;
|
||||
|
||||
ASSERT_NE(nullptr, iar.publicKeys.encryptionKey);
|
||||
|
||||
ByteBuffer verify_oer =
|
||||
asn1::encode_oer(asn_DEF_Vanetza_Security_PublicVerificationKey, &iar.publicKeys.verificationKey);
|
||||
ByteBuffer enc_oer = asn1::encode_oer(asn_DEF_Vanetza_Security_PublicEncryptionKey, iar.publicKeys.encryptionKey);
|
||||
ByteBuffer combined;
|
||||
combined.insert(combined.end(), verify_oer.begin(), verify_oer.end());
|
||||
combined.insert(combined.end(), enc_oer.begin(), enc_oer.end());
|
||||
ByteBuffer hmac_key(iar.hmacKey.buf, iar.hmacKey.buf + iar.hmacKey.size);
|
||||
ByteBuffer expected_tag = m_security.calculate_hmac_sha256(hmac_key, combined);
|
||||
EXPECT_EQ(0, std::memcmp(iar.sharedAtRequest.keyTag.buf, expected_tag.data(), 16));
|
||||
}
|
||||
|
||||
// ecSignature is the privacy-preserving `encryptedEcSignature` variant; the
|
||||
// inner ciphertext is addressed to the EA (recipientId = HashedId8(EA)).
|
||||
TEST_F(AuthorizationRequestTest, ec_signature_is_encrypted_to_ea)
|
||||
{
|
||||
auto params = make_params();
|
||||
ByteBuffer encoded = build_signed_authorization_request(m_security, params);
|
||||
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
|
||||
ASSERT_TRUE(decode_inner_management_data(encoded, mgmt));
|
||||
const auto& iar = mgmt->content.choice.authorizationRequest;
|
||||
ASSERT_EQ(Vanetza_Security_EcSignature_PR_encryptedEcSignature, iar.ecSignature.present);
|
||||
|
||||
const auto& enc_sig = iar.ecSignature.choice.encryptedEcSignature;
|
||||
ASSERT_EQ(Vanetza_Security_Ieee1609Dot2Content_PR_encryptedData, enc_sig.content->present);
|
||||
const auto& recipients = enc_sig.content->choice.encryptedData.recipients;
|
||||
ASSERT_GE(recipients.list.count, 1);
|
||||
|
||||
HashedId8 ea_hid8 = m_ea.calculate_hashed_id8(m_security);
|
||||
const auto* recip = recipients.list.array[0];
|
||||
ASSERT_EQ(Vanetza_Security_RecipientInfo_PR_certRecipInfo, recip->present);
|
||||
EXPECT_TRUE(recip->choice.certRecipInfo.recipientId == ea_hid8.octets);
|
||||
}
|
||||
|
||||
// By default no validityPeriod hint is sent (TS 102 941 §6.2.3.3.1: optional).
|
||||
// The AA picks the validity unilaterally.
|
||||
TEST_F(AuthorizationRequestTest, default_omits_validity_period_hint)
|
||||
{
|
||||
auto params = make_params();
|
||||
ByteBuffer encoded = build_signed_authorization_request(m_security, params);
|
||||
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
|
||||
ASSERT_TRUE(decode_inner_management_data(encoded, mgmt));
|
||||
const auto& sar = mgmt->content.choice.authorizationRequest.sharedAtRequest;
|
||||
EXPECT_EQ(nullptr, sar.requestedSubjectAttributes.validityPeriod);
|
||||
}
|
||||
|
||||
// When `validity_period` is set, the SharedAtRequest carries a validityPeriod
|
||||
// hint with the requested start (encoded as IEEE 1609.2 Time32) and duration
|
||||
// (hours variant). Per CP §7.2.1, duration ≤ 1 week.
|
||||
TEST_F(AuthorizationRequestTest, validity_period_hint_propagates_to_shared_at_request)
|
||||
{
|
||||
auto params = make_params();
|
||||
Clock::time_point start = Clock::time_point { std::chrono::hours(24 * 7) }; // arbitrary t > epoch
|
||||
ValidityPeriodHint hint;
|
||||
hint.start = start;
|
||||
hint.duration = std::chrono::hours(168);
|
||||
params.validity_period = hint;
|
||||
|
||||
ByteBuffer encoded = build_signed_authorization_request(m_security, params);
|
||||
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
|
||||
ASSERT_TRUE(decode_inner_management_data(encoded, mgmt));
|
||||
const auto& sar = mgmt->content.choice.authorizationRequest.sharedAtRequest;
|
||||
ASSERT_NE(nullptr, sar.requestedSubjectAttributes.validityPeriod);
|
||||
|
||||
EXPECT_EQ(security::v3::convert_time32(start), sar.requestedSubjectAttributes.validityPeriod->start);
|
||||
ASSERT_EQ(Vanetza_Security_Duration_PR_hours, sar.requestedSubjectAttributes.validityPeriod->duration.present);
|
||||
EXPECT_EQ(168u, sar.requestedSubjectAttributes.validityPeriod->duration.choice.hours);
|
||||
}
|
||||
|
||||
// build_authorization_request returns an EncryptedData addressed to the AA.
|
||||
TEST_F(AuthorizationRequestTest, outer_encrypted_to_aa)
|
||||
{
|
||||
auto params = make_params();
|
||||
EncryptedData encrypted = build_authorization_request(m_security, params);
|
||||
|
||||
ASSERT_EQ(Vanetza_Security_Ieee1609Dot2Content_PR_encryptedData, encrypted->content->present);
|
||||
const auto& recipients = encrypted->content->choice.encryptedData.recipients;
|
||||
ASSERT_GE(recipients.list.count, 1);
|
||||
|
||||
HashedId8 aa_hid8 = m_aa.calculate_hashed_id8(m_security);
|
||||
const auto* recip = recipients.list.array[0];
|
||||
ASSERT_EQ(Vanetza_Security_RecipientInfo_PR_certRecipInfo, recip->present);
|
||||
EXPECT_TRUE(recip->choice.certRecipInfo.recipientId == aa_hid8.octets);
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,180 @@
|
||||
#include "at_response.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "exception.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include "stub_certificate.hpp"
|
||||
#include "time.hpp"
|
||||
#include "validation.hpp"
|
||||
#include <vanetza/asn1/asn1c_wrapper.hpp>
|
||||
#include <vanetza/asn1/security/AuthorizationResponseCode.h>
|
||||
#include <vanetza/asn1/security/EtsiTs102941Data.h>
|
||||
#include <vanetza/asn1/security/EtsiTs103097Certificate.h>
|
||||
#include <vanetza/asn1/security/Ieee1609Dot2Data.h>
|
||||
#include <vanetza/asn1/security/InnerAtResponse.h>
|
||||
#include <vanetza/asn1/security/SignedData.h>
|
||||
#include <vanetza/asn1/security/Time64.h>
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
#include <vanetza/security/v3/basic_elements.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr std::uint8_t protocol_version = 3;
|
||||
|
||||
// Builder for a decrypted authorization response. Defaults produce a valid
|
||||
// response with code=ok and a synthetic AT certificate. Knobs flip individual
|
||||
// invariants for negative tests.
|
||||
struct AtResponseBuilder
|
||||
{
|
||||
OpenSslSecurityModule* security;
|
||||
const Certificate* aa_cert;
|
||||
const PublicKey* aa_signing_key;
|
||||
|
||||
Vanetza_Security_AuthorizationResponseCode_t code = Vanetza_Security_AuthorizationResponseCode_ok;
|
||||
bool include_certificate = true;
|
||||
boost::optional<long> psid_override;
|
||||
boost::optional<HashedId8> signer_digest_override;
|
||||
|
||||
ByteBuffer build() const
|
||||
{
|
||||
// Inner: EtsiTs102941Data { authorizationResponse { InnerAtResponse } }
|
||||
MgmtData inner;
|
||||
inner->version = Vanetza_Security_Version_v1;
|
||||
inner->content.present = Vanetza_Security_EtsiTs102941DataContent_PR_authorizationResponse;
|
||||
auto& at_resp = inner->content.choice.authorizationResponse;
|
||||
|
||||
uint8_t req_hash[16];
|
||||
std::fill(std::begin(req_hash), std::end(req_hash), 0xCD);
|
||||
OCTET_STRING_fromBuf(&at_resp.requestHash, reinterpret_cast<char*>(req_hash), 16);
|
||||
at_resp.responseCode = code;
|
||||
if (include_certificate) {
|
||||
PublicKey dummy_key = security->create_key(KeyType::NistP256);
|
||||
Certificate dummy_at = build_stub_certificate(dummy_key);
|
||||
ByteBuffer encoded_at = dummy_at.encode();
|
||||
void* decoded = nullptr;
|
||||
EXPECT_TRUE(asn1::decode_oer(asn_DEF_Vanetza_Security_EtsiTs103097Certificate, &decoded, encoded_at));
|
||||
at_resp.certificate = static_cast<Vanetza_Security_EtsiTs103097Certificate*>(decoded);
|
||||
}
|
||||
EXPECT_TRUE(inner.validate());
|
||||
ByteBuffer inner_encoded = inner.encode();
|
||||
|
||||
// Outer: EtsiTs103097Data { signedData (signer=digest(AA)) }
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_Ieee1609Dot2Data_t> outer(asn_DEF_Vanetza_Security_Ieee1609Dot2Data);
|
||||
outer->protocolVersion = protocol_version;
|
||||
outer->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
outer->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_signedData;
|
||||
outer->content->choice.signedData = asn1::allocate<Vanetza_Security_SignedData_t>();
|
||||
auto& sd = *outer->content->choice.signedData;
|
||||
sd.hashId = Vanetza_Security_HashAlgorithm_sha256;
|
||||
|
||||
sd.signer.present = Vanetza_Security_SignerIdentifier_PR_digest;
|
||||
HashedId8 aa_hid8 = signer_digest_override.value_or(aa_cert->calculate_hashed_id8(*security));
|
||||
OCTET_STRING_fromBuf(&sd.signer.choice.digest, reinterpret_cast<const char*>(aa_hid8.octets.data()), 8);
|
||||
|
||||
sd.tbsData = asn1::allocate<Vanetza_Security_ToBeSignedData_t>();
|
||||
sd.tbsData->headerInfo.psid = psid_override.value_or(aid::SCR);
|
||||
sd.tbsData->headerInfo.generationTime = asn1::allocate<Vanetza_Security_Time64_t>();
|
||||
asn_uint642INTEGER(sd.tbsData->headerInfo.generationTime, security::v3::convert_time64(current_time()));
|
||||
|
||||
sd.tbsData->payload = asn1::allocate<Vanetza_Security_SignedDataPayload_t>();
|
||||
auto* sdp = sd.tbsData->payload;
|
||||
sdp->data = asn1::allocate<Vanetza_Security_EtsiTs103097Data_t>();
|
||||
sdp->data->protocolVersion = protocol_version;
|
||||
sdp->data->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
sdp->data->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_unsecuredData;
|
||||
copy(inner_encoded, sdp->data->content->choice.unsecuredData);
|
||||
|
||||
Sha256Hash digest = calculate_digest<Sha256Hash>(*security, *sd.tbsData, &aa_cert->raw());
|
||||
auto signature = security->sign(ByteBuffer { digest.octets.begin(), digest.octets.end() }, *aa_signing_key);
|
||||
EXPECT_TRUE(static_cast<bool>(signature));
|
||||
|
||||
sd.signature.present = Vanetza_Security_Signature_PR_ecdsaNistP256Signature;
|
||||
sd.signature.choice.ecdsaNistP256Signature.rSig.present = Vanetza_Security_EccP256CurvePoint_PR_x_only;
|
||||
copy_left_padded(signature->r, sd.signature.choice.ecdsaNistP256Signature.rSig.choice.x_only, 32);
|
||||
copy_left_padded(signature->s, sd.signature.choice.ecdsaNistP256Signature.sSig, 32);
|
||||
|
||||
return outer.encode();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class AuthorizationResponseTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
AuthorizationResponseTest() :
|
||||
m_credentials(std::make_shared<MockCredentialStorage>()), m_security(m_credentials),
|
||||
m_aa_key(m_security.create_key(KeyType::NistP256)), m_aa_certificate(build_stub_certificate(m_aa_key))
|
||||
{
|
||||
}
|
||||
|
||||
AtResponseBuilder builder()
|
||||
{
|
||||
return AtResponseBuilder { &m_security, &m_aa_certificate, &m_aa_key };
|
||||
}
|
||||
|
||||
std::shared_ptr<MockCredentialStorage> m_credentials;
|
||||
OpenSslSecurityModule m_security;
|
||||
PublicKey m_aa_key;
|
||||
Certificate m_aa_certificate;
|
||||
};
|
||||
|
||||
TEST_F(AuthorizationResponseTest, happy_path_returns_code_ok_and_certificate)
|
||||
{
|
||||
ByteBuffer payload = builder().build();
|
||||
AuthorizationResponse resp = parse_authorization_response(m_security, payload, m_aa_certificate);
|
||||
EXPECT_EQ(Vanetza_Security_AuthorizationResponseCode_ok, resp.code);
|
||||
ASSERT_TRUE(resp.certificate.has_value());
|
||||
EXPECT_EQ(16u, resp.request_hash.size());
|
||||
}
|
||||
|
||||
TEST_F(AuthorizationResponseTest, rejects_signer_digest_mismatch)
|
||||
{
|
||||
AtResponseBuilder b = builder();
|
||||
HashedId8 other;
|
||||
other.octets = { 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22 };
|
||||
b.signer_digest_override = other;
|
||||
ByteBuffer payload = b.build();
|
||||
EXPECT_THROW(parse_authorization_response(m_security, payload, m_aa_certificate), VerificationFailure);
|
||||
}
|
||||
|
||||
TEST_F(AuthorizationResponseTest, rejects_wrong_psid)
|
||||
{
|
||||
AtResponseBuilder b = builder();
|
||||
b.psid_override = 0x20; // arbitrary non-SCR
|
||||
ByteBuffer payload = b.build();
|
||||
EXPECT_THROW(parse_authorization_response(m_security, payload, m_aa_certificate), DecodingFailure);
|
||||
}
|
||||
|
||||
TEST_F(AuthorizationResponseTest, non_ok_code_returns_without_throwing)
|
||||
{
|
||||
AtResponseBuilder b = builder();
|
||||
b.code = Vanetza_Security_AuthorizationResponseCode_deniedpermissions;
|
||||
b.include_certificate = false;
|
||||
ByteBuffer payload = b.build();
|
||||
AuthorizationResponse resp = parse_authorization_response(m_security, payload, m_aa_certificate);
|
||||
EXPECT_EQ(Vanetza_Security_AuthorizationResponseCode_deniedpermissions, resp.code);
|
||||
EXPECT_FALSE(resp.certificate.has_value());
|
||||
}
|
||||
|
||||
TEST_F(AuthorizationResponseTest, rejects_ok_without_certificate)
|
||||
{
|
||||
AtResponseBuilder b = builder();
|
||||
b.code = Vanetza_Security_AuthorizationResponseCode_ok;
|
||||
b.include_certificate = false;
|
||||
ByteBuffer payload = b.build();
|
||||
EXPECT_THROW(parse_authorization_response(m_security, payload, m_aa_certificate), DecodingFailure);
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
#include "certificate_revocation_list.hpp"
|
||||
#include "crl_test_fixture.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class CertificateRevocationListTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
CertificateRevocationListTest() :
|
||||
m_credentials(std::make_shared<MockCredentialStorage>()), m_security(m_credentials)
|
||||
{
|
||||
}
|
||||
|
||||
std::shared_ptr<MockCredentialStorage> m_credentials;
|
||||
OpenSslSecurityModule m_security;
|
||||
};
|
||||
|
||||
TEST_F(CertificateRevocationListTest, decode_rejects_garbage)
|
||||
{
|
||||
CertificateRevocationList crl;
|
||||
EXPECT_FALSE(crl.decode(ByteBuffer { 0x00, 0x01, 0x02, 0x03 }));
|
||||
}
|
||||
|
||||
TEST_F(CertificateRevocationListTest, get_hashed_id8_returns_signer_digest)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
CertificateRevocationList crl = build_test_crl(issuer, { make_id(0x11) });
|
||||
|
||||
auto got = crl.get_hashed_id8(m_security);
|
||||
ASSERT_TRUE(got);
|
||||
EXPECT_EQ(issuer, *got);
|
||||
}
|
||||
|
||||
TEST_F(CertificateRevocationListTest, revoked_entries_returns_list)
|
||||
{
|
||||
HashedId8 a = make_id(0x11);
|
||||
HashedId8 b = make_id(0x22);
|
||||
HashedId8 c = make_id(0x33);
|
||||
CertificateRevocationList crl = build_test_crl(make_id(0xAB), { a, b, c });
|
||||
|
||||
auto entries = crl.revoked_entries();
|
||||
ASSERT_TRUE(entries);
|
||||
ASSERT_EQ(3u, entries->size());
|
||||
EXPECT_EQ(a, (*entries)[0]);
|
||||
EXPECT_EQ(b, (*entries)[1]);
|
||||
EXPECT_EQ(c, (*entries)[2]);
|
||||
}
|
||||
|
||||
TEST_F(CertificateRevocationListTest, revoked_entries_empty_is_valid)
|
||||
{
|
||||
CertificateRevocationList crl = build_test_crl(make_id(0xAB), {});
|
||||
|
||||
auto entries = crl.revoked_entries();
|
||||
ASSERT_TRUE(entries);
|
||||
EXPECT_TRUE(entries->empty());
|
||||
}
|
||||
|
||||
TEST_F(CertificateRevocationListTest, encode_decode_roundtrip)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
HashedId8 revoked = make_id(0x42);
|
||||
CertificateRevocationList original = build_test_crl(issuer, { revoked });
|
||||
|
||||
ByteBuffer bytes = original.encode();
|
||||
CertificateRevocationList decoded;
|
||||
ASSERT_TRUE(decoded.decode(bytes));
|
||||
|
||||
auto issuer_out = decoded.get_hashed_id8(m_security);
|
||||
ASSERT_TRUE(issuer_out);
|
||||
EXPECT_EQ(issuer, *issuer_out);
|
||||
|
||||
auto entries = decoded.revoked_entries();
|
||||
ASSERT_TRUE(entries);
|
||||
ASSERT_EQ(1u, entries->size());
|
||||
EXPECT_EQ(revoked, (*entries)[0]);
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,163 @@
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include <vanetza/asn1/security/EtsiTs103097Certificate.h>
|
||||
#include <vanetza/asn1/security/PsidGroupPermissions.h>
|
||||
#include <vanetza/asn1/security/PsidSsp.h>
|
||||
#include <vanetza/asn1/security/SequenceOfPsidGroupPermissions.h>
|
||||
#include <vanetza/asn1/security/SequenceOfPsidSsp.h>
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
#include <vanetza/security/public_key.hpp>
|
||||
#include <vanetza/security/v3/certificate.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
PublicKey make_key()
|
||||
{
|
||||
static auto credentials = std::make_shared<MockCredentialStorage>();
|
||||
static OpenSslSecurityModule sm(credentials);
|
||||
return sm.create_key(KeyType::BrainpoolP256r1);
|
||||
}
|
||||
|
||||
enum class Issuer
|
||||
{
|
||||
Self,
|
||||
Digest
|
||||
};
|
||||
|
||||
enum class CertId
|
||||
{
|
||||
Name,
|
||||
None
|
||||
};
|
||||
|
||||
void set_issuer(Vanetza_Security_EtsiTs103097Certificate_t& cert, Issuer issuer)
|
||||
{
|
||||
if (issuer == Issuer::Self) {
|
||||
cert.issuer.present = Vanetza_Security_IssuerIdentifier_PR_self;
|
||||
cert.issuer.choice.self = Vanetza_Security_HashAlgorithm_sha256;
|
||||
} else {
|
||||
static const std::array<char, 8> digest { 1, 2, 3, 4, 5, 6, 7, 8 };
|
||||
cert.issuer.present = Vanetza_Security_IssuerIdentifier_PR_sha256AndDigest;
|
||||
OCTET_STRING_fromBuf(&cert.issuer.choice.sha256AndDigest, digest.data(), digest.size());
|
||||
}
|
||||
}
|
||||
|
||||
void set_cert_id(Vanetza_Security_ToBeSignedCertificate_t& tbs, CertId id)
|
||||
{
|
||||
if (id == CertId::Name) {
|
||||
tbs.id.present = Vanetza_Security_CertificateId_PR_name;
|
||||
OCTET_STRING_fromBuf(&tbs.id.choice.name, "test-cert", 9);
|
||||
} else {
|
||||
tbs.id.present = Vanetza_Security_CertificateId_PR_none;
|
||||
}
|
||||
}
|
||||
|
||||
void add_app_perm(Vanetza_Security_ToBeSignedCertificate_t& tbs, ItsAid psid)
|
||||
{
|
||||
if (!tbs.appPermissions) {
|
||||
tbs.appPermissions = asn1::allocate<Vanetza_Security_SequenceOfPsidSsp_t>();
|
||||
}
|
||||
auto* ps = asn1::allocate<Vanetza_Security_PsidSsp_t>();
|
||||
ps->psid = static_cast<long>(psid);
|
||||
ASN_SEQUENCE_ADD(&tbs.appPermissions->list, ps);
|
||||
}
|
||||
|
||||
void add_cert_issue_perm(Vanetza_Security_ToBeSignedCertificate_t& tbs, ItsAid psid)
|
||||
{
|
||||
if (!tbs.certIssuePermissions) {
|
||||
tbs.certIssuePermissions = asn1::allocate<Vanetza_Security_SequenceOfPsidGroupPermissions_t>();
|
||||
}
|
||||
auto* group = asn1::allocate<Vanetza_Security_PsidGroupPermissions_t>();
|
||||
group->subjectPermissions.present = Vanetza_Security_SubjectPermissions_PR_explicit;
|
||||
vanetza::security::v3::add_psid_group_permission(group, psid, { 0x01 }, { 0xff });
|
||||
ASN_SEQUENCE_ADD(&tbs.certIssuePermissions->list, group);
|
||||
}
|
||||
|
||||
Certificate build(Issuer issuer, CertId id, const std::vector<ItsAid>& app_perms,
|
||||
const std::vector<ItsAid>& cert_issue_perms)
|
||||
{
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs103097Certificate_t>
|
||||
cert(asn_DEF_Vanetza_Security_EtsiTs103097Certificate);
|
||||
cert->version = 3;
|
||||
cert->type = Vanetza_Security_CertificateType_explicit;
|
||||
set_issuer(*cert, issuer);
|
||||
|
||||
auto& tbs = cert->toBeSigned;
|
||||
set_cert_id(tbs, id);
|
||||
tbs.cracaId.buf = static_cast<uint8_t*>(std::calloc(3, 1));
|
||||
tbs.cracaId.size = 3;
|
||||
tbs.crlSeries = 0;
|
||||
tbs.validityPeriod.start = 0;
|
||||
tbs.validityPeriod.duration.present = Vanetza_Security_Duration_PR_hours;
|
||||
tbs.validityPeriod.duration.choice.hours = 24;
|
||||
tbs.verifyKeyIndicator.present = Vanetza_Security_VerificationKeyIndicator_PR_verificationKey;
|
||||
set_verification_key(tbs.verifyKeyIndicator.choice.verificationKey, make_key());
|
||||
|
||||
for (ItsAid p : app_perms) {
|
||||
add_app_perm(tbs, p);
|
||||
}
|
||||
for (ItsAid p : cert_issue_perms) {
|
||||
add_cert_issue_perm(tbs, p);
|
||||
}
|
||||
|
||||
Certificate result;
|
||||
EXPECT_TRUE(result.decode(cert.encode()));
|
||||
return result;
|
||||
}
|
||||
|
||||
TEST(CertificateRole, root_ca_self_issued_with_cert_issue_and_crl_ctl_app_perms)
|
||||
{
|
||||
auto cert = build(Issuer::Self, CertId::Name, { aid::CRL, aid::CTL }, { aid::SCR });
|
||||
EXPECT_EQ(CertificateRole::RootCa, certificate_role(cert));
|
||||
}
|
||||
|
||||
TEST(CertificateRole, tlm_self_issued_signs_ctl_without_cert_issue)
|
||||
{
|
||||
auto cert = build(Issuer::Self, CertId::Name, { aid::CTL }, {});
|
||||
EXPECT_EQ(CertificateRole::Tlm, certificate_role(cert));
|
||||
}
|
||||
|
||||
TEST(CertificateRole, enrolment_authority_issues_scr)
|
||||
{
|
||||
auto cert = build(Issuer::Digest, CertId::Name, { aid::SCR }, { aid::SCR });
|
||||
EXPECT_EQ(CertificateRole::EnrolmentAuthority, certificate_role(cert));
|
||||
}
|
||||
|
||||
TEST(CertificateRole, authorization_authority_issues_services)
|
||||
{
|
||||
auto cert = build(Issuer::Digest, CertId::Name, { aid::SCR }, { aid::CA });
|
||||
EXPECT_EQ(CertificateRole::AuthorizationAuthority, certificate_role(cert));
|
||||
}
|
||||
|
||||
TEST(CertificateRole, enrolment_credential_has_certificate_id_name)
|
||||
{
|
||||
auto cert = build(Issuer::Digest, CertId::Name, { aid::SCR }, {});
|
||||
EXPECT_EQ(CertificateRole::EnrolmentCredential, certificate_role(cert));
|
||||
}
|
||||
|
||||
TEST(CertificateRole, authorization_ticket_has_certificate_id_none)
|
||||
{
|
||||
auto cert = build(Issuer::Digest, CertId::None, { aid::CA }, {});
|
||||
EXPECT_EQ(CertificateRole::AuthorizationTicket, certificate_role(cert));
|
||||
}
|
||||
|
||||
TEST(CertificateRole, unknown_when_no_profile_matches)
|
||||
{
|
||||
auto cert = build(Issuer::Self, CertId::Name, { aid::CA }, {});
|
||||
EXPECT_EQ(CertificateRole::Unknown, certificate_role(cert));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
#include "certificate_trust_list.hpp"
|
||||
#include "ctl_test_fixture.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include "stub_certificate.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class CertificateTrustListTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
CertificateTrustListTest() :
|
||||
m_credentials(std::make_shared<MockCredentialStorage>()),
|
||||
m_security(std::make_shared<OpenSslSecurityModule>(m_credentials))
|
||||
{
|
||||
}
|
||||
|
||||
std::shared_ptr<MockCredentialStorage> m_credentials;
|
||||
std::shared_ptr<OpenSslSecurityModule> m_security;
|
||||
};
|
||||
|
||||
TEST_F(CertificateTrustListTest, is_full_ctl_true_for_full)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
auto ctl = TestRcaCtlBuilder(issuer, true, 5).build();
|
||||
auto is_full = ctl.is_full_ctl();
|
||||
ASSERT_TRUE(is_full);
|
||||
EXPECT_TRUE(*is_full);
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, is_full_ctl_false_for_delta)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
auto ctl = TestRcaCtlBuilder(issuer, false, 6).build();
|
||||
auto is_full = ctl.is_full_ctl();
|
||||
ASSERT_TRUE(is_full);
|
||||
EXPECT_FALSE(*is_full);
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, ctl_sequence_returns_value)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
auto ctl = TestRcaCtlBuilder(issuer, true, 42).build();
|
||||
auto seq = ctl.ctl_sequence();
|
||||
ASSERT_TRUE(seq);
|
||||
EXPECT_EQ(42u, *seq);
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, get_hashed_id8_matches_signer)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
auto ctl = TestRcaCtlBuilder(issuer, true, 1).build();
|
||||
auto got = ctl.get_hashed_id8(*m_security);
|
||||
ASSERT_TRUE(got);
|
||||
EXPECT_EQ(issuer, *got);
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, processor_records_aa_from_full_ctl)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
PublicKey aa_key = m_security->create_key(KeyType::NistP256);
|
||||
auto ctl = TestRcaCtlBuilder(issuer, true, 1).add_aa(aa_key, "https://aa.example/v1").build();
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(ctl);
|
||||
|
||||
auto aa = processor.get_authorization_authority(issuer);
|
||||
ASSERT_TRUE(aa);
|
||||
EXPECT_EQ("https://aa.example/v1", aa->access_point);
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, processor_records_ea_from_full_ctl)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
PublicKey ea_key = m_security->create_key(KeyType::NistP256);
|
||||
auto ctl = TestRcaCtlBuilder(issuer, true, 1).add_ea(ea_key, "https://ea.example/aa").build();
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(ctl);
|
||||
|
||||
auto ea = processor.get_enrolment_authority(issuer);
|
||||
ASSERT_TRUE(ea);
|
||||
EXPECT_EQ("https://ea.example/aa", ea->aa_access_point);
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, full_ctl_clears_prior_state)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
PublicKey key_a = m_security->create_key(KeyType::NistP256);
|
||||
PublicKey key_b = m_security->create_key(KeyType::NistP256);
|
||||
|
||||
auto ctl_first = TestRcaCtlBuilder(issuer, true, 1)
|
||||
.add_aa(key_a, "https://aa.example/v1")
|
||||
.add_ea(key_a, "https://ea.example/aa")
|
||||
.build();
|
||||
auto ctl_second =
|
||||
TestRcaCtlBuilder(issuer, true, 2).add_aa(key_b, "https://aa.example/v2").build(); // No EA in second full CTL
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(ctl_first);
|
||||
ASSERT_TRUE(processor.get_authorization_authority(issuer));
|
||||
ASSERT_TRUE(processor.get_enrolment_authority(issuer));
|
||||
|
||||
processor.process(ctl_second);
|
||||
auto aa = processor.get_authorization_authority(issuer);
|
||||
ASSERT_TRUE(aa);
|
||||
EXPECT_EQ("https://aa.example/v2", aa->access_point);
|
||||
// EA was in the first full CTL but absent from the second: must be gone.
|
||||
EXPECT_FALSE(processor.get_enrolment_authority(issuer));
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, delta_ctl_applies_on_top_of_prior_state)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
PublicKey ea_key = m_security->create_key(KeyType::NistP256);
|
||||
PublicKey aa_key = m_security->create_key(KeyType::NistP256);
|
||||
|
||||
auto full = TestRcaCtlBuilder(issuer, true, 1).add_ea(ea_key, "https://ea.example/aa").build();
|
||||
auto delta = TestRcaCtlBuilder(issuer, false, 2).add_aa(aa_key, "https://aa.example/v1").build();
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(full);
|
||||
processor.process(delta);
|
||||
|
||||
// EA from the full CTL must survive — delta does NOT clear.
|
||||
EXPECT_TRUE(processor.get_enrolment_authority(issuer));
|
||||
// AA was added by the delta.
|
||||
EXPECT_TRUE(processor.get_authorization_authority(issuer));
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, delta_ctl_can_delete_certificate)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
PublicKey aa_key = m_security->create_key(KeyType::NistP256);
|
||||
|
||||
auto full = TestRcaCtlBuilder(issuer, true, 1).add_aa(aa_key, "https://aa.example/v1").build();
|
||||
auto delta = TestRcaCtlBuilder(issuer, false, 2)
|
||||
.delete_cert(issuer) // remove the entry keyed by the issuer (the AA bucket)
|
||||
.build();
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(full);
|
||||
ASSERT_TRUE(processor.get_authorization_authority(issuer));
|
||||
|
||||
processor.process(delta);
|
||||
EXPECT_FALSE(processor.get_authorization_authority(issuer));
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, re_announced_aa_overwrites_previous)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
PublicKey key_a = m_security->create_key(KeyType::NistP256);
|
||||
PublicKey key_b = m_security->create_key(KeyType::NistP256);
|
||||
|
||||
auto ctl = TestRcaCtlBuilder(issuer, true, 1)
|
||||
.add_aa(key_a, "https://aa.example/v1")
|
||||
.add_aa(key_b, "https://aa.example/v2")
|
||||
.build();
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(ctl);
|
||||
|
||||
auto aa = processor.get_authorization_authority(issuer);
|
||||
ASSERT_TRUE(aa);
|
||||
// Last add wins (insert_or_assign).
|
||||
EXPECT_EQ("https://aa.example/v2", aa->access_point);
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, is_full_ctl_works_for_tlm_ctl)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xCD);
|
||||
auto ctl = TestTlmCtlBuilder(issuer, true, 7).build();
|
||||
auto is_full = ctl.is_full_ctl();
|
||||
ASSERT_TRUE(is_full);
|
||||
EXPECT_TRUE(*is_full);
|
||||
auto seq = ctl.ctl_sequence();
|
||||
ASSERT_TRUE(seq);
|
||||
EXPECT_EQ(7u, *seq);
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, processor_records_root_ca_from_tlm_ctl)
|
||||
{
|
||||
HashedId8 tlm_issuer = make_id(0xCD);
|
||||
PublicKey rca_key = m_security->create_key(KeyType::NistP256);
|
||||
auto ectl = TestTlmCtlBuilder(tlm_issuer, true, 1).add_root_ca(rca_key).build();
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(ectl);
|
||||
|
||||
// The RCA's HashedId8 is computed from the cert itself (not the issuer).
|
||||
// Locate it via the same path the processor uses internally.
|
||||
Certificate stub = build_stub_certificate(rca_key);
|
||||
HashedId8 rca_hid = stub.calculate_hashed_id8(*m_security);
|
||||
|
||||
EXPECT_TRUE(processor.get_root_ca(rca_hid));
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, processor_records_tlm_from_tlm_ctl)
|
||||
{
|
||||
HashedId8 tlm_issuer = make_id(0xCD);
|
||||
PublicKey tlm_key = m_security->create_key(KeyType::NistP256);
|
||||
auto ectl = TestTlmCtlBuilder(tlm_issuer, true, 1).add_tlm(tlm_key, "https://cpoc.example/").build();
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(ectl);
|
||||
|
||||
Certificate stub = build_stub_certificate(tlm_key);
|
||||
HashedId8 tlm_hid = stub.calculate_hashed_id8(*m_security);
|
||||
|
||||
EXPECT_TRUE(processor.get_trust_list_manager(tlm_hid));
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, full_tlm_ctl_clears_prior_root_cas)
|
||||
{
|
||||
HashedId8 tlm_issuer = make_id(0xCD);
|
||||
PublicKey rca_a_key = m_security->create_key(KeyType::NistP256);
|
||||
PublicKey rca_b_key = m_security->create_key(KeyType::NistP256);
|
||||
|
||||
Certificate rca_a_stub = build_stub_certificate(rca_a_key);
|
||||
HashedId8 rca_a_hid = rca_a_stub.calculate_hashed_id8(*m_security);
|
||||
|
||||
auto first = TestTlmCtlBuilder(tlm_issuer, true, 1).add_root_ca(rca_a_key).build();
|
||||
auto second = TestTlmCtlBuilder(tlm_issuer, true, 2).add_root_ca(rca_b_key).build();
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(first);
|
||||
ASSERT_TRUE(processor.get_root_ca(rca_a_hid));
|
||||
|
||||
processor.process(second);
|
||||
// RCA A was in the first full ECTL but absent from the second: must be gone.
|
||||
EXPECT_FALSE(processor.get_root_ca(rca_a_hid));
|
||||
}
|
||||
|
||||
TEST_F(CertificateTrustListTest, delta_tlm_ctl_can_delete_root_ca)
|
||||
{
|
||||
HashedId8 tlm_issuer = make_id(0xCD);
|
||||
PublicKey rca_key = m_security->create_key(KeyType::NistP256);
|
||||
Certificate rca_stub = build_stub_certificate(rca_key);
|
||||
HashedId8 rca_hid = rca_stub.calculate_hashed_id8(*m_security);
|
||||
|
||||
auto full = TestTlmCtlBuilder(tlm_issuer, true, 1).add_root_ca(rca_key).build();
|
||||
auto delta = TestTlmCtlBuilder(tlm_issuer, false, 2).delete_cert(rca_hid).build();
|
||||
|
||||
CertificateTrustListProcessor processor(m_security);
|
||||
processor.process(full);
|
||||
ASSERT_TRUE(processor.get_root_ca(rca_hid));
|
||||
|
||||
processor.process(delta);
|
||||
EXPECT_FALSE(processor.get_root_ca(rca_hid));
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
#include "credential_filesystem_storage.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <fstream>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
using namespace vanetza::pki;
|
||||
|
||||
static const std::string privKeyPem =
|
||||
"-----BEGIN EC PRIVATE KEY-----\n"
|
||||
"MHcCAQEEIHlpKyVDNCQ2x5IFeBelQyyvANI8iTFIoCTfNARm1LVxoAoGCCqGSM49\n"
|
||||
"AwEHoUQDQgAEvfkk5CcwIqP29fGu9n6kXmojUpV2xUlnM2BwJMoYmiVU7lF9p7tu\n"
|
||||
"+KUbGlhPaYLN0UZ13KQTmi3gV3aLolyXUw==\n"
|
||||
"-----END EC PRIVATE KEY-----";
|
||||
|
||||
static const vanetza::ByteBuffer privKeyRaw = {
|
||||
0x18, 0xab, 0x8e, 0xcd, 0xf6, 0x5f, 0xb8, 0x06,
|
||||
0x3b, 0x24, 0xe5, 0xd6, 0x77, 0xf9, 0x47, 0x6a,
|
||||
0xd6, 0xef, 0x7b, 0x54, 0x0b, 0x8f, 0x2a, 0x15,
|
||||
0xe0, 0x09, 0x7c, 0x23, 0xe9, 0xf2, 0x73, 0x11
|
||||
};
|
||||
|
||||
static const vanetza::ByteBuffer pubKeyRawX = {
|
||||
0x54, 0x80, 0xd4, 0x56, 0x9e, 0x50, 0x4d, 0x8e,
|
||||
0x1a, 0x14, 0xcc, 0x0e, 0xd7, 0xea, 0xd0, 0xcf,
|
||||
0x13, 0x49, 0xb8, 0x84, 0xa9, 0xb7, 0x9b, 0xe5,
|
||||
0x2f, 0x4a, 0x4c, 0xe1, 0x62, 0xa3, 0x75, 0xe4
|
||||
};
|
||||
|
||||
static const vanetza::ByteBuffer pubKeyRawY = {
|
||||
0x95, 0x01, 0x22, 0x47, 0xb5, 0x7a, 0x32, 0xc0,
|
||||
0xd9, 0x3c, 0x61, 0x00, 0xd7, 0xe6, 0x7e, 0xe9,
|
||||
0x24, 0xe1, 0x40, 0x84, 0x77, 0x12, 0x7e, 0xa6,
|
||||
0x9f, 0x23, 0x7b, 0xda, 0x40, 0xd9, 0x09, 0x32
|
||||
};
|
||||
|
||||
class CredentialFilesystemStorageTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
root = std::filesystem::temp_directory_path() / "pki_unit_test" / "credentials";
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
|
||||
std::filesystem::path root;
|
||||
};
|
||||
|
||||
TEST_F(CredentialFilesystemStorageTest, store_and_fetch)
|
||||
{
|
||||
CredentialFilesystemStorage storage(root);
|
||||
PublicKey pub;
|
||||
pub.type = KeyType::BrainpoolP256r1;
|
||||
pub.compression = KeyCompression::NoCompression;
|
||||
pub.x = pubKeyRawX;
|
||||
pub.y = pubKeyRawY;
|
||||
|
||||
PrivateKey priv;
|
||||
priv.key = privKeyRaw;
|
||||
priv.type = KeyType::BrainpoolP256r1;
|
||||
|
||||
const std::string filename = "025480D4569E504D8E1A14CC0ED7EAD0CF1349B884A9B79BE52F4A4CE162A375E4.pem";
|
||||
std::filesystem::remove(root / filename);
|
||||
EXPECT_FALSE(std::filesystem::is_regular_file(root / filename));
|
||||
EXPECT_FALSE(storage.fetch(pub));
|
||||
storage.store(pub, priv);
|
||||
boost::optional<PrivateKey> fetched = storage.fetch(pub);
|
||||
ASSERT_TRUE(fetched);
|
||||
EXPECT_EQ(priv.type, fetched->type);
|
||||
EXPECT_EQ(priv.key, fetched->key);
|
||||
EXPECT_TRUE(std::filesystem::is_regular_file(root / filename));
|
||||
}
|
||||
|
||||
TEST_F(CredentialFilesystemStorageTest, list_yields_canonical_hex_stems_only)
|
||||
{
|
||||
std::filesystem::create_directories(root);
|
||||
// Three valid-looking .pem files (P-256 stems = "02"/"03" + 64 hex chars).
|
||||
std::ofstream(root / "025480D4569E504D8E1A14CC0ED7EAD0CF1349B884A9B79BE52F4A4CE162A375E4.pem").put('x');
|
||||
std::ofstream(root / "030000000000000000000000000000000000000000000000000000000000000000.pem").put('x');
|
||||
std::ofstream(root / "021111111111111111111111111111111111111111111111111111111111111111.pem").put('x');
|
||||
// Decoys that must not appear in list():
|
||||
std::ofstream(root / "ignore.txt").put('x'); // wrong extension
|
||||
std::ofstream(root / "garbage.pem").put('x'); // wrong stem format
|
||||
std::ofstream(root / "025480D4569E504D8E1A14CC0ED7EAD0CF1349B884A9B79BE52F4A4CE162A375.pem")
|
||||
.put('x'); // 64 chars (one short)
|
||||
std::filesystem::create_directory(root / "subdir");
|
||||
|
||||
CredentialFilesystemStorage storage(root);
|
||||
std::set<std::string> seen;
|
||||
for (const auto& name : storage.list()) {
|
||||
seen.insert(name);
|
||||
}
|
||||
|
||||
EXPECT_EQ(3u, seen.size());
|
||||
EXPECT_TRUE(seen.count("025480D4569E504D8E1A14CC0ED7EAD0CF1349B884A9B79BE52F4A4CE162A375E4"));
|
||||
EXPECT_TRUE(seen.count("030000000000000000000000000000000000000000000000000000000000000000"));
|
||||
EXPECT_TRUE(seen.count("021111111111111111111111111111111111111111111111111111111111111111"));
|
||||
}
|
||||
|
||||
TEST_F(CredentialFilesystemStorageTest, list_on_empty_directory)
|
||||
{
|
||||
std::filesystem::create_directories(root);
|
||||
CredentialFilesystemStorage storage(root);
|
||||
std::vector<std::string> names;
|
||||
for (const auto& n : storage.list()) {
|
||||
names.push_back(n);
|
||||
}
|
||||
EXPECT_TRUE(names.empty());
|
||||
}
|
||||
|
||||
TEST_F(CredentialFilesystemStorageTest, discard_by_name_removes_file)
|
||||
{
|
||||
std::filesystem::create_directories(root);
|
||||
const std::string stem = "030000000000000000000000000000000000000000000000000000000000000000";
|
||||
std::ofstream(root / (stem + ".pem")).put('x');
|
||||
ASSERT_TRUE(std::filesystem::is_regular_file(root / (stem + ".pem")));
|
||||
|
||||
CredentialFilesystemStorage storage(root);
|
||||
EXPECT_TRUE(storage.discard(stem));
|
||||
EXPECT_FALSE(std::filesystem::is_regular_file(root / (stem + ".pem")));
|
||||
}
|
||||
|
||||
TEST_F(CredentialFilesystemStorageTest, discard_by_name_returns_false_for_unknown)
|
||||
{
|
||||
std::filesystem::create_directories(root);
|
||||
CredentialFilesystemStorage storage(root);
|
||||
EXPECT_FALSE(storage.discard("020000000000000000000000000000000000000000000000000000000000000000"));
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
#include "crl_store.hpp"
|
||||
#include "certificate_revocation_list.hpp"
|
||||
#include "crl_test_fixture.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <fstream>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
class CrlFilesystemStoreTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
CrlFilesystemStoreTest() :
|
||||
m_credentials(std::make_shared<MockCredentialStorage>()),
|
||||
m_security(std::make_shared<OpenSslSecurityModule>(m_credentials))
|
||||
{
|
||||
}
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
m_root = std::filesystem::temp_directory_path() / "pki_unit_test" / "crl_store";
|
||||
std::filesystem::remove_all(m_root);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
std::filesystem::remove_all(m_root);
|
||||
}
|
||||
|
||||
std::shared_ptr<MockCredentialStorage> m_credentials;
|
||||
std::shared_ptr<OpenSslSecurityModule> m_security;
|
||||
std::filesystem::path m_root;
|
||||
};
|
||||
|
||||
TEST_F(CrlFilesystemStoreTest, store_and_lookup_roundtrip)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
HashedId8 revoked = make_id(0x42);
|
||||
CertificateRevocationList crl = build_test_crl(issuer, { revoked });
|
||||
|
||||
CrlFilesystemStore store(m_security, m_root);
|
||||
ASSERT_TRUE(store.store(crl));
|
||||
EXPECT_TRUE(store.is_revoked(issuer.octets, revoked.octets));
|
||||
}
|
||||
|
||||
TEST_F(CrlFilesystemStoreTest, is_revoked_returns_false_for_unknown_issuer)
|
||||
{
|
||||
CrlFilesystemStore store(m_security, m_root);
|
||||
HashedId8 known_issuer = make_id(0xAB);
|
||||
HashedId8 revoked = make_id(0x42);
|
||||
ASSERT_TRUE(store.store(build_test_crl(known_issuer, { revoked })));
|
||||
|
||||
HashedId8 other_issuer = make_id(0xCD);
|
||||
EXPECT_FALSE(store.is_revoked(other_issuer.octets, revoked.octets));
|
||||
}
|
||||
|
||||
TEST_F(CrlFilesystemStoreTest, is_revoked_returns_false_for_unlisted_cert)
|
||||
{
|
||||
CrlFilesystemStore store(m_security, m_root);
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
ASSERT_TRUE(store.store(build_test_crl(issuer, { make_id(0x42) })));
|
||||
|
||||
HashedId8 other_cert = make_id(0x99);
|
||||
EXPECT_FALSE(store.is_revoked(issuer.octets, other_cert.octets));
|
||||
}
|
||||
|
||||
TEST_F(CrlFilesystemStoreTest, empty_crl_clears_prior_entries)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
HashedId8 revoked = make_id(0x42);
|
||||
|
||||
CrlFilesystemStore store(m_security, m_root);
|
||||
ASSERT_TRUE(store.store(build_test_crl(issuer, { revoked })));
|
||||
ASSERT_TRUE(store.is_revoked(issuer.octets, revoked.octets));
|
||||
|
||||
ASSERT_TRUE(store.store(build_test_crl(issuer, {})));
|
||||
EXPECT_FALSE(store.is_revoked(issuer.octets, revoked.octets));
|
||||
}
|
||||
|
||||
TEST_F(CrlFilesystemStoreTest, constructor_loads_existing_crl_files)
|
||||
{
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
HashedId8 revoked = make_id(0x42);
|
||||
|
||||
{
|
||||
CrlFilesystemStore writer(m_security, m_root);
|
||||
ASSERT_TRUE(writer.store(build_test_crl(issuer, { revoked })));
|
||||
}
|
||||
|
||||
CrlFilesystemStore reader(m_security, m_root);
|
||||
EXPECT_TRUE(reader.is_revoked(issuer.octets, revoked.octets));
|
||||
}
|
||||
|
||||
TEST_F(CrlFilesystemStoreTest, constructor_ignores_non_crl_files_and_garbage)
|
||||
{
|
||||
std::filesystem::create_directories(m_root);
|
||||
std::ofstream(m_root / "not_a_crl.txt") << "ignored";
|
||||
std::ofstream(m_root / "garbage.crl") << "\x00\x01\x02";
|
||||
|
||||
HashedId8 issuer = make_id(0xAB);
|
||||
HashedId8 revoked = make_id(0x42);
|
||||
{
|
||||
CrlFilesystemStore writer(m_security, m_root);
|
||||
ASSERT_TRUE(writer.store(build_test_crl(issuer, { revoked })));
|
||||
}
|
||||
|
||||
CrlFilesystemStore reader(m_security, m_root);
|
||||
EXPECT_TRUE(reader.is_revoked(issuer.octets, revoked.octets));
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
|
||||
#include "asn1.hpp"
|
||||
#include "certificate_revocation_list.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include <vanetza/asn1/asn1c_wrapper.hpp>
|
||||
#include <vanetza/asn1/security/EtsiTs102941Data.h>
|
||||
#include <vanetza/asn1/security/EtsiTs103097Data.h>
|
||||
#include <vanetza/asn1/security/Ieee1609Dot2Content.h>
|
||||
#include <vanetza/asn1/security/SignedData.h>
|
||||
#include <vanetza/asn1/security/ToBeSignedCrl.h>
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
#include <array>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
// Build a CertificateRevocationList carrying \p revoked entries, signed (in
|
||||
// the SignerIdentifier sense) by the digest \p issuer. The ECDSA signature
|
||||
// itself is dummy zeros — sufficient for tests that don't verify it.
|
||||
inline CertificateRevocationList build_test_crl(const HashedId8& issuer, const std::vector<HashedId8>& revoked)
|
||||
{
|
||||
// The signed payload is an EtsiTs102941Data whose content CHOICE carries the
|
||||
// ToBeSignedCrl — matching CertificateRevocationList::revoked_entries().
|
||||
ByteBuffer tbs_bytes;
|
||||
{
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> mgmt(asn_DEF_Vanetza_Security_EtsiTs102941Data);
|
||||
mgmt->version = 1;
|
||||
mgmt->content.present = Vanetza_Security_EtsiTs102941DataContent_PR_certificateRevocationList;
|
||||
Vanetza_Security_ToBeSignedCrl_t& tbs = mgmt->content.choice.certificateRevocationList;
|
||||
tbs.version = 1;
|
||||
tbs.thisUpdate = 0;
|
||||
tbs.nextUpdate = 0;
|
||||
for (const auto& id : revoked) {
|
||||
auto* entry = asn1::allocate<Vanetza_Security_CrlEntry_t>();
|
||||
OCTET_STRING_fromBuf(entry, reinterpret_cast<const char*>(id.octets.data()), id.octets.size());
|
||||
asn_sequence_add(&tbs.entries, entry);
|
||||
}
|
||||
tbs_bytes = mgmt.encode();
|
||||
}
|
||||
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_CertificateRevocationListMessage_t>
|
||||
outer(asn_DEF_Vanetza_Security_CertificateRevocationListMessage);
|
||||
outer->protocolVersion = 3;
|
||||
outer->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
outer->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_signedData;
|
||||
|
||||
auto* signed_data = asn1::allocate<Vanetza_Security_SignedData_t>();
|
||||
outer->content->choice.signedData = signed_data;
|
||||
signed_data->hashId = Vanetza_Security_HashAlgorithm_sha256;
|
||||
|
||||
signed_data->signer.present = Vanetza_Security_SignerIdentifier_PR_digest;
|
||||
OCTET_STRING_fromBuf(&signed_data->signer.choice.digest, reinterpret_cast<const char*>(issuer.octets.data()),
|
||||
issuer.octets.size());
|
||||
|
||||
signed_data->tbsData = asn1::allocate<Vanetza_Security_ToBeSignedData_t>();
|
||||
signed_data->tbsData->headerInfo.psid = aid::CRL;
|
||||
signed_data->tbsData->payload = asn1::allocate<Vanetza_Security_SignedDataPayload_t>();
|
||||
auto* inner = asn1::allocate<Vanetza_Security_EtsiTs103097Data_t>();
|
||||
signed_data->tbsData->payload->data = inner;
|
||||
inner->protocolVersion = 3;
|
||||
inner->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
inner->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_unsecuredData;
|
||||
OCTET_STRING_fromBuf(&inner->content->choice.unsecuredData, reinterpret_cast<const char*>(tbs_bytes.data()),
|
||||
tbs_bytes.size());
|
||||
|
||||
signed_data->signature.present = Vanetza_Security_Signature_PR_ecdsaNistP256Signature;
|
||||
signed_data->signature.choice.ecdsaNistP256Signature.rSig.present = Vanetza_Security_EccP256CurvePoint_PR_x_only;
|
||||
std::array<std::uint8_t, 32> zeros {};
|
||||
OCTET_STRING_fromBuf(&signed_data->signature.choice.ecdsaNistP256Signature.rSig.choice.x_only,
|
||||
reinterpret_cast<const char*>(zeros.data()), zeros.size());
|
||||
OCTET_STRING_fromBuf(&signed_data->signature.choice.ecdsaNistP256Signature.sSig,
|
||||
reinterpret_cast<const char*>(zeros.data()), zeros.size());
|
||||
|
||||
ByteBuffer outer_bytes = outer.encode();
|
||||
CertificateRevocationList crl;
|
||||
if (!crl.decode(outer_bytes)) {
|
||||
throw std::runtime_error("test fixture: outer CRL message could not be decoded");
|
||||
}
|
||||
return crl;
|
||||
}
|
||||
|
||||
inline HashedId8 make_id(std::uint8_t fill)
|
||||
{
|
||||
HashedId8 id;
|
||||
id.octets.fill(fill);
|
||||
return id;
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,313 @@
|
||||
#pragma once
|
||||
|
||||
#include "asn1.hpp"
|
||||
#include "certificate_trust_list.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "keys.hpp"
|
||||
#include <vanetza/asn1/asn1c_wrapper.hpp>
|
||||
#include <vanetza/asn1/security/CtlCommand.h>
|
||||
#include <vanetza/asn1/security/EtsiTs102941Data.h>
|
||||
#include <vanetza/asn1/security/EtsiTs103097Data.h>
|
||||
#include <vanetza/asn1/security/Ieee1609Dot2Content.h>
|
||||
#include <vanetza/asn1/security/RcaCertificateTrustListMessage.h>
|
||||
#include <vanetza/asn1/security/SignedData.h>
|
||||
#include <vanetza/asn1/security/TlmCertificateTrustListMessage.h>
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
#include <vanetza/security/v3/basic_elements.hpp>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
// Builds an RCA-signed CTL message. Internal: fills an EtsiTs102941Data with a
|
||||
// CtlFormat, OER-encodes it, wraps in a signed message with the issuer's
|
||||
// HashedId8 as signer (dummy signature). decode() the result into a
|
||||
// CertificateTrustList for use in tests.
|
||||
class TestRcaCtlBuilder
|
||||
{
|
||||
public:
|
||||
TestRcaCtlBuilder(const HashedId8& issuer, bool is_full, std::uint8_t sequence) : m_issuer(issuer)
|
||||
{
|
||||
m_data->version = 1;
|
||||
m_data->content.present = Vanetza_Security_EtsiTs102941DataContent_PR_certificateTrustListRca;
|
||||
auto& format = m_data->content.choice.certificateTrustListRca;
|
||||
format.version = 1;
|
||||
format.nextUpdate = 0;
|
||||
format.isFullCtl = is_full ? 1 : 0;
|
||||
format.ctlSequence = sequence;
|
||||
}
|
||||
|
||||
TestRcaCtlBuilder& add_aa(const PublicKey& key, const std::string& url)
|
||||
{
|
||||
auto* cmd = asn1::allocate<Vanetza_Security_CtlCommand_t>();
|
||||
cmd->present = Vanetza_Security_CtlCommand_PR_add;
|
||||
cmd->choice.add.present = Vanetza_Security_CtlEntry_PR_aa;
|
||||
auto& aa = cmd->choice.add.choice.aa;
|
||||
populate_stub_cert(aa.aaCertificate, key, "test-aa");
|
||||
OCTET_STRING_fromBuf(&aa.accessPoint, url.data(), url.size());
|
||||
asn_sequence_add(&m_data->content.choice.certificateTrustListRca.ctlCommands, cmd);
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestRcaCtlBuilder& add_ea(const PublicKey& key, const std::string& aa_access_point)
|
||||
{
|
||||
auto* cmd = asn1::allocate<Vanetza_Security_CtlCommand_t>();
|
||||
cmd->present = Vanetza_Security_CtlCommand_PR_add;
|
||||
cmd->choice.add.present = Vanetza_Security_CtlEntry_PR_ea;
|
||||
auto& ea = cmd->choice.add.choice.ea;
|
||||
populate_stub_cert(ea.eaCertificate, key, "test-ea");
|
||||
OCTET_STRING_fromBuf(&ea.aaAccessPoint, aa_access_point.data(), aa_access_point.size());
|
||||
// itsAccessPoint left null (OPTIONAL)
|
||||
asn_sequence_add(&m_data->content.choice.certificateTrustListRca.ctlCommands, cmd);
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestRcaCtlBuilder& add_dc(const std::string& url, const std::vector<HashedId8>& certs)
|
||||
{
|
||||
auto* cmd = asn1::allocate<Vanetza_Security_CtlCommand_t>();
|
||||
cmd->present = Vanetza_Security_CtlCommand_PR_add;
|
||||
cmd->choice.add.present = Vanetza_Security_CtlEntry_PR_dc;
|
||||
auto& dc = cmd->choice.add.choice.dc;
|
||||
OCTET_STRING_fromBuf(&dc.url, url.data(), url.size());
|
||||
for (const auto& id : certs) {
|
||||
auto* entry = asn1::allocate<Vanetza_Security_HashedId8_t>();
|
||||
OCTET_STRING_fromBuf(entry, reinterpret_cast<const char*>(id.octets.data()), id.octets.size());
|
||||
asn_sequence_add(&dc.cert, entry);
|
||||
}
|
||||
asn_sequence_add(&m_data->content.choice.certificateTrustListRca.ctlCommands, cmd);
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestRcaCtlBuilder& delete_cert(const HashedId8& id)
|
||||
{
|
||||
auto* cmd = asn1::allocate<Vanetza_Security_CtlCommand_t>();
|
||||
cmd->present = Vanetza_Security_CtlCommand_PR_delete;
|
||||
cmd->choice.Delete.present = Vanetza_Security_CtlDelete_PR_cert;
|
||||
OCTET_STRING_fromBuf(&cmd->choice.Delete.choice.cert, reinterpret_cast<const char*>(id.octets.data()),
|
||||
id.octets.size());
|
||||
asn_sequence_add(&m_data->content.choice.certificateTrustListRca.ctlCommands, cmd);
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestRcaCtlBuilder& delete_dc(const std::string& url)
|
||||
{
|
||||
auto* cmd = asn1::allocate<Vanetza_Security_CtlCommand_t>();
|
||||
cmd->present = Vanetza_Security_CtlCommand_PR_delete;
|
||||
cmd->choice.Delete.present = Vanetza_Security_CtlDelete_PR_dc;
|
||||
OCTET_STRING_fromBuf(&cmd->choice.Delete.choice.dc, url.data(), url.size());
|
||||
asn_sequence_add(&m_data->content.choice.certificateTrustListRca.ctlCommands, cmd);
|
||||
return *this;
|
||||
}
|
||||
|
||||
CertificateTrustList build()
|
||||
{
|
||||
ByteBuffer inner_bytes = m_data.encode();
|
||||
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_RcaCertificateTrustListMessage_t>
|
||||
outer(asn_DEF_Vanetza_Security_RcaCertificateTrustListMessage);
|
||||
outer->protocolVersion = 3;
|
||||
outer->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
outer->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_signedData;
|
||||
|
||||
auto* signed_data = asn1::allocate<Vanetza_Security_SignedData_t>();
|
||||
outer->content->choice.signedData = signed_data;
|
||||
signed_data->hashId = Vanetza_Security_HashAlgorithm_sha256;
|
||||
|
||||
signed_data->signer.present = Vanetza_Security_SignerIdentifier_PR_digest;
|
||||
OCTET_STRING_fromBuf(&signed_data->signer.choice.digest, reinterpret_cast<const char*>(m_issuer.octets.data()),
|
||||
m_issuer.octets.size());
|
||||
|
||||
signed_data->tbsData = asn1::allocate<Vanetza_Security_ToBeSignedData_t>();
|
||||
signed_data->tbsData->headerInfo.psid = aid::CTL;
|
||||
signed_data->tbsData->payload = asn1::allocate<Vanetza_Security_SignedDataPayload_t>();
|
||||
auto* inner_data = asn1::allocate<Vanetza_Security_EtsiTs103097Data_t>();
|
||||
signed_data->tbsData->payload->data = inner_data;
|
||||
inner_data->protocolVersion = 3;
|
||||
inner_data->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
inner_data->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_unsecuredData;
|
||||
OCTET_STRING_fromBuf(&inner_data->content->choice.unsecuredData,
|
||||
reinterpret_cast<const char*>(inner_bytes.data()), inner_bytes.size());
|
||||
|
||||
signed_data->signature.present = Vanetza_Security_Signature_PR_ecdsaNistP256Signature;
|
||||
signed_data->signature.choice.ecdsaNistP256Signature.rSig.present =
|
||||
Vanetza_Security_EccP256CurvePoint_PR_x_only;
|
||||
std::array<std::uint8_t, 32> zeros {};
|
||||
OCTET_STRING_fromBuf(&signed_data->signature.choice.ecdsaNistP256Signature.rSig.choice.x_only,
|
||||
reinterpret_cast<const char*>(zeros.data()), zeros.size());
|
||||
OCTET_STRING_fromBuf(&signed_data->signature.choice.ecdsaNistP256Signature.sSig,
|
||||
reinterpret_cast<const char*>(zeros.data()), zeros.size());
|
||||
|
||||
ByteBuffer outer_bytes = outer.encode();
|
||||
CertificateTrustList ctl;
|
||||
if (!ctl.decode(outer_bytes)) {
|
||||
throw std::runtime_error("CTL test fixture: outer message could not be decoded");
|
||||
}
|
||||
return ctl;
|
||||
}
|
||||
|
||||
private:
|
||||
static void populate_stub_cert(Vanetza_Security_EtsiTs103097Certificate_t& cert, const PublicKey& key,
|
||||
const char* name)
|
||||
{
|
||||
cert.version = 3;
|
||||
cert.type = Vanetza_Security_CertificateType_explicit;
|
||||
cert.issuer.present = Vanetza_Security_IssuerIdentifier_PR_self;
|
||||
cert.issuer.choice.self = Vanetza_Security_HashAlgorithm_sha256;
|
||||
|
||||
auto& tbs = cert.toBeSigned;
|
||||
tbs.id.present = Vanetza_Security_CertificateId_PR_name;
|
||||
OCTET_STRING_fromBuf(&tbs.id.choice.name, name, std::strlen(name));
|
||||
tbs.cracaId.buf = static_cast<std::uint8_t*>(std::calloc(3, 1));
|
||||
tbs.cracaId.size = 3;
|
||||
tbs.crlSeries = 0;
|
||||
tbs.validityPeriod.start = security::v3::convert_time32(Clock::time_point {});
|
||||
tbs.validityPeriod.duration.present = Vanetza_Security_Duration_PR_hours;
|
||||
tbs.validityPeriod.duration.choice.hours = 24;
|
||||
tbs.verifyKeyIndicator.present = Vanetza_Security_VerificationKeyIndicator_PR_verificationKey;
|
||||
set_verification_key(tbs.verifyKeyIndicator.choice.verificationKey, key);
|
||||
}
|
||||
|
||||
HashedId8 m_issuer;
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> m_data { asn_DEF_Vanetza_Security_EtsiTs102941Data };
|
||||
};
|
||||
|
||||
inline HashedId8 make_id(std::uint8_t fill)
|
||||
{
|
||||
HashedId8 id;
|
||||
id.octets.fill(fill);
|
||||
return id;
|
||||
}
|
||||
|
||||
// Builds a TLM-signed CTL message (ECTL). Mirrors TestRcaCtlBuilder but for
|
||||
// the certificateTrustListTlm variant; entries are RootCaEntry / TlmEntry / DcEntry.
|
||||
class TestTlmCtlBuilder
|
||||
{
|
||||
public:
|
||||
TestTlmCtlBuilder(const HashedId8& issuer, bool is_full, std::uint8_t sequence) : m_issuer(issuer)
|
||||
{
|
||||
m_data->version = 1;
|
||||
m_data->content.present = Vanetza_Security_EtsiTs102941DataContent_PR_certificateTrustListTlm;
|
||||
auto& format = m_data->content.choice.certificateTrustListTlm;
|
||||
format.version = 1;
|
||||
format.nextUpdate = 0;
|
||||
format.isFullCtl = is_full ? 1 : 0;
|
||||
format.ctlSequence = sequence;
|
||||
}
|
||||
|
||||
TestTlmCtlBuilder& add_root_ca(const PublicKey& key)
|
||||
{
|
||||
auto* cmd = asn1::allocate<Vanetza_Security_CtlCommand_t>();
|
||||
cmd->present = Vanetza_Security_CtlCommand_PR_add;
|
||||
cmd->choice.add.present = Vanetza_Security_CtlEntry_PR_rca;
|
||||
auto& rca = cmd->choice.add.choice.rca;
|
||||
// Name matches build_stub_certificate so tests can derive the same HashedId8.
|
||||
populate_stub_cert(rca.selfsignedRootCa, key, "test-cert");
|
||||
asn_sequence_add(&m_data->content.choice.certificateTrustListTlm.ctlCommands, cmd);
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestTlmCtlBuilder& add_tlm(const PublicKey& key, const std::string& cpoc_url)
|
||||
{
|
||||
auto* cmd = asn1::allocate<Vanetza_Security_CtlCommand_t>();
|
||||
cmd->present = Vanetza_Security_CtlCommand_PR_add;
|
||||
cmd->choice.add.present = Vanetza_Security_CtlEntry_PR_tlm;
|
||||
auto& tlm = cmd->choice.add.choice.tlm;
|
||||
populate_stub_cert(tlm.selfSignedTLMCertificate, key, "test-cert");
|
||||
OCTET_STRING_fromBuf(&tlm.accessPoint, cpoc_url.data(), cpoc_url.size());
|
||||
asn_sequence_add(&m_data->content.choice.certificateTrustListTlm.ctlCommands, cmd);
|
||||
return *this;
|
||||
}
|
||||
|
||||
TestTlmCtlBuilder& delete_cert(const HashedId8& id)
|
||||
{
|
||||
auto* cmd = asn1::allocate<Vanetza_Security_CtlCommand_t>();
|
||||
cmd->present = Vanetza_Security_CtlCommand_PR_delete;
|
||||
cmd->choice.Delete.present = Vanetza_Security_CtlDelete_PR_cert;
|
||||
OCTET_STRING_fromBuf(&cmd->choice.Delete.choice.cert, reinterpret_cast<const char*>(id.octets.data()),
|
||||
id.octets.size());
|
||||
asn_sequence_add(&m_data->content.choice.certificateTrustListTlm.ctlCommands, cmd);
|
||||
return *this;
|
||||
}
|
||||
|
||||
CertificateTrustList build()
|
||||
{
|
||||
ByteBuffer inner_bytes = m_data.encode();
|
||||
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_TlmCertificateTrustListMessage_t>
|
||||
outer(asn_DEF_Vanetza_Security_TlmCertificateTrustListMessage);
|
||||
outer->protocolVersion = 3;
|
||||
outer->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
outer->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_signedData;
|
||||
|
||||
auto* signed_data = asn1::allocate<Vanetza_Security_SignedData_t>();
|
||||
outer->content->choice.signedData = signed_data;
|
||||
signed_data->hashId = Vanetza_Security_HashAlgorithm_sha256;
|
||||
|
||||
signed_data->signer.present = Vanetza_Security_SignerIdentifier_PR_digest;
|
||||
OCTET_STRING_fromBuf(&signed_data->signer.choice.digest, reinterpret_cast<const char*>(m_issuer.octets.data()),
|
||||
m_issuer.octets.size());
|
||||
|
||||
signed_data->tbsData = asn1::allocate<Vanetza_Security_ToBeSignedData_t>();
|
||||
signed_data->tbsData->headerInfo.psid = aid::CTL;
|
||||
signed_data->tbsData->payload = asn1::allocate<Vanetza_Security_SignedDataPayload_t>();
|
||||
auto* inner_data = asn1::allocate<Vanetza_Security_EtsiTs103097Data_t>();
|
||||
signed_data->tbsData->payload->data = inner_data;
|
||||
inner_data->protocolVersion = 3;
|
||||
inner_data->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
inner_data->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_unsecuredData;
|
||||
OCTET_STRING_fromBuf(&inner_data->content->choice.unsecuredData,
|
||||
reinterpret_cast<const char*>(inner_bytes.data()), inner_bytes.size());
|
||||
|
||||
signed_data->signature.present = Vanetza_Security_Signature_PR_ecdsaNistP256Signature;
|
||||
signed_data->signature.choice.ecdsaNistP256Signature.rSig.present =
|
||||
Vanetza_Security_EccP256CurvePoint_PR_x_only;
|
||||
std::array<std::uint8_t, 32> zeros {};
|
||||
OCTET_STRING_fromBuf(&signed_data->signature.choice.ecdsaNistP256Signature.rSig.choice.x_only,
|
||||
reinterpret_cast<const char*>(zeros.data()), zeros.size());
|
||||
OCTET_STRING_fromBuf(&signed_data->signature.choice.ecdsaNistP256Signature.sSig,
|
||||
reinterpret_cast<const char*>(zeros.data()), zeros.size());
|
||||
|
||||
ByteBuffer outer_bytes = outer.encode();
|
||||
CertificateTrustList ctl;
|
||||
if (!ctl.decode(outer_bytes)) {
|
||||
throw std::runtime_error("TLM CTL test fixture: outer message could not be decoded");
|
||||
}
|
||||
return ctl;
|
||||
}
|
||||
|
||||
private:
|
||||
static void populate_stub_cert(Vanetza_Security_EtsiTs103097Certificate_t& cert, const PublicKey& key,
|
||||
const char* name)
|
||||
{
|
||||
cert.version = 3;
|
||||
cert.type = Vanetza_Security_CertificateType_explicit;
|
||||
cert.issuer.present = Vanetza_Security_IssuerIdentifier_PR_self;
|
||||
cert.issuer.choice.self = Vanetza_Security_HashAlgorithm_sha256;
|
||||
|
||||
auto& tbs = cert.toBeSigned;
|
||||
tbs.id.present = Vanetza_Security_CertificateId_PR_name;
|
||||
OCTET_STRING_fromBuf(&tbs.id.choice.name, name, std::strlen(name));
|
||||
tbs.cracaId.buf = static_cast<std::uint8_t*>(std::calloc(3, 1));
|
||||
tbs.cracaId.size = 3;
|
||||
tbs.crlSeries = 0;
|
||||
tbs.validityPeriod.start = security::v3::convert_time32(Clock::time_point {});
|
||||
tbs.validityPeriod.duration.present = Vanetza_Security_Duration_PR_hours;
|
||||
tbs.validityPeriod.duration.choice.hours = 24;
|
||||
tbs.verifyKeyIndicator.present = Vanetza_Security_VerificationKeyIndicator_PR_verificationKey;
|
||||
set_verification_key(tbs.verifyKeyIndicator.choice.verificationKey, key);
|
||||
}
|
||||
|
||||
HashedId8 m_issuer;
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_EtsiTs102941Data_t> m_data { asn_DEF_Vanetza_Security_EtsiTs102941Data };
|
||||
};
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,283 @@
|
||||
#include "ea_request.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include "signed_data.hpp"
|
||||
#include "stub_certificate.hpp"
|
||||
#include "validation.hpp"
|
||||
#include <vanetza/asn1/security/EtsiTs102941Data.h>
|
||||
#include <vanetza/asn1/security/InnerEcRequest.h>
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
|
||||
// Fixture that provides a SecurityModule with in-memory credential storage and
|
||||
// a fresh bootstrap/verification key pair.
|
||||
class EnrolmentRequestTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
EnrolmentRequestTest() : m_credentials(std::make_shared<MockCredentialStorage>()), m_security(m_credentials)
|
||||
{
|
||||
}
|
||||
|
||||
EnrolmentRequestParameters make_params(KeyType type = KeyType::NistP256)
|
||||
{
|
||||
EnrolmentRequestParameters params;
|
||||
params.its_id = "test-station-42";
|
||||
params.verification_key = m_security.create_key(type);
|
||||
params.outer_signer_key = m_security.create_key(type);
|
||||
return params;
|
||||
}
|
||||
|
||||
// Decode the unsecuredData payload of a signed Ieee1609Dot2Data into the given wrapper type.
|
||||
template<class Wrapper> static Wrapper decode_unsecured(const Vanetza_Security_SignedData_t& signed_data)
|
||||
{
|
||||
const auto& payload = signed_data.tbsData->payload->data->content->choice.unsecuredData;
|
||||
Wrapper w;
|
||||
EXPECT_TRUE(w.decode(payload.buf, payload.size));
|
||||
return w;
|
||||
}
|
||||
|
||||
std::shared_ptr<MockCredentialStorage> m_credentials;
|
||||
OpenSslSecurityModule m_security;
|
||||
};
|
||||
|
||||
// The builder must reject parameters that do not satisfy ETSI TS 102 941 / IEEE 1609.2 basic invariants.
|
||||
TEST_F(EnrolmentRequestTest, rejects_empty_its_id)
|
||||
{
|
||||
auto params = make_params();
|
||||
params.its_id.clear();
|
||||
EXPECT_THROW(build_signed_enrolment_request(m_security, params), std::invalid_argument);
|
||||
}
|
||||
|
||||
// The signed output must parse as an EtsiTs103097Data-Signed with the exact
|
||||
// structure specified in ETSI TS 102 941 clause 6.2.3.2:
|
||||
// Ieee1609Dot2Data(signedData(tbsData.payload.data = unsecured(<EtsiTs102941Data>), signer = self))
|
||||
TEST_F(EnrolmentRequestTest, outer_structure_matches_TS102941)
|
||||
{
|
||||
auto params = make_params();
|
||||
ByteBuffer encoded = build_signed_enrolment_request(m_security, params);
|
||||
|
||||
SignedData outer;
|
||||
ASSERT_TRUE(outer.decode(encoded));
|
||||
EXPECT_EQ(3u, outer->protocolVersion);
|
||||
ASSERT_EQ(Vanetza_Security_Ieee1609Dot2Content_PR_signedData, outer->content->present);
|
||||
const auto* sd = outer->content->choice.signedData;
|
||||
ASSERT_NE(nullptr, sd);
|
||||
EXPECT_EQ(Vanetza_Security_HashAlgorithm_sha256, sd->hashId);
|
||||
EXPECT_EQ(Vanetza_Security_SignerIdentifier_PR_self, sd->signer.present);
|
||||
ASSERT_NE(nullptr, sd->tbsData);
|
||||
EXPECT_EQ(aid::SCR, sd->tbsData->headerInfo.psid);
|
||||
ASSERT_NE(nullptr, sd->tbsData->payload);
|
||||
ASSERT_NE(nullptr, sd->tbsData->payload->data);
|
||||
ASSERT_EQ(Vanetza_Security_Ieee1609Dot2Content_PR_unsecuredData, sd->tbsData->payload->data->content->present);
|
||||
}
|
||||
|
||||
// The outer signature must verify against the canonical (bootstrap) key, as
|
||||
// required by ETSI TS 103 525-2 clause 5.2.2.1 (SECPKI_ITSS_ENR_02_BV).
|
||||
TEST_F(EnrolmentRequestTest, outer_signature_verifies_with_canonical_key)
|
||||
{
|
||||
auto params = make_params();
|
||||
ByteBuffer encoded = build_signed_enrolment_request(m_security, params);
|
||||
|
||||
SignedData outer;
|
||||
ASSERT_TRUE(outer.decode(encoded));
|
||||
const auto& sd = *outer->content->choice.signedData;
|
||||
|
||||
Sha256Hash digest = calculate_digest<Sha256Hash>(m_security, *sd.tbsData, nullptr);
|
||||
Signature signature = make_signature(sd.signature);
|
||||
EXPECT_TRUE(m_security.verify(digest, signature, params.outer_signer_key));
|
||||
}
|
||||
|
||||
// The POP signature carried in the EtsiTs102941Data enrolmentRequest content
|
||||
// must verify against the *new* verification key (IEEE 1609.2 / TS 102 941
|
||||
// proof-of-possession).
|
||||
TEST_F(EnrolmentRequestTest, pop_signature_verifies_with_verification_key)
|
||||
{
|
||||
auto params = make_params();
|
||||
ByteBuffer encoded = build_signed_enrolment_request(m_security, params);
|
||||
|
||||
SignedData outer;
|
||||
ASSERT_TRUE(outer.decode(encoded));
|
||||
MgmtData mgmt = decode_unsecured<MgmtData>(*outer->content->choice.signedData);
|
||||
ASSERT_EQ(Vanetza_Security_EtsiTs102941DataContent_PR_enrolmentRequest, mgmt->content.present);
|
||||
|
||||
const auto& pop = mgmt->content.choice.enrolmentRequest;
|
||||
EXPECT_EQ(3u, pop.protocolVersion);
|
||||
ASSERT_EQ(Vanetza_Security_Ieee1609Dot2Content_PR_signedData, pop.content->present);
|
||||
const auto& pop_sd = *pop.content->choice.signedData;
|
||||
EXPECT_EQ(Vanetza_Security_SignerIdentifier_PR_self, pop_sd.signer.present);
|
||||
|
||||
Sha256Hash digest = calculate_digest<Sha256Hash>(m_security, *pop_sd.tbsData, nullptr);
|
||||
Signature signature = make_signature(pop_sd.signature);
|
||||
EXPECT_TRUE(m_security.verify(digest, signature, params.verification_key));
|
||||
}
|
||||
|
||||
// The innermost InnerEcRequest must carry the inputs unchanged: itsId, the
|
||||
// requested verification key, and the permission list.
|
||||
TEST_F(EnrolmentRequestTest, inner_ec_request_carries_inputs)
|
||||
{
|
||||
auto params = make_params();
|
||||
ByteBuffer encoded = build_signed_enrolment_request(m_security, params);
|
||||
|
||||
SignedData outer;
|
||||
ASSERT_TRUE(outer.decode(encoded));
|
||||
MgmtData mgmt = decode_unsecured<MgmtData>(*outer->content->choice.signedData);
|
||||
|
||||
// Extract inner request from the POP's unsecured payload
|
||||
const auto& pop_sd = *mgmt->content.choice.enrolmentRequest.content->choice.signedData;
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_InnerEcRequest_t> inner(asn_DEF_Vanetza_Security_InnerEcRequest);
|
||||
const auto& pop_payload = pop_sd.tbsData->payload->data->content->choice.unsecuredData;
|
||||
ASSERT_TRUE(inner.decode(pop_payload.buf, pop_payload.size));
|
||||
|
||||
EXPECT_EQ(params.its_id.size(), static_cast<size_t>(inner->itsId.size));
|
||||
EXPECT_EQ(0, std::memcmp(inner->itsId.buf, params.its_id.data(), inner->itsId.size));
|
||||
EXPECT_EQ(Vanetza_Security_CertificateFormat_ts103097v131, inner->certificateFormat);
|
||||
|
||||
ASSERT_NE(nullptr, inner->requestedSubjectAttributes.appPermissions);
|
||||
ASSERT_EQ(1, inner->requestedSubjectAttributes.appPermissions->list.count);
|
||||
auto* first = inner->requestedSubjectAttributes.appPermissions->list.array[0];
|
||||
EXPECT_EQ(static_cast<long>(aid::SCR), first->psid);
|
||||
EXPECT_NE(nullptr, first->ssp);
|
||||
}
|
||||
|
||||
// The verification key placed inside the InnerEcRequest must match the
|
||||
// curve/encoding expected for its KeyType.
|
||||
TEST_F(EnrolmentRequestTest, inner_verification_key_matches_curve)
|
||||
{
|
||||
for (auto key_type : { KeyType::NistP256, KeyType::BrainpoolP256r1, KeyType::BrainpoolP384r1 }) {
|
||||
auto params = make_params(key_type);
|
||||
ByteBuffer encoded = build_signed_enrolment_request(m_security, params);
|
||||
|
||||
SignedData outer;
|
||||
ASSERT_TRUE(outer.decode(encoded));
|
||||
MgmtData mgmt = decode_unsecured<MgmtData>(*outer->content->choice.signedData);
|
||||
const auto& pop_sd = *mgmt->content.choice.enrolmentRequest.content->choice.signedData;
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_InnerEcRequest_t> inner(asn_DEF_Vanetza_Security_InnerEcRequest);
|
||||
const auto& pop_payload = pop_sd.tbsData->payload->data->content->choice.unsecuredData;
|
||||
ASSERT_TRUE(inner.decode(pop_payload.buf, pop_payload.size));
|
||||
|
||||
const auto& vkey = inner->publicKeys.verificationKey;
|
||||
switch (key_type) {
|
||||
case KeyType::NistP256:
|
||||
EXPECT_EQ(Vanetza_Security_PublicVerificationKey_PR_ecdsaNistP256, vkey.present);
|
||||
break;
|
||||
case KeyType::BrainpoolP256r1:
|
||||
EXPECT_EQ(Vanetza_Security_PublicVerificationKey_PR_ecdsaBrainpoolP256r1, vkey.present);
|
||||
break;
|
||||
case KeyType::BrainpoolP384r1:
|
||||
EXPECT_EQ(Vanetza_Security_PublicVerificationKey_PR_ecdsaBrainpoolP384r1, vkey.present);
|
||||
break;
|
||||
default:
|
||||
FAIL() << "unexpected key type";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ETSI TS 103 097 v1.3.1 clause 7.2.2 "Enrolment credential":
|
||||
// The appPermissions of an enrolment credential shall contain only the PSID
|
||||
// for secured certificate requests (SCR, 623).
|
||||
// This is also what the TS 103 525-3 abstract test suite requests in its
|
||||
// reference inner EC request, and what the 0_EU-EA_L0 EA's certIssuePermissions
|
||||
// authorise it to issue. Requesting CA/DENM/etc. permissions on the EC causes
|
||||
// the EA to reject the request (often with an opaque error).
|
||||
//
|
||||
// The builder is permissive by design: It accepts whatever the caller passes
|
||||
// so that test tools can also exercise non-conformant requests. But we fix
|
||||
// the *conformant* expected shape in a regression test so drift is caught.
|
||||
TEST_F(EnrolmentRequestTest, conformant_EC_permission_is_SCR_only_TS103097_7_2_2)
|
||||
{
|
||||
EnrolmentRequestParameters params;
|
||||
params.its_id = "station-conformant";
|
||||
params.verification_key = m_security.create_key(KeyType::NistP256);
|
||||
params.outer_signer_key = m_security.create_key(KeyType::NistP256);
|
||||
// TS 103 525-3 reference value PX_INNER_EC_CERTFICATE_BITMAP_SSP_SCR
|
||||
|
||||
ByteBuffer encoded = build_signed_enrolment_request(m_security, params);
|
||||
SignedData outer;
|
||||
ASSERT_TRUE(outer.decode(encoded));
|
||||
MgmtData mgmt = decode_unsecured<MgmtData>(*outer->content->choice.signedData);
|
||||
const auto& pop_sd = *mgmt->content.choice.enrolmentRequest.content->choice.signedData;
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_InnerEcRequest_t> inner(asn_DEF_Vanetza_Security_InnerEcRequest);
|
||||
const auto& pop_payload = pop_sd.tbsData->payload->data->content->choice.unsecuredData;
|
||||
ASSERT_TRUE(inner.decode(pop_payload.buf, pop_payload.size));
|
||||
|
||||
const auto* perms = inner->requestedSubjectAttributes.appPermissions;
|
||||
ASSERT_NE(nullptr, perms);
|
||||
ASSERT_EQ(1, perms->list.count);
|
||||
EXPECT_EQ(static_cast<long>(aid::SCR), perms->list.array[0]->psid);
|
||||
ASSERT_NE(nullptr, perms->list.array[0]->ssp);
|
||||
EXPECT_EQ(Vanetza_Security_ServiceSpecificPermissions_PR_bitmapSsp, perms->list.array[0]->ssp->present);
|
||||
}
|
||||
|
||||
// Mixed-curve stress: use Brainpool for the bootstrap key and NIST for the
|
||||
// verification key (or vice versa) and verify both signatures still validate.
|
||||
// This catches bugs where the code assumes both keys share a curve.
|
||||
TEST_F(EnrolmentRequestTest, mixed_curves_still_verify)
|
||||
{
|
||||
EnrolmentRequestParameters params;
|
||||
params.its_id = "mixed-curve-station";
|
||||
params.verification_key = m_security.create_key(KeyType::BrainpoolP256r1);
|
||||
params.outer_signer_key = m_security.create_key(KeyType::NistP256);
|
||||
|
||||
ByteBuffer encoded = build_signed_enrolment_request(m_security, params);
|
||||
SignedData outer;
|
||||
ASSERT_TRUE(outer.decode(encoded));
|
||||
const auto& outer_sd = *outer->content->choice.signedData;
|
||||
EXPECT_TRUE(m_security.verify(calculate_digest<Sha256Hash>(m_security, *outer_sd.tbsData, nullptr),
|
||||
make_signature(outer_sd.signature), params.outer_signer_key));
|
||||
|
||||
MgmtData mgmt = decode_unsecured<MgmtData>(outer_sd);
|
||||
const auto& pop_sd = *mgmt->content.choice.enrolmentRequest.content->choice.signedData;
|
||||
EXPECT_TRUE(m_security.verify(calculate_digest<Sha256Hash>(m_security, *pop_sd.tbsData, nullptr),
|
||||
make_signature(pop_sd.signature), params.verification_key));
|
||||
}
|
||||
|
||||
// TS 102 941 §6.2.3.2.1: for a re-keying (renewal) enrolment request, the
|
||||
// outer EtsiTs103097Data-Signed shall use SignerIdentifier = digest holding
|
||||
// HashedId8 of the current EC, and the signature shall be computed with the
|
||||
// EC's private key. Passing `outer_signer_certificate` selects this variant.
|
||||
TEST_F(EnrolmentRequestTest, outer_digest_signed_when_certificate_provided)
|
||||
{
|
||||
// Stand-in EC: a verification-key-only stub certificate whose private key
|
||||
// the security module can sign with.
|
||||
PublicKey ec_key = m_security.create_key(KeyType::NistP256);
|
||||
Certificate ec_cert = build_stub_certificate(ec_key);
|
||||
|
||||
EnrolmentRequestParameters params;
|
||||
params.its_id = "renewal-station"; // would be HashedId8 bytes in real use
|
||||
params.verification_key = m_security.create_key(KeyType::NistP256);
|
||||
params.outer_signer_key = ec_key;
|
||||
params.outer_signer_certificate = &ec_cert;
|
||||
|
||||
ByteBuffer encoded = build_signed_enrolment_request(m_security, params);
|
||||
|
||||
SignedData outer;
|
||||
ASSERT_TRUE(outer.decode(encoded));
|
||||
const auto& sd = *outer->content->choice.signedData;
|
||||
|
||||
// signer.present == digest, and digest matches HashedId8(ec_cert)
|
||||
ASSERT_EQ(Vanetza_Security_SignerIdentifier_PR_digest, sd.signer.present);
|
||||
HashedId8 expected_hid8 = ec_cert.calculate_hashed_id8(m_security);
|
||||
EXPECT_TRUE(sd.signer.choice.digest == expected_hid8.octets);
|
||||
|
||||
// signature verifies over calculate_digest(tbs, &ec_cert) using the EC key
|
||||
Sha256Hash digest = calculate_digest<Sha256Hash>(m_security, *sd.tbsData, &ec_cert.raw());
|
||||
EXPECT_TRUE(m_security.verify(digest, make_signature(sd.signature), ec_key));
|
||||
|
||||
// PoP (inner) remains signer = self regardless of outer signer choice.
|
||||
MgmtData mgmt = decode_unsecured<MgmtData>(sd);
|
||||
const auto& pop_sd = *mgmt->content.choice.enrolmentRequest.content->choice.signedData;
|
||||
EXPECT_EQ(Vanetza_Security_SignerIdentifier_PR_self, pop_sd.signer.present);
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,279 @@
|
||||
#include "ea_response.hpp"
|
||||
#include "asn1.hpp"
|
||||
#include "certificate.hpp"
|
||||
#include "exception.hpp"
|
||||
#include "hashed_id8.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include "stub_certificate.hpp"
|
||||
#include "time.hpp"
|
||||
#include "validation.hpp"
|
||||
#include <vanetza/asn1/asn1c_wrapper.hpp>
|
||||
#include <vanetza/asn1/security/EnrolmentResponseCode.h>
|
||||
#include <vanetza/asn1/security/EtsiTs102941Data.h>
|
||||
#include <vanetza/asn1/security/EtsiTs103097Certificate.h>
|
||||
#include <vanetza/asn1/security/Ieee1609Dot2Data.h>
|
||||
#include <vanetza/asn1/security/InnerEcResponse.h>
|
||||
#include <vanetza/asn1/security/SignedData.h>
|
||||
#include <vanetza/asn1/security/Time64.h>
|
||||
#include <vanetza/common/byte_buffer.hpp>
|
||||
#include <vanetza/common/its_aid.hpp>
|
||||
#include <vanetza/security/v3/basic_elements.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
|
||||
namespace vanetza
|
||||
{
|
||||
namespace pki
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr std::uint8_t protocol_version = 3;
|
||||
|
||||
// Builder for a decrypted enrolment response. Defaults produce a structurally
|
||||
// valid response with code=ok and a synthetic EC certificate. Each knob
|
||||
// switches one invariant off so tests can assert the parser rejects it.
|
||||
struct ResponseBuilder
|
||||
{
|
||||
OpenSslSecurityModule* security;
|
||||
const Certificate* ea_cert;
|
||||
const PublicKey* ea_signing_key;
|
||||
|
||||
// Knobs
|
||||
Vanetza_Security_EnrolmentResponseCode_t code = Vanetza_Security_EnrolmentResponseCode_ok;
|
||||
bool include_certificate = true;
|
||||
boost::optional<long> psid_override;
|
||||
// NOTHING = use the correct `digest` variant; other values force that variant
|
||||
Vanetza_Security_SignerIdentifier_PR signer_variant = Vanetza_Security_SignerIdentifier_PR_NOTHING;
|
||||
// if set, forces the digest bytes placed in signer.digest (overriding the
|
||||
// correct EA HashedId8). Only consulted when signer_variant is `digest`
|
||||
// or NOTHING (digest default).
|
||||
boost::optional<HashedId8> signer_digest_override;
|
||||
bool corrupt_signature = false;
|
||||
// if true, wrap the final payload as unsecuredData instead of signedData
|
||||
bool outer_as_unsecured = false;
|
||||
|
||||
ByteBuffer build() const
|
||||
{
|
||||
// inner: EtsiTs102941Data { enrolmentResponse { InnerEcResponse } }
|
||||
MgmtData inner;
|
||||
inner->version = Vanetza_Security_Version_v1;
|
||||
inner->content.present = Vanetza_Security_EtsiTs102941DataContent_PR_enrolmentResponse;
|
||||
auto& ec_resp = inner->content.choice.enrolmentResponse;
|
||||
// 16-byte requestHash (SHA-256 prefix in the real flow)
|
||||
uint8_t req_hash[16];
|
||||
std::fill(std::begin(req_hash), std::end(req_hash), 0xAB);
|
||||
OCTET_STRING_fromBuf(&ec_resp.requestHash, reinterpret_cast<char*>(req_hash), 16);
|
||||
ec_resp.responseCode = code;
|
||||
if (include_certificate) {
|
||||
// Synthesize a dummy EC cert. Not cryptographically valid, just
|
||||
// OER-encodable, so the parser can decode it and the test can
|
||||
// check it came through intact.
|
||||
PublicKey dummy_key = security->create_key(KeyType::NistP256);
|
||||
Certificate dummy_ec = build_stub_certificate(dummy_key);
|
||||
ByteBuffer encoded_ec = dummy_ec.encode();
|
||||
void* decoded = nullptr;
|
||||
EXPECT_TRUE(asn1::decode_oer(asn_DEF_Vanetza_Security_EtsiTs103097Certificate, &decoded, encoded_ec));
|
||||
// InnerEcResponse.certificate is a pointer to the shim struct; the decoded layout is bit-compatible.
|
||||
ec_resp.certificate = static_cast<Vanetza_Security_EtsiTs103097Certificate*>(decoded);
|
||||
}
|
||||
EXPECT_TRUE(inner.validate());
|
||||
ByteBuffer inner_encoded = inner.encode();
|
||||
|
||||
// outer: EtsiTs103097Data { signedData or unsecuredData }
|
||||
asn1::asn1c_oer_wrapper<Vanetza_Security_Ieee1609Dot2Data_t> outer(asn_DEF_Vanetza_Security_Ieee1609Dot2Data);
|
||||
outer->protocolVersion = protocol_version;
|
||||
outer->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
|
||||
if (outer_as_unsecured) {
|
||||
outer->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_unsecuredData;
|
||||
copy(inner_encoded, outer->content->choice.unsecuredData);
|
||||
return outer.encode();
|
||||
}
|
||||
|
||||
outer->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_signedData;
|
||||
outer->content->choice.signedData = asn1::allocate<Vanetza_Security_SignedData_t>();
|
||||
auto& sd = *outer->content->choice.signedData;
|
||||
sd.hashId = Vanetza_Security_HashAlgorithm_sha256;
|
||||
|
||||
// signer
|
||||
const auto variant = (signer_variant == Vanetza_Security_SignerIdentifier_PR_NOTHING) ?
|
||||
Vanetza_Security_SignerIdentifier_PR_digest :
|
||||
signer_variant;
|
||||
sd.signer.present = variant;
|
||||
if (variant == Vanetza_Security_SignerIdentifier_PR_digest) {
|
||||
HashedId8 ea_hid8 = ea_cert->calculate_hashed_id8(*security);
|
||||
if (signer_digest_override) {
|
||||
ea_hid8 = *signer_digest_override;
|
||||
}
|
||||
OCTET_STRING_fromBuf(&sd.signer.choice.digest, reinterpret_cast<const char*>(ea_hid8.octets.data()), 8);
|
||||
}
|
||||
|
||||
// tbsData
|
||||
sd.tbsData = asn1::allocate<Vanetza_Security_ToBeSignedData_t>();
|
||||
sd.tbsData->headerInfo.psid = psid_override.value_or(aid::SCR);
|
||||
sd.tbsData->headerInfo.generationTime = asn1::allocate<Vanetza_Security_Time64_t>();
|
||||
asn_uint642INTEGER(sd.tbsData->headerInfo.generationTime, security::v3::convert_time64(current_time()));
|
||||
|
||||
sd.tbsData->payload = asn1::allocate<Vanetza_Security_SignedDataPayload_t>();
|
||||
auto* sdp = sd.tbsData->payload;
|
||||
sdp->data = asn1::allocate<Vanetza_Security_EtsiTs103097Data_t>();
|
||||
sdp->data->protocolVersion = protocol_version;
|
||||
sdp->data->content = asn1::allocate<Vanetza_Security_Ieee1609Dot2Content_t>();
|
||||
sdp->data->content->present = Vanetza_Security_Ieee1609Dot2Content_PR_unsecuredData;
|
||||
copy(inner_encoded, sdp->data->content->choice.unsecuredData);
|
||||
|
||||
// signature
|
||||
const Vanetza_Security_Certificate_t* signer_cert_for_hash =
|
||||
(variant == Vanetza_Security_SignerIdentifier_PR_self) ? nullptr : &ea_cert->raw();
|
||||
Sha256Hash digest = calculate_digest<Sha256Hash>(*security, *sd.tbsData, signer_cert_for_hash);
|
||||
auto signature = security->sign(ByteBuffer { digest.octets.begin(), digest.octets.end() }, *ea_signing_key);
|
||||
EXPECT_TRUE(static_cast<bool>(signature));
|
||||
|
||||
sd.signature.present = Vanetza_Security_Signature_PR_ecdsaNistP256Signature;
|
||||
sd.signature.choice.ecdsaNistP256Signature.rSig.present = Vanetza_Security_EccP256CurvePoint_PR_x_only;
|
||||
copy_left_padded(signature->r, sd.signature.choice.ecdsaNistP256Signature.rSig.choice.x_only, 32);
|
||||
copy_left_padded(signature->s, sd.signature.choice.ecdsaNistP256Signature.sSig, 32);
|
||||
|
||||
if (corrupt_signature) {
|
||||
// Flip one byte in the encoded signature after we've laid it down.
|
||||
// Easiest: perturb sSig in place.
|
||||
auto& s_oct = sd.signature.choice.ecdsaNistP256Signature.sSig;
|
||||
if (s_oct.size > 0) {
|
||||
s_oct.buf[0] ^= 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
return outer.encode();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class EnrolmentResponseTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
EnrolmentResponseTest() :
|
||||
m_credentials(std::make_shared<MockCredentialStorage>()), m_security(m_credentials),
|
||||
m_ea_key(m_security.create_key(KeyType::NistP256)), m_ea_certificate(build_stub_certificate(m_ea_key))
|
||||
{
|
||||
}
|
||||
|
||||
ResponseBuilder builder()
|
||||
{
|
||||
return ResponseBuilder { &m_security, &m_ea_certificate, &m_ea_key };
|
||||
}
|
||||
|
||||
std::shared_ptr<MockCredentialStorage> m_credentials;
|
||||
OpenSslSecurityModule m_security;
|
||||
PublicKey m_ea_key;
|
||||
Certificate m_ea_certificate;
|
||||
};
|
||||
|
||||
// Happy path: a properly EA-signed response with code=ok and a certificate
|
||||
// round-trips into an EnrolmentResponse whose fields match what went in.
|
||||
TEST_F(EnrolmentResponseTest, happy_path_returns_code_ok_and_certificate)
|
||||
{
|
||||
ByteBuffer payload = builder().build();
|
||||
|
||||
EnrolmentResponse resp = parse_enrolment_response(m_security, payload, m_ea_certificate);
|
||||
|
||||
EXPECT_EQ(Vanetza_Security_EnrolmentResponseCode_ok, resp.code);
|
||||
ASSERT_TRUE(resp.certificate.has_value());
|
||||
EXPECT_EQ(16u, resp.request_hash.size());
|
||||
}
|
||||
|
||||
// The parser must reject random bytes that do not decode as Ieee1609Dot2Data.
|
||||
TEST_F(EnrolmentResponseTest, rejects_undecodable_input)
|
||||
{
|
||||
ByteBuffer garbage(16, 0xFF);
|
||||
EXPECT_THROW(parse_enrolment_response(m_security, garbage, m_ea_certificate), DecodingFailure);
|
||||
}
|
||||
|
||||
// The outer content must be signedData: unsecuredData is not acceptable for
|
||||
// an enrolment response.
|
||||
TEST_F(EnrolmentResponseTest, rejects_outer_unsecured_data)
|
||||
{
|
||||
ResponseBuilder b = builder();
|
||||
b.outer_as_unsecured = true;
|
||||
ByteBuffer payload = b.build();
|
||||
|
||||
EXPECT_THROW(parse_enrolment_response(m_security, payload, m_ea_certificate), DecodingFailure);
|
||||
}
|
||||
|
||||
// TS 102 941 §6.2.3.2.2: the outer signer shall be `digest` naming the EA.
|
||||
// A `self` signer (e.g. accidentally replaying a request shape) is rejected.
|
||||
TEST_F(EnrolmentResponseTest, rejects_self_signer)
|
||||
{
|
||||
ResponseBuilder b = builder();
|
||||
b.signer_variant = Vanetza_Security_SignerIdentifier_PR_self;
|
||||
ByteBuffer payload = b.build();
|
||||
|
||||
EXPECT_THROW(parse_enrolment_response(m_security, payload, m_ea_certificate), DecodingFailure);
|
||||
}
|
||||
|
||||
// The signer's digest must match HashedId8(ea_certificate). A mismatch means
|
||||
// the response was signed by someone else — must be rejected.
|
||||
TEST_F(EnrolmentResponseTest, rejects_signer_digest_mismatch)
|
||||
{
|
||||
ResponseBuilder b = builder();
|
||||
HashedId8 other;
|
||||
other.octets = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77 };
|
||||
b.signer_digest_override = other;
|
||||
ByteBuffer payload = b.build();
|
||||
|
||||
EXPECT_THROW(parse_enrolment_response(m_security, payload, m_ea_certificate), VerificationFailure);
|
||||
}
|
||||
|
||||
// TS 102 941 §6.2.3.2.2: the outer tbsData.headerInfo.psid shall be SCR.
|
||||
TEST_F(EnrolmentResponseTest, rejects_wrong_psid)
|
||||
{
|
||||
ResponseBuilder b = builder();
|
||||
b.psid_override = 0x20; // arbitrary non-SCR
|
||||
ByteBuffer payload = b.build();
|
||||
|
||||
EXPECT_THROW(parse_enrolment_response(m_security, payload, m_ea_certificate), DecodingFailure);
|
||||
}
|
||||
|
||||
// A signature that does not verify against the EA certificate must be
|
||||
// rejected — even when all structural fields match.
|
||||
TEST_F(EnrolmentResponseTest, rejects_bad_signature)
|
||||
{
|
||||
ResponseBuilder b = builder();
|
||||
b.corrupt_signature = true;
|
||||
ByteBuffer payload = b.build();
|
||||
|
||||
EXPECT_THROW(parse_enrolment_response(m_security, payload, m_ea_certificate), VerificationFailure);
|
||||
}
|
||||
|
||||
// A non-ok response code is not a structural failure; the parser returns it
|
||||
// to the caller so initial and renewal flows can treat it differently.
|
||||
// (Current initial flow will still throw on non-ok, but that's a caller
|
||||
// policy, not a parser one.)
|
||||
TEST_F(EnrolmentResponseTest, non_ok_code_returns_without_throwing)
|
||||
{
|
||||
ResponseBuilder b = builder();
|
||||
b.code = Vanetza_Security_EnrolmentResponseCode_deniedpermissions;
|
||||
b.include_certificate = false;
|
||||
ByteBuffer payload = b.build();
|
||||
|
||||
EnrolmentResponse resp = parse_enrolment_response(m_security, payload, m_ea_certificate);
|
||||
|
||||
EXPECT_EQ(Vanetza_Security_EnrolmentResponseCode_deniedpermissions, resp.code);
|
||||
EXPECT_FALSE(resp.certificate.has_value());
|
||||
}
|
||||
|
||||
// Code=ok but no certificate violates TS 102 941 — parser must reject.
|
||||
TEST_F(EnrolmentResponseTest, rejects_ok_without_certificate)
|
||||
{
|
||||
ResponseBuilder b = builder();
|
||||
b.code = Vanetza_Security_EnrolmentResponseCode_ok;
|
||||
b.include_certificate = false;
|
||||
ByteBuffer payload = b.build();
|
||||
|
||||
EXPECT_THROW(parse_enrolment_response(m_security, payload, m_ea_certificate), DecodingFailure);
|
||||
}
|
||||
|
||||
} // namespace pki
|
||||
} // namespace vanetza
|
||||
@@ -0,0 +1,358 @@
|
||||
#include "ecies.hpp"
|
||||
#include "mock_credential_storage.hpp"
|
||||
#include "openssl_security_module.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <openssl/bn.h>
|
||||
#include <openssl/ec.h>
|
||||
#include <openssl/ecdh.h>
|
||||
#include <openssl/obj_mac.h>
|
||||
|
||||
using vanetza::ByteBuffer;
|
||||
using namespace vanetza::pki;
|
||||
|
||||
/**
|
||||
* Test vectors from IEEE 1609.2-2017 (Annex D)
|
||||
*/
|
||||
|
||||
TEST(Ecies, kdf2_vector1)
|
||||
{
|
||||
const ByteBuffer shared {{
|
||||
0x96, 0xC0, 0x56, 0x19, 0xD5, 0x6C, 0x32, 0x8A,
|
||||
0xB9, 0x5F, 0xE8, 0x4B, 0x18, 0x26, 0x4B, 0x08,
|
||||
0x72, 0x5B, 0x85, 0xE3, 0x3F, 0xD3, 0x4F, 0x08
|
||||
}};
|
||||
const ByteBuffer kdp;
|
||||
const ByteBuffer expected {{
|
||||
0x44, 0x30, 0x24, 0xc3, 0xda, 0xe6, 0x6b, 0x95,
|
||||
0xe6, 0xf5, 0x67, 0x06, 0x01, 0x55, 0x8f, 0x71
|
||||
}};
|
||||
|
||||
OpenSslSecurityModule security(std::make_shared<MockCredentialStorage>());
|
||||
EXPECT_EQ(expected, calculate_kdf2(security, shared, kdp, expected.size()));
|
||||
}
|
||||
|
||||
TEST(Ecies, kdf2_vector2)
|
||||
{
|
||||
const ByteBuffer shared {{
|
||||
0x96, 0xF6, 0x00, 0xB7, 0x3A, 0xD6, 0xAC, 0x56,
|
||||
0x29, 0x57, 0x7E, 0xCE, 0xD5, 0x17, 0x43, 0xDD,
|
||||
0x2C, 0x24, 0xC2, 0x1B, 0x1A, 0xC8, 0x3E, 0xE4
|
||||
}};
|
||||
const ByteBuffer kdp;
|
||||
const ByteBuffer expected {{
|
||||
0xb6, 0x29, 0x51, 0x62, 0xa7, 0x80, 0x4f, 0x56,
|
||||
0x67, 0xba, 0x90, 0x70, 0xf8, 0x2f, 0xa5, 0x22
|
||||
}};
|
||||
|
||||
OpenSslSecurityModule security(std::make_shared<MockCredentialStorage>());
|
||||
EXPECT_EQ(expected, calculate_kdf2(security, shared, kdp, expected.size()));
|
||||
}
|
||||
|
||||
TEST(Ecies, kdf2_vector3)
|
||||
{
|
||||
const ByteBuffer shared {{
|
||||
0x22, 0x51, 0x8B, 0x10, 0xE7, 0x0F, 0x2A, 0x3F,
|
||||
0x24, 0x38, 0x10, 0xAE, 0x32, 0x54, 0x13, 0x9E,
|
||||
0xFB, 0xEE, 0x04, 0xAA, 0x57, 0xC7, 0xAF, 0x7D
|
||||
}};
|
||||
const ByteBuffer kdp {{
|
||||
0x75, 0xEE, 0xF8, 0x1A, 0xA3, 0x04, 0x1E, 0x33,
|
||||
0xB8, 0x09, 0x71, 0x20, 0x3D, 0x2C, 0x0C, 0x52
|
||||
}};
|
||||
const ByteBuffer expected {{
|
||||
0xc4, 0x98, 0xaf, 0x77, 0x16, 0x1c, 0xc5, 0x9f,
|
||||
0x29, 0x62, 0xb9, 0xa7, 0x13, 0xe2, 0xb2, 0x15,
|
||||
0x15, 0x2d, 0x13, 0x97, 0x66, 0xce, 0x34, 0xa7,
|
||||
0x76, 0xdf, 0x11, 0x86, 0x6a, 0x69, 0xbf, 0x2e,
|
||||
0x52, 0xa1, 0x3d, 0x9c, 0x7c, 0x6f, 0xc8, 0x78,
|
||||
0xc5, 0x0c, 0x5e, 0xa0, 0xbc, 0x7b, 0x00, 0xe0,
|
||||
0xda, 0x24, 0x47, 0xcf, 0xd8, 0x74, 0xf6, 0xcf,
|
||||
0x92, 0xf3, 0x0d, 0x00, 0x97, 0x11, 0x14, 0x85,
|
||||
0x50, 0x0c, 0x90, 0xc3, 0xaf, 0x8b, 0x48, 0x78,
|
||||
0x72, 0xd0, 0x46, 0x85, 0xd1, 0x4c, 0x8d, 0x1d,
|
||||
0xc8, 0xd7, 0xfa, 0x08, 0xbe, 0xb0, 0xce, 0x0a,
|
||||
0xba, 0xbc, 0x11, 0xf0, 0xbd, 0x49, 0x62, 0x69,
|
||||
0x14, 0x2d, 0x43, 0x52, 0x5a, 0x78, 0xe5, 0xbc,
|
||||
0x79, 0xa1, 0x7f, 0x59, 0x67, 0x6a, 0x57, 0x06,
|
||||
0xdc, 0x54, 0xd5, 0x4d, 0x4d, 0x1f, 0x0b, 0xd7,
|
||||
0xe3, 0x86, 0x12, 0x8e, 0xc2, 0x6a, 0xfc, 0x21
|
||||
}};
|
||||
|
||||
OpenSslSecurityModule security(std::make_shared<MockCredentialStorage>());
|
||||
EXPECT_EQ(expected, calculate_kdf2(security, shared, kdp, expected.size()));
|
||||
}
|
||||
|
||||
TEST(Ecies, kdf2_vector4)
|
||||
{
|
||||
const ByteBuffer shared {{
|
||||
0x7E, 0x33, 0x5A, 0xFA, 0x4B, 0x31, 0xD7, 0x72,
|
||||
0xC0, 0x63, 0x5C, 0x7B, 0x0E, 0x06, 0xF2, 0x6F,
|
||||
0xCD, 0x78, 0x1D, 0xF9, 0x47, 0xD2, 0x99, 0x0A
|
||||
}};
|
||||
const ByteBuffer kdp {{
|
||||
0xD6, 0x5A, 0x48, 0x12, 0x73, 0x3F, 0x8C, 0xDB,
|
||||
0xCD, 0xFB, 0x4B, 0x2F, 0x4C, 0x19, 0x1D, 0x87
|
||||
}};
|
||||
const ByteBuffer expected {{
|
||||
0xc0, 0xbd, 0x9e, 0x38, 0xa8, 0xf9, 0xde, 0x14,
|
||||
0xc2, 0xac, 0xd3, 0x5b, 0x2f, 0x34, 0x10, 0xc6,
|
||||
0x98, 0x8c, 0xf0, 0x24, 0x00, 0x54, 0x36, 0x31,
|
||||
0xe0, 0xd6, 0xa4, 0xc1, 0xd0, 0x30, 0x36, 0x5a,
|
||||
0xcb, 0xf3, 0x98, 0x11, 0x5e, 0x51, 0xaa, 0xdd,
|
||||
0xeb, 0xdc, 0x95, 0x90, 0x66, 0x42, 0x10, 0xf9,
|
||||
0xaa, 0x9f, 0xed, 0x77, 0x0d, 0x4c, 0x57, 0xed,
|
||||
0xea, 0xfa, 0x0b, 0x8c, 0x14, 0xf9, 0x33, 0x00,
|
||||
0x86, 0x52, 0x51, 0x21, 0x8c, 0x26, 0x2d, 0x63,
|
||||
0xda, 0xdc, 0x47, 0xdf, 0xa0, 0xe0, 0x28, 0x48,
|
||||
0x26, 0x79, 0x39, 0x85, 0x13, 0x7e, 0x0a, 0x54,
|
||||
0x4e, 0xc8, 0x0a, 0xbf, 0x2f, 0xdf, 0x5a, 0xb9,
|
||||
0x0b, 0xda, 0xea, 0x66, 0x20, 0x40, 0x12, 0xef,
|
||||
0xe3, 0x49, 0x71, 0xdc, 0x43, 0x1d, 0x62, 0x5c,
|
||||
0xd9, 0xa3, 0x29, 0xb8, 0x21, 0x7c, 0xc8, 0xfd,
|
||||
0x0d, 0x9f, 0x02, 0xb1, 0x3f, 0x2f, 0x6b, 0x0b
|
||||
}};
|
||||
|
||||
OpenSslSecurityModule security(std::make_shared<MockCredentialStorage>());
|
||||
EXPECT_EQ(expected, calculate_kdf2(security, shared, kdp, expected.size()));
|
||||
}
|
||||
|
||||
// Regression test: previously, the 0_EU-EA_L0 certificate's encryption key is
|
||||
// stored in compressed-y-1 form and the ECIES context must still derive a
|
||||
// working shared secret from it.
|
||||
TEST(Ecies, shared_secret_with_compressed_y1_key)
|
||||
{
|
||||
// x coordinate of the 0_EU-EA_L0 EA certificate encryption key
|
||||
const ByteBuffer eu_ea_x {{
|
||||
0x53, 0x8A, 0x8D, 0x36, 0x0D, 0x00, 0xD8, 0x48,
|
||||
0x0F, 0x5B, 0x29, 0x54, 0xFA, 0xB2, 0x42, 0xFB,
|
||||
0x98, 0xB3, 0x38, 0x70, 0xF7, 0xA0, 0xC3, 0x3C,
|
||||
0xF6, 0x52, 0xCB, 0x46, 0xA1, 0x74, 0xE2, 0x48
|
||||
}};
|
||||
|
||||
PublicKey compressed;
|
||||
compressed.type = KeyType::NistP256;
|
||||
compressed.compression = KeyCompression::Y1;
|
||||
compressed.x = eu_ea_x;
|
||||
|
||||
OpenSslSecurityModule security(std::make_shared<MockCredentialStorage>());
|
||||
Sha256Hash info {};
|
||||
// Must not throw and must produce a non-empty 32-byte shared secret
|
||||
auto ecies = security.create_ecies_context(compressed, info);
|
||||
EXPECT_EQ(32u, ecies->shared_secret().size());
|
||||
EXPECT_FALSE(ecies->shared_secret() == ByteBuffer(32, 0));
|
||||
}
|
||||
|
||||
TEST(Ecies, encryption_roundtrip_compressed_receiver)
|
||||
{
|
||||
// Receiver was created as uncompressed but is then compressed before handing it
|
||||
// to the ECIES context. Decryption must still succeed -- otherwise we have a
|
||||
// bug in how compressed keys are mapped back to curve points.
|
||||
OpenSslSecurityModule security(std::make_shared<MockCredentialStorage>());
|
||||
PublicKey receiver = security.create_key(KeyType::NistP256);
|
||||
ASSERT_NE(KeyCompression::NoCompression, receiver.compression);
|
||||
|
||||
Sha256Hash info { 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef };
|
||||
auto ecies = security.create_ecies_context(receiver, info);
|
||||
|
||||
const ByteBuffer plaintext {{
|
||||
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
|
||||
0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff
|
||||
}};
|
||||
const ByteBuffer ciphertext = ecies->encrypt(plaintext);
|
||||
const ByteBuffer decrypted = ecies->decrypt(ciphertext.data(), ciphertext.size());
|
||||
EXPECT_NE(plaintext, ciphertext);
|
||||
EXPECT_EQ(plaintext, decrypted);
|
||||
}
|
||||
|
||||
TEST(Ecies, encryption_roundtrip)
|
||||
{
|
||||
OpenSslSecurityModule security(std::make_shared<MockCredentialStorage>());
|
||||
PublicKey receiver = security.create_key(KeyType::NistP256);
|
||||
Sha256Hash info { 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef };
|
||||
auto ecies = security.create_ecies_context(receiver, info);
|
||||
|
||||
const ByteBuffer plaintext {{
|
||||
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
|
||||
0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff
|
||||
}};
|
||||
const ByteBuffer ciphertext = ecies->encrypt(plaintext);
|
||||
const ByteBuffer decrypted = ecies->decrypt(ciphertext.data(), ciphertext.size());
|
||||
EXPECT_NE(plaintext, ciphertext);
|
||||
EXPECT_EQ(plaintext, decrypted);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Mock that returns fixed test-vector values, so encrypt_key() can be tested
|
||||
// against IEEE 1609.2 Annex D.6.2 known-answer data.
|
||||
class FixedEciesContext : public SecurityModule::EciesContext
|
||||
{
|
||||
public:
|
||||
FixedEciesContext(ByteBuffer shared) : m_shared_secret(std::move(shared))
|
||||
{
|
||||
}
|
||||
|
||||
PublicKey ephemeral_public_key() const override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
PublicKey recipient_public_key() const override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
ByteBuffer shared_secret() const override
|
||||
{
|
||||
return m_shared_secret;
|
||||
}
|
||||
|
||||
ByteBuffer encrypted_key() const override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
ByteBuffer authentication_tag() const override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
ByteBuffer nonce() const override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
void nonce(const ByteBuffer&) override
|
||||
{
|
||||
}
|
||||
|
||||
ByteBuffer encrypt(const ByteBuffer&) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
ByteBuffer decrypt(const std::uint8_t*, std::size_t) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
private:
|
||||
ByteBuffer m_shared_secret;
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// IEEE 1609.2-2016 Annex D.6.2 ECIES1 test vector
|
||||
// End-to-end: runs ECDH with the vector's ephemeral private key and recipient
|
||||
// public key through OpenSSL, then pipes the resulting shared secret through
|
||||
// encrypt_key(). This catches any bug in the ECDH input handling that a
|
||||
// standalone KDF2 test would miss.
|
||||
TEST(Ecies, ecies_IEEE_1609_2_Annex_D_6_2_ECIES1_full)
|
||||
{
|
||||
const ByteBuffer sender_eph_priv {{
|
||||
0x13, 0x84, 0xC3, 0x1D, 0x69, 0x82, 0xD5, 0x2B,
|
||||
0xCA, 0x3B, 0xED, 0x8A, 0x7E, 0x60, 0xF5, 0x2F,
|
||||
0xEC, 0xDA, 0xB4, 0x4E, 0x5C, 0x0E, 0xA1, 0x66,
|
||||
0x81, 0x5A, 0x81, 0x59, 0xE0, 0x9F, 0xFB, 0x42
|
||||
}};
|
||||
const ByteBuffer recipient_pub_x {{
|
||||
0x8C, 0x5E, 0x20, 0xFE, 0x31, 0x93, 0x5F, 0x6F,
|
||||
0xA6, 0x82, 0xA1, 0xF6, 0xD4, 0x6E, 0x44, 0x68,
|
||||
0x53, 0x4F, 0xFE, 0xA1, 0xA6, 0x98, 0xB1, 0x4B,
|
||||
0x0B, 0x12, 0x51, 0x3E, 0xED, 0x8D, 0xEB, 0x11
|
||||
}};
|
||||
const ByteBuffer recipient_pub_y {{
|
||||
0x12, 0x70, 0xFE, 0xC2, 0x42, 0x7E, 0x6A, 0x15,
|
||||
0x4D, 0xFC, 0xAE, 0x33, 0x68, 0x58, 0x43, 0x96,
|
||||
0xC8, 0x25, 0x1A, 0x04, 0xE2, 0xAE, 0x7D, 0x87,
|
||||
0xB0, 0x16, 0xFF, 0x65, 0xD2, 0x2D, 0x6F, 0x9E
|
||||
}};
|
||||
const ByteBuffer aes_key {{
|
||||
0x91, 0x69, 0x15, 0x5B, 0x08, 0xB0, 0x76, 0x74,
|
||||
0xCB, 0xAD, 0xF7, 0x5F, 0xB4, 0x6A, 0x7B, 0x0D
|
||||
}};
|
||||
const Sha256Hash recipient_info {{
|
||||
0xA6, 0xB7, 0xB5, 0x25, 0x54, 0xB4, 0x20, 0x3F,
|
||||
0x7E, 0x3A, 0xCF, 0xDB, 0x3A, 0x3E, 0xD8, 0x67,
|
||||
0x4E, 0xE0, 0x86, 0xCE, 0x59, 0x06, 0xA7, 0xCA,
|
||||
0xC2, 0xF8, 0xA3, 0x98, 0x30, 0x6D, 0x3B, 0xE9
|
||||
}};
|
||||
const ByteBuffer expected_wrapped {{
|
||||
0xA6, 0x34, 0x20, 0x13, 0xD6, 0x23, 0xAD, 0x6C,
|
||||
0x5F, 0x68, 0x82, 0x46, 0x96, 0x73, 0xAE, 0x33
|
||||
}};
|
||||
const ByteBuffer expected_tag {{
|
||||
0x80, 0xE1, 0xD8, 0x5D, 0x30, 0xF1, 0xBA, 0xE4,
|
||||
0xEC, 0xF1, 0xA5, 0x34, 0xA8, 0x9A, 0x07, 0x86
|
||||
}};
|
||||
|
||||
// Set up sender ephemeral EC_KEY with the test vector's private scalar
|
||||
EC_KEY* sender = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
|
||||
BIGNUM* priv_bn = BN_bin2bn(sender_eph_priv.data(), sender_eph_priv.size(), nullptr);
|
||||
ASSERT_EQ(1, EC_KEY_set_private_key(sender, priv_bn));
|
||||
|
||||
// Reconstruct recipient public point from (x, y)
|
||||
const EC_GROUP* group = EC_KEY_get0_group(sender);
|
||||
EC_POINT* recipient_point = EC_POINT_new(group);
|
||||
BIGNUM* rx = BN_bin2bn(recipient_pub_x.data(), recipient_pub_x.size(), nullptr);
|
||||
BIGNUM* ry = BN_bin2bn(recipient_pub_y.data(), recipient_pub_y.size(), nullptr);
|
||||
ASSERT_EQ(1, EC_POINT_set_affine_coordinates(group, recipient_point, rx, ry, nullptr));
|
||||
|
||||
// Compute shared secret
|
||||
ByteBuffer shared(32);
|
||||
int got = ECDH_compute_key(shared.data(), shared.size(), recipient_point, sender, nullptr);
|
||||
ASSERT_EQ(32, got);
|
||||
|
||||
BN_free(priv_bn);
|
||||
BN_free(rx);
|
||||
BN_free(ry);
|
||||
EC_POINT_free(recipient_point);
|
||||
EC_KEY_free(sender);
|
||||
|
||||
// Now feed the shared secret through encrypt_key
|
||||
OpenSslSecurityModule security(std::make_shared<MockCredentialStorage>());
|
||||
FixedEciesContext ecies(shared);
|
||||
auto result = encrypt_key(security, ecies, aes_key, recipient_info);
|
||||
|
||||
EXPECT_EQ(expected_wrapped, result.wrapped_key);
|
||||
EXPECT_EQ(expected_tag, result.authentication_tag);
|
||||
}
|
||||
|
||||
// IEEE 1609.2-2016 Annex D.6.2 ECIES1 test vector
|
||||
// Verifies the KDF2 + key wrapping + HMAC-SHA256 tag chain used by encrypt_key().
|
||||
TEST(Ecies, encrypt_key_IEEE_1609_2_Annex_D_6_2_ECIES1)
|
||||
{
|
||||
// Shared secret z = x-coord of [k_E] * Q_recipient (precomputed)
|
||||
const ByteBuffer shared_secret {{
|
||||
0xd4, 0x43, 0x08, 0x02, 0x3f, 0xbb, 0xd3, 0xe9,
|
||||
0x06, 0xb9, 0xf3, 0xb4, 0x0e, 0x5e, 0x0b, 0x62,
|
||||
0x54, 0x11, 0x70, 0x3c, 0x4a, 0x99, 0x24, 0x9d,
|
||||
0x35, 0xa1, 0x39, 0x06, 0x38, 0x6a, 0x3e, 0xe3
|
||||
}};
|
||||
const ByteBuffer aes_key {{
|
||||
0x91, 0x69, 0x15, 0x5B, 0x08, 0xB0, 0x76, 0x74,
|
||||
0xCB, 0xAD, 0xF7, 0x5F, 0xB4, 0x6A, 0x7B, 0x0D
|
||||
}};
|
||||
const Sha256Hash recipient_info {{
|
||||
0xA6, 0xB7, 0xB5, 0x25, 0x54, 0xB4, 0x20, 0x3F,
|
||||
0x7E, 0x3A, 0xCF, 0xDB, 0x3A, 0x3E, 0xD8, 0x67,
|
||||
0x4E, 0xE0, 0x86, 0xCE, 0x59, 0x06, 0xA7, 0xCA,
|
||||
0xC2, 0xF8, 0xA3, 0x98, 0x30, 0x6D, 0x3B, 0xE9
|
||||
}};
|
||||
const ByteBuffer expected_wrapped {{
|
||||
0xA6, 0x34, 0x20, 0x13, 0xD6, 0x23, 0xAD, 0x6C,
|
||||
0x5F, 0x68, 0x82, 0x46, 0x96, 0x73, 0xAE, 0x33
|
||||
}};
|
||||
const ByteBuffer expected_tag {{
|
||||
0x80, 0xE1, 0xD8, 0x5D, 0x30, 0xF1, 0xBA, 0xE4,
|
||||
0xEC, 0xF1, 0xA5, 0x34, 0xA8, 0x9A, 0x07, 0x86
|
||||
}};
|
||||
|
||||
OpenSslSecurityModule security(std::make_shared<MockCredentialStorage>());
|
||||
FixedEciesContext ecies(shared_secret);
|
||||
auto result = encrypt_key(security, ecies, aes_key, recipient_info);
|
||||
|
||||
EXPECT_EQ(expected_wrapped, result.wrapped_key);
|
||||
EXPECT_EQ(expected_tag, result.authentication_tag);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#include "hexstring.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace vanetza;
|
||||
using namespace vanetza::pki;
|
||||
|
||||
TEST(HexString, encode_buffer)
|
||||
{
|
||||
const ByteBuffer buffer { 0x00, 0x0f, 0xa5, 0xff };
|
||||
EXPECT_EQ("000FA5FF", hexstring(buffer));
|
||||
EXPECT_EQ("", hexstring(ByteBuffer {}));
|
||||
}
|
||||
|
||||
TEST(HexString, encode_pointer)
|
||||
{
|
||||
const std::uint8_t bytes[] { 0xde, 0xad, 0xbe, 0xef };
|
||||
EXPECT_EQ("DEADBEEF", hexstring(bytes, sizeof(bytes)));
|
||||
}
|
||||
|
||||
TEST(HexString, encode_array)
|
||||
{
|
||||
const std::array<std::uint8_t, 3> array { 0x01, 0x23, 0x45 };
|
||||
EXPECT_EQ("012345", hexstring(array));
|
||||
}
|
||||
|
||||
TEST(HexString, encode_string)
|
||||
{
|
||||
EXPECT_EQ("414243", hexstring(std::string { "ABC" }));
|
||||
}
|
||||
|
||||
TEST(HexString, is_valid_accepts_hex)
|
||||
{
|
||||
EXPECT_TRUE(is_valid_hexstring("00"));
|
||||
EXPECT_TRUE(is_valid_hexstring("DEADBEEF"));
|
||||
EXPECT_TRUE(is_valid_hexstring("deadbeef"));
|
||||
EXPECT_TRUE(is_valid_hexstring("0123456789abcdefABCDEF"));
|
||||
}
|
||||
|
||||
TEST(HexString, is_valid_rejects_empty)
|
||||
{
|
||||
EXPECT_FALSE(is_valid_hexstring(""));
|
||||
}
|
||||
|
||||
TEST(HexString, is_valid_rejects_odd_length)
|
||||
{
|
||||
EXPECT_FALSE(is_valid_hexstring("0"));
|
||||
EXPECT_FALSE(is_valid_hexstring("abc"));
|
||||
}
|
||||
|
||||
TEST(HexString, is_valid_rejects_non_hex)
|
||||
{
|
||||
EXPECT_FALSE(is_valid_hexstring("0g"));
|
||||
EXPECT_FALSE(is_valid_hexstring("xy"));
|
||||
EXPECT_FALSE(is_valid_hexstring("12 34"));
|
||||
EXPECT_FALSE(is_valid_hexstring("0x12"));
|
||||
}
|
||||
|
||||
TEST(HexString, parse_decodes_to_bytes)
|
||||
{
|
||||
const std::string raw = parse_hexstring("DEADBEEF");
|
||||
const ByteBuffer expected { 0xde, 0xad, 0xbe, 0xef };
|
||||
EXPECT_EQ(ByteBuffer(raw.begin(), raw.end()), expected);
|
||||
}
|
||||
|
||||
TEST(HexString, parse_is_case_insensitive)
|
||||
{
|
||||
EXPECT_EQ(parse_hexstring("abcdef"), parse_hexstring("ABCDEF"));
|
||||
}
|
||||
|
||||
TEST(HexString, parse_roundtrip)
|
||||
{
|
||||
const ByteBuffer buffer { 0x00, 0x7f, 0x80, 0xff };
|
||||
const std::string raw = parse_hexstring(hexstring(buffer));
|
||||
EXPECT_EQ(ByteBuffer(raw.begin(), raw.end()), buffer);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
#include "http.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace vanetza::pki;
|
||||
|
||||
TEST(HttpQuery, from_url_http)
|
||||
{
|
||||
HttpQuery result = HttpQuery::from_url("http://www.example.com");
|
||||
EXPECT_EQ("www.example.com", result.host);
|
||||
EXPECT_EQ("http", result.which_service());
|
||||
EXPECT_EQ("", result.path);
|
||||
EXPECT_FALSE(result.secure);
|
||||
}
|
||||
|
||||
TEST(HttpQuery, from_url_http_with_port)
|
||||
{
|
||||
HttpQuery result = HttpQuery::from_url("http://www.example.com:80");
|
||||
EXPECT_EQ("www.example.com", result.host);
|
||||
EXPECT_EQ("80", result.which_service());
|
||||
EXPECT_FALSE(result.secure);
|
||||
}
|
||||
|
||||
TEST(HttpQuery, from_url_http_with_path)
|
||||
{
|
||||
HttpQuery result = HttpQuery::from_url("http://www.example.com/foo/bar");
|
||||
EXPECT_EQ("www.example.com", result.host);
|
||||
EXPECT_EQ("/foo/bar", result.path);
|
||||
}
|
||||
|
||||
TEST(HttpQuery, from_url_http_with_port_and_path)
|
||||
{
|
||||
HttpQuery result = HttpQuery::from_url("http://www.example.com:80/foo/bar");
|
||||
EXPECT_EQ("www.example.com", result.host);
|
||||
EXPECT_EQ("/foo/bar", result.path);
|
||||
EXPECT_EQ("80", result.which_service());
|
||||
}
|
||||
|
||||
TEST(HttpQuery, from_url_broken_prefix)
|
||||
{
|
||||
EXPECT_ANY_THROW(HttpQuery::from_url("shttp://www.example.com"));
|
||||
}
|
||||
|
||||
TEST(HttpQuery, from_url_https)
|
||||
{
|
||||
HttpQuery result = HttpQuery::from_url("https://example.com/");
|
||||
EXPECT_EQ("example.com", result.host);
|
||||
EXPECT_EQ("/", result.path);
|
||||
EXPECT_EQ("https", result.which_service());
|
||||
EXPECT_TRUE(result.secure);
|
||||
}
|
||||
|
||||
TEST(HttpQuery, from_url_https_with_port)
|
||||
{
|
||||
HttpQuery result = HttpQuery::from_url("https://www.example.com:8080");
|
||||
EXPECT_EQ("www.example.com", result.host);
|
||||
EXPECT_EQ("", result.path);
|
||||
EXPECT_EQ("8080", result.which_service());
|
||||
EXPECT_TRUE(result.secure);
|
||||
}
|
||||
|
||||
TEST(ResolveUrl, empty_reference_keeps_base)
|
||||
{
|
||||
EXPECT_EQ("https://aa.example.com/aa/0001", resolve_url("https://aa.example.com/aa/0001", ""));
|
||||
}
|
||||
|
||||
TEST(ResolveUrl, absolute_reference_passes_through)
|
||||
{
|
||||
EXPECT_EQ("http://other.com/x", resolve_url("https://aa.example.com/aa/0001", "http://other.com/x"));
|
||||
EXPECT_EQ("https://other.com", resolve_url("https://aa.example.com/aa/0001", "https://other.com"));
|
||||
}
|
||||
|
||||
TEST(ResolveUrl, absolute_path_replaces_path)
|
||||
{
|
||||
EXPECT_EQ("https://aa.example.com/foo", resolve_url("https://aa.example.com/aa/0001", "/foo"));
|
||||
EXPECT_EQ("https://aa.example.com/", resolve_url("https://aa.example.com/aa/0001", "/"));
|
||||
}
|
||||
|
||||
TEST(ResolveUrl, absolute_path_keeps_authority_with_port)
|
||||
{
|
||||
EXPECT_EQ("https://aa.example.com:8443/foo", resolve_url("https://aa.example.com:8443/aa/0001", "/foo"));
|
||||
}
|
||||
|
||||
TEST(ResolveUrl, relative_path_merges_onto_base_directory)
|
||||
{
|
||||
EXPECT_EQ("https://aa.example.com/aa/foo", resolve_url("https://aa.example.com/aa/0001", "foo"));
|
||||
// base without a path is treated as having a root directory
|
||||
EXPECT_EQ("https://aa.example.com/foo", resolve_url("https://aa.example.com", "foo"));
|
||||
// a trailing slash on the base keeps the last segment
|
||||
EXPECT_EQ("https://aa.example.com/aa/0001/foo", resolve_url("https://aa.example.com/aa/0001/", "foo"));
|
||||
}
|
||||
|
||||
TEST(ResolveUrl, network_path_replaces_authority_keeps_scheme)
|
||||
{
|
||||
EXPECT_EQ("https://other.com/z", resolve_url("https://aa.example.com/aa/0001", "//other.com/z"));
|
||||
EXPECT_EQ("http://other.com/z", resolve_url("http://aa.example.com/aa/0001", "//other.com/z"));
|
||||
}
|
||||
|
||||
TEST(ResolveUrl, base_query_and_fragment_are_dropped)
|
||||
{
|
||||
EXPECT_EQ("https://aa.example.com/foo", resolve_url("https://aa.example.com/aa/0001?x=1#frag", "/foo"));
|
||||
}
|
||||
|
||||
TEST(ResolveUrl, unparseable_base_throws)
|
||||
{
|
||||
EXPECT_ANY_THROW(resolve_url("not-a-url", "/foo"));
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#include "openssl.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <openssl/bn.h>
|
||||
#include <openssl/ec.h>
|
||||
#include <openssl/obj_mac.h>
|
||||
|
||||
using vanetza::ByteBuffer;
|
||||
using namespace vanetza::pki;
|
||||
|
||||
// Brainpool P-384 known-answer vector.
|
||||
// y is even, so the compressed form is Y0 and the compressed x bytes equal the uncompressed x bytes.
|
||||
static const ByteBuffer brainpool_p384_x {{
|
||||
0x3b, 0xef, 0x3b, 0xa0, 0x6b, 0x70, 0xe7, 0x20,
|
||||
0x03, 0x3e, 0x63, 0xdd, 0x7d, 0xf8, 0x7c, 0xd8,
|
||||
0x80, 0x84, 0x25, 0x0a, 0xdc, 0x52, 0xf3, 0xa7,
|
||||
0x61, 0xaf, 0xaf, 0xa0, 0x71, 0xdf, 0x29, 0x07,
|
||||
0x92, 0x45, 0x90, 0x57, 0x84, 0xe4, 0x85, 0x9d,
|
||||
0x02, 0x53, 0x67, 0xd7, 0xf4, 0xe9, 0x46, 0x30
|
||||
}};
|
||||
|
||||
static const ByteBuffer brainpool_p384_y {{
|
||||
0x08, 0x82, 0xc2, 0xf5, 0x04, 0x74, 0x72, 0xfe,
|
||||
0xcb, 0x65, 0xa4, 0xc4, 0x37, 0xfd, 0x22, 0x08,
|
||||
0x59, 0x83, 0x31, 0xbf, 0xb6, 0xcc, 0xb9, 0x30,
|
||||
0xb6, 0x73, 0xf7, 0xcc, 0x06, 0x51, 0x1e, 0x14,
|
||||
0xba, 0x52, 0x9a, 0xcf, 0x95, 0x25, 0x40, 0x85,
|
||||
0x83, 0xe7, 0x72, 0x53, 0x46, 0xb1, 0xb4, 0xba
|
||||
}};
|
||||
|
||||
TEST(KeyCompression, brainpool_p384_compress)
|
||||
{
|
||||
OpenSslPointer<EC_GROUP> group { EC_GROUP_new_by_curve_name(NID_brainpoolP384r1) };
|
||||
OpenSslPointer<EC_POINT> point { EC_POINT_new(group.raw()) };
|
||||
auto x = make_bignum(brainpool_p384_x);
|
||||
auto y = make_bignum(brainpool_p384_y);
|
||||
openssl_result(EC_POINT_set_affine_coordinates(group.raw(), point.raw(), x.raw(), y.raw(), nullptr), "set affine");
|
||||
|
||||
OpenSslPointer<EC_KEY> ec_key { EC_KEY_new_by_curve_name(NID_brainpoolP384r1) };
|
||||
openssl_result(EC_KEY_set_public_key(ec_key.raw(), point.raw()), "set pubkey");
|
||||
|
||||
PublicKey pub = make_public_key(ec_key.raw());
|
||||
EXPECT_EQ(KeyType::BrainpoolP384r1, pub.type);
|
||||
EXPECT_EQ(KeyCompression::Y0, pub.compression);
|
||||
EXPECT_EQ(brainpool_p384_x, pub.x);
|
||||
EXPECT_TRUE(pub.y.empty());
|
||||
}
|
||||
|
||||
TEST(KeyCompression, brainpool_p384_decompress)
|
||||
{
|
||||
PublicKey compressed;
|
||||
compressed.type = KeyType::BrainpoolP384r1;
|
||||
compressed.compression = KeyCompression::Y0;
|
||||
compressed.x = brainpool_p384_x;
|
||||
|
||||
OpenSslPointer<EC_POINT> point = make_ec_point(compressed);
|
||||
OpenSslPointer<EC_GROUP> group { EC_GROUP_new_by_curve_name(NID_brainpoolP384r1) };
|
||||
OpenSslPointer<BIGNUM> rx { BN_new() };
|
||||
OpenSslPointer<BIGNUM> ry { BN_new() };
|
||||
openssl_result(EC_POINT_get_affine_coordinates(group.raw(), point.raw(), rx.raw(), ry.raw(), nullptr),
|
||||
"get affine");
|
||||
|
||||
EXPECT_EQ(brainpool_p384_x, make_buffer(rx.raw()));
|
||||
EXPECT_EQ(brainpool_p384_y, make_buffer(ry.raw()));
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user