34#include <choc/text/choc_JSON.h>
45#include <system_error>
81 for (
auto& c : s) c =
static_cast<char>(std::tolower(
static_cast<unsigned char>(c)));
87inline std::optional<std::vector<std::uint8_t>>
srl_hex_decode(std::string_view hex) {
88 if (hex.size() % 2 != 0)
return std::nullopt;
89 auto nibble = [](
char c) ->
int {
90 if (c >=
'0' && c <=
'9')
return c -
'0';
91 if (c >=
'a' && c <=
'f')
return c -
'a' + 10;
92 if (c >=
'A' && c <=
'F')
return c -
'A' + 10;
95 std::vector<std::uint8_t> out;
96 out.reserve(hex.size() / 2);
97 for (std::size_t i = 0; i < hex.size(); i += 2) {
98 const int hi = nibble(hex[i]), lo = nibble(hex[i + 1]);
99 if (hi < 0 || lo < 0)
return std::nullopt;
100 out.push_back(
static_cast<std::uint8_t
>((hi << 4) | lo));
123 std::vector<std::uint8_t> out;
124 auto put_u32 = [&out](std::uint32_t n) {
125 out.push_back(
static_cast<std::uint8_t
>(n & 0xFF));
126 out.push_back(
static_cast<std::uint8_t
>((n >> 8) & 0xFF));
127 out.push_back(
static_cast<std::uint8_t
>((n >> 16) & 0xFF));
128 out.push_back(
static_cast<std::uint8_t
>((n >> 24) & 0xFF));
130 auto put_u64 = [&out](std::uint64_t n) {
131 for (
int i = 0; i < 8; ++i) out.push_back(
static_cast<std::uint8_t
>((n >> (i * 8)) & 0xFF));
133 auto put_field = [&](std::string_view s) {
134 put_u32(
static_cast<std::uint32_t
>(s.size()));
135 out.insert(out.end(), s.begin(), s.end());
137 put_field(
"pulp-revocation-list-v1");
156inline std::optional<SignedRevocationList>
parse_srl(std::string_view json,
157 std::string&
error) {
160 auto v = choc::json::parse(std::string(json));
161 if (!v.isObject()) {
error =
"SRL is not a JSON object";
return std::nullopt; }
162 if (!v.hasObjectMember(
"epoch") || !v[
"epoch"].isInt()) {
163 error =
"missing/invalid integer 'epoch'";
return std::nullopt;
165 const std::int64_t raw_epoch = v[
"epoch"].getInt64();
166 if (raw_epoch < 0) {
error =
"'epoch' must be non-negative";
return std::nullopt; }
167 srl.
epoch =
static_cast<std::uint64_t
>(raw_epoch);
169 if (v.hasObjectMember(
"schema_version") && v[
"schema_version"].isInt())
170 srl.
schema_version =
static_cast<int>(v[
"schema_version"].getInt64());
171 if (v.hasObjectMember(
"issued_utc") && v[
"issued_utc"].isString())
174 auto read_str_array = [&](
const char* key, std::vector<std::string>& dst) ->
bool {
175 if (!v.hasObjectMember(key))
return true;
176 const auto& arr = v[key];
177 if (!arr.isArray()) {
error = std::string(
"'") + key +
"' must be an array";
return false; }
178 for (uint32_t i = 0; i < arr.size(); ++i) {
179 if (!arr[i].isString()) {
180 error = std::string(
"'") + key +
"[" + std::to_string(i) +
"]' must be a string";
183 dst.push_back(
srl_to_lower(std::string(arr[i].getString())));
190 if (v.hasObjectMember(
"signer") && v[
"signer"].isString()) {
193 if (v.hasObjectMember(
"signature") && v[
"signature"].isString()) {
196 }
catch (
const std::exception& e) {
197 error = std::string(
"SRL JSON parse failed: ") + e.what();
206 auto esc = [](std::string_view s) {
208 o.reserve(s.size() + 2);
211 case '"': o +=
"\\\"";
break;
212 case '\\': o +=
"\\\\";
break;
213 case '\n': o +=
"\\n";
break;
214 case '\r': o +=
"\\r";
break;
215 case '\t': o +=
"\\t";
break;
216 default: o += c;
break;
221 auto str_array = [&](
const std::vector<std::string>& xs) {
223 for (std::size_t i = 0; i < xs.size(); ++i) {
225 o +=
"\"" + esc(xs[i]) +
"\"";
230 std::ostringstream os;
233 <<
"\"epoch\":" << srl.
epoch <<
","
234 <<
"\"issued_utc\":\"" << esc(srl.
issued_utc) <<
"\","
250 const std::vector<std::uint8_t>& trusted_revocation_pubkey) {
262 message.data(), message.size())) {
270 const std::vector<std::uint8_t>& trusted_revocation_pubkey,
275 auto res =
verify_srl(*parsed, trusted_revocation_pubkey);
276 if (res.ok()) out = std::move(*parsed);
287 std::string_view signer_key_fpr,
288 std::string_view artifact_hash) {
289 if (!signer_key_fpr.empty()) {
290 const std::string want =
srl_to_lower(std::string(signer_key_fpr));
297 if (!artifact_hash.empty()) {
298 const std::string want =
srl_to_lower(std::string(artifact_hash));
340 std::string_view revocation_pubkey_fpr,
341 std::string_view update_channel) {
342 auto put = [](std::string& o, std::string_view s) {
343 o += std::to_string(s.size());
345 o.append(s.begin(), s.end());
349 put(key,
srl_to_lower(std::string(revocation_pubkey_fpr)));
350 put(key, update_channel);
355 std::filesystem::path
path_for(std::string_view namespace_key)
const {
364 const auto p =
path_for(namespace_key);
366 if (!std::filesystem::exists(p, ec) || ec)
return {};
367 std::ifstream in(p, std::ios::binary);
369 std::string contents((std::istreambuf_iterator<char>(in)),
370 std::istreambuf_iterator<char>());
380 std::optional<SignedRevocationList>
last_known(std::string_view namespace_key)
const {
392 std::string_view plugin_id,
393 std::string_view revocation_pubkey_fpr,
394 std::string_view update_channel) {
415 std::filesystem::create_directories(root_, ec);
416 if (ec)
return false;
417 const auto target =
path_for(namespace_key);
418 const auto tmp = std::filesystem::path(target).concat(
".tmp");
420 std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
421 if (!out)
return false;
424 if (!out) { std::filesystem::remove(tmp, ec);
return false; }
426 std::filesystem::rename(tmp, target, ec);
427 if (ec) { std::error_code rmec; std::filesystem::remove(tmp, rmec);
return false; }
431 std::filesystem::path root_;
std::string sha256_hex(const uint8_t *data, size_t size)
Compute SHA-256 hash as hex string (64 chars).
std::string hex_encode(const uint8_t *data, size_t size)
Lowercase-hex encode raw bytes.
Definition crypto.hpp:32
bool ed25519_verify(const uint8_t *public_key_32, size_t public_key_size, const uint8_t *signature_64, size_t signature_size, const uint8_t *message, size_t message_size)
Verify a 64-byte detached signature against a message + 32-byte public key.
constexpr size_t ed25519_public_key_size
Ed25519 key sizes (RFC 8032 §5.1).
Definition crypto.hpp:159
constexpr size_t ed25519_signature_size
Definition crypto.hpp:162