Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
revocation.hpp
Go to the documentation of this file.
1#pragma once
2
31
33
34#include <choc/text/choc_JSON.h>
35
36#include <algorithm>
37#include <cctype>
38#include <cstdint>
39#include <filesystem>
40#include <fstream>
41#include <optional>
42#include <sstream>
43#include <string>
44#include <string_view>
45#include <system_error>
46#include <vector>
47
48namespace pulp::format::reload {
49
50// ── SRL data model ──────────────────────────────────────────────────────────
51
56 std::uint64_t epoch = 0;
57 std::string issued_utc;
58 std::vector<std::string> revoked_signer_key_fprs;
59 std::vector<std::string> revoked_artifact_hashes;
60 std::vector<std::uint8_t> signer_public_key;
61 std::vector<std::uint8_t> signature;
62};
63
70
73 std::string detail;
74 bool ok() const { return status == SrlVerify::Ok; }
75};
76
77// ── small helpers ────────────────────────────────────────────────────────────
78
80inline std::string srl_to_lower(std::string s) {
81 for (auto& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
82 return s;
83}
84
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;
93 return -1;
94 };
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));
101 }
102 return out;
103}
104
106inline std::string srl_hex_encode(const std::vector<std::uint8_t>& bytes) {
107 return runtime::hex_encode(bytes);
108}
109
110// ── canonical signed message ───────────────────────────────────────────────
111
122inline std::vector<std::uint8_t> srl_signed_message(const SignedRevocationList& srl) {
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));
129 };
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));
132 };
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());
136 };
137 put_field("pulp-revocation-list-v1");
138 put_u32(static_cast<std::uint32_t>(srl.schema_version));
139 put_u64(srl.epoch);
140 put_field(srl.issued_utc);
141 put_u32(static_cast<std::uint32_t>(srl.revoked_signer_key_fprs.size()));
142 for (const auto& fpr : srl.revoked_signer_key_fprs) put_field(srl_to_lower(fpr));
143 put_u32(static_cast<std::uint32_t>(srl.revoked_artifact_hashes.size()));
144 for (const auto& h : srl.revoked_artifact_hashes) put_field(srl_to_lower(h));
145 return out;
146}
147
148// ── parse / serialize (JSON wire form) ───────────────────────────────────────
149
156inline std::optional<SignedRevocationList> parse_srl(std::string_view json,
157 std::string& error) {
159 try {
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;
164 }
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);
168
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())
172 srl.issued_utc = v["issued_utc"].getString();
173
174 auto read_str_array = [&](const char* key, std::vector<std::string>& dst) -> bool {
175 if (!v.hasObjectMember(key)) return true; // optional
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";
181 return false;
182 }
183 dst.push_back(srl_to_lower(std::string(arr[i].getString())));
184 }
185 return true;
186 };
187 if (!read_str_array("revoked_signer_key_fprs", srl.revoked_signer_key_fprs)) return std::nullopt;
188 if (!read_str_array("revoked_artifact_hashes", srl.revoked_artifact_hashes)) return std::nullopt;
189
190 if (v.hasObjectMember("signer") && v["signer"].isString()) {
191 if (auto k = srl_hex_decode(v["signer"].getString())) srl.signer_public_key = std::move(*k);
192 }
193 if (v.hasObjectMember("signature") && v["signature"].isString()) {
194 if (auto s = srl_hex_decode(v["signature"].getString())) srl.signature = std::move(*s);
195 }
196 } catch (const std::exception& e) {
197 error = std::string("SRL JSON parse failed: ") + e.what();
198 return std::nullopt;
199 }
200 return srl;
201}
202
205inline std::string serialize_srl_json(const SignedRevocationList& srl) {
206 auto esc = [](std::string_view s) {
207 std::string o;
208 o.reserve(s.size() + 2);
209 for (char c : s) {
210 switch (c) {
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;
217 }
218 }
219 return o;
220 };
221 auto str_array = [&](const std::vector<std::string>& xs) {
222 std::string o = "[";
223 for (std::size_t i = 0; i < xs.size(); ++i) {
224 if (i) o += ",";
225 o += "\"" + esc(xs[i]) + "\"";
226 }
227 o += "]";
228 return o;
229 };
230 std::ostringstream os;
231 os << "{"
232 << "\"schema_version\":" << srl.schema_version << ","
233 << "\"epoch\":" << srl.epoch << ","
234 << "\"issued_utc\":\"" << esc(srl.issued_utc) << "\","
235 << "\"revoked_signer_key_fprs\":" << str_array(srl.revoked_signer_key_fprs) << ","
236 << "\"revoked_artifact_hashes\":" << str_array(srl.revoked_artifact_hashes) << ","
237 << "\"signer\":\"" << srl_hex_encode(srl.signer_public_key) << "\","
238 << "\"signature\":\"" << srl_hex_encode(srl.signature) << "\""
239 << "}";
240 return os.str();
241}
242
243// ── verification ─────────────────────────────────────────────────────────────
244
250 const std::vector<std::uint8_t>& trusted_revocation_pubkey) {
252 trusted_revocation_pubkey.size() != runtime::ed25519_public_key_size ||
253 srl.signer_public_key != trusted_revocation_pubkey) {
254 return {SrlVerify::UntrustedSigner, "SRL signer key is not the trusted revocation key"};
255 }
256 if (srl.signature.size() != runtime::ed25519_signature_size) {
257 return {SrlVerify::BadSignature, "signature size invalid"};
258 }
259 const auto message = srl_signed_message(srl);
261 srl.signature.data(), srl.signature.size(),
262 message.data(), message.size())) {
263 return {SrlVerify::BadSignature, "Ed25519 verification failed"};
264 }
265 return {SrlVerify::Ok, ""};
266}
267
269inline SrlVerifyResult parse_and_verify_srl(std::string_view json,
270 const std::vector<std::uint8_t>& trusted_revocation_pubkey,
272 std::string err;
273 auto parsed = parse_srl(json, err);
274 if (!parsed) return {SrlVerify::ParseError, err};
275 auto res = verify_srl(*parsed, trusted_revocation_pubkey);
276 if (res.ok()) out = std::move(*parsed);
277 return res;
278}
279
280// ── revocation query ─────────────────────────────────────────────────────────
281
286inline bool is_revoked(const SignedRevocationList& srl,
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));
291 if (std::find(srl.revoked_signer_key_fprs.begin(),
292 srl.revoked_signer_key_fprs.end(), want) !=
293 srl.revoked_signer_key_fprs.end()) {
294 return true;
295 }
296 }
297 if (!artifact_hash.empty()) {
298 const std::string want = srl_to_lower(std::string(artifact_hash));
299 if (std::find(srl.revoked_artifact_hashes.begin(),
300 srl.revoked_artifact_hashes.end(), want) !=
301 srl.revoked_artifact_hashes.end()) {
302 return true;
303 }
304 }
305 return false;
306}
307
308// ── namespaced monotonic epoch floor store ───────────────────────────────────
309
317
321 std::uint64_t epoch = 0;
322 std::optional<SignedRevocationList> last_known;
323};
324
334public:
335 explicit EpochFloorStore(std::filesystem::path root) : root_(std::move(root)) {}
336
339 static std::string make_namespace(std::string_view plugin_id,
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());
344 o += ':';
345 o.append(s.begin(), s.end());
346 };
347 std::string key;
348 put(key, plugin_id);
349 put(key, srl_to_lower(std::string(revocation_pubkey_fpr)));
350 put(key, update_channel);
351 return key;
352 }
353
355 std::filesystem::path path_for(std::string_view namespace_key) const {
356 return root_ / (runtime::sha256_hex(namespace_key) + ".srl.json");
357 }
358
363 SrlFloorState read_floor(std::string_view namespace_key) const {
364 const auto p = path_for(namespace_key);
365 std::error_code ec;
366 if (!std::filesystem::exists(p, ec) || ec) return {}; // Absent
367 std::ifstream in(p, std::ios::binary);
368 if (!in) return {SrlFloorState::Kind::Corrupt, 0, std::nullopt};
369 std::string contents((std::istreambuf_iterator<char>(in)),
370 std::istreambuf_iterator<char>());
371 if (in.bad()) return {SrlFloorState::Kind::Corrupt, 0, std::nullopt};
372 std::string err;
373 auto parsed = parse_srl(contents, err);
374 if (!parsed) return {SrlFloorState::Kind::Corrupt, 0, std::nullopt};
375 return {SrlFloorState::Kind::Present, parsed->epoch, std::move(parsed)};
376 }
377
380 std::optional<SignedRevocationList> last_known(std::string_view namespace_key) const {
381 auto st = read_floor(namespace_key);
382 if (st.kind == SrlFloorState::Kind::Present) return st.last_known;
383 return std::nullopt;
384 }
385
392 std::string_view plugin_id,
393 std::string_view revocation_pubkey_fpr,
394 std::string_view update_channel) {
395 return accept_srl(srl, make_namespace(plugin_id, revocation_pubkey_fpr, update_channel));
396 }
397
399 SrlAcceptance accept_srl(const SignedRevocationList& srl, std::string_view namespace_key) {
400 auto st = read_floor(namespace_key);
402 if (st.kind == SrlFloorState::Kind::Present && srl.epoch <= st.epoch) {
404 }
405 return persist(srl, namespace_key) ? SrlAcceptance::Accepted
407 }
408
409private:
413 bool persist(const SignedRevocationList& srl, std::string_view namespace_key) {
414 std::error_code ec;
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");
419 {
420 std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
421 if (!out) return false;
422 out << serialize_srl_json(srl);
423 out.flush();
424 if (!out) { std::filesystem::remove(tmp, ec); return false; }
425 }
426 std::filesystem::rename(tmp, target, ec);
427 if (ec) { std::error_code rmec; std::filesystem::remove(tmp, rmec); return false; }
428 return true;
429 }
430
431 std::filesystem::path root_;
432};
433
434} // namespace pulp::format::reload
SrlAcceptance accept_srl(const SignedRevocationList &srl, std::string_view namespace_key)
Overload taking a pre-computed namespace key.
Definition revocation.hpp:399
std::filesystem::path path_for(std::string_view namespace_key) const
Absolute path of the file backing a namespace.
Definition revocation.hpp:355
std::optional< SignedRevocationList > last_known(std::string_view namespace_key) const
The last-known accepted SRL for a namespace, if any (offline-last-known).
Definition revocation.hpp:380
static std::string make_namespace(std::string_view plugin_id, std::string_view revocation_pubkey_fpr, std::string_view update_channel)
Canonical namespace key: length-prefixed so ("a","bc",...) can't collide with ("ab",...
Definition revocation.hpp:339
SrlAcceptance accept_srl(const SignedRevocationList &srl, std::string_view plugin_id, std::string_view revocation_pubkey_fpr, std::string_view update_channel)
Offer a (already signature-verified) SRL for the given namespace.
Definition revocation.hpp:391
SrlFloorState read_floor(std::string_view namespace_key) const
Read the persisted floor for a namespace.
Definition revocation.hpp:363
EpochFloorStore(std::filesystem::path root)
Definition revocation.hpp:335
Definition build_fingerprint.hpp:23
SrlVerify
Definition revocation.hpp:64
@ Ok
Definition revocation.hpp:65
@ UntrustedSigner
the SRL's signer key is not the trusted revocation key
Definition revocation.hpp:67
@ BadSignature
Ed25519 verification failed (or malformed sig/key sizes).
Definition revocation.hpp:68
@ ParseError
JSON malformed or a required field missing/mistyped.
Definition revocation.hpp:66
bool is_revoked(const SignedRevocationList &srl, std::string_view signer_key_fpr, std::string_view artifact_hash)
True iff signer_key_fpr is a revoked signer OR artifact_hash is a revoked artifact in srl.
Definition revocation.hpp:286
std::string srl_to_lower(std::string s)
Lowercase a hex/ASCII string for case-insensitive fingerprint/hash compares.
Definition revocation.hpp:80
SrlAcceptance
Outcome of offering an SRL to the store.
Definition revocation.hpp:311
@ RejectedRollback
epoch <= stored floor — replay/rollback of an older SRL, refused
Definition revocation.hpp:313
@ Accepted
epoch is strictly newer than the floor; SRL persisted as last-known
Definition revocation.hpp:312
@ PersistError
could not atomically write the new last-known SRL
Definition revocation.hpp:315
@ FloorCorrupt
a floor file exists but is unreadable/garbage — refuse (fail-closed)
Definition revocation.hpp:314
std::optional< std::vector< std::uint8_t > > srl_hex_decode(std::string_view hex)
Decode a hex string to bytes.
Definition revocation.hpp:87
std::string serialize_srl_json(const SignedRevocationList &srl)
Serialize an SRL to a stable, canonical JSON string (fixed field order).
Definition revocation.hpp:205
std::vector< std::uint8_t > srl_signed_message(const SignedRevocationList &srl)
Deterministic canonical payload the SRL signature covers.
Definition revocation.hpp:122
SrlVerifyResult parse_and_verify_srl(std::string_view json, const std::vector< std::uint8_t > &trusted_revocation_pubkey, SignedRevocationList &out)
Parse + verify in one fail-closed step. A parse failure maps to ParseError.
Definition revocation.hpp:269
std::string srl_hex_encode(const std::vector< std::uint8_t > &bytes)
Encode bytes to lowercase hex.
Definition revocation.hpp:106
SrlVerifyResult verify_srl(const SignedRevocationList &srl, const std::vector< std::uint8_t > &trusted_revocation_pubkey)
Verify the SRL's Ed25519 signature against trusted_revocation_pubkey.
Definition revocation.hpp:249
@ Ok
Definition pack_install.hpp:27
std::optional< SignedRevocationList > parse_srl(std::string_view json, std::string &error)
Parse an SRL from JSON.
Definition revocation.hpp:156
@ error
Definition validation_harness.hpp:20
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
STL namespace.
A signed revocation list.
Definition revocation.hpp:54
std::uint64_t epoch
monotonic; higher = newer
Definition revocation.hpp:56
std::vector< std::uint8_t > signer_public_key
32 bytes (Ed25519), empty if unsigned
Definition revocation.hpp:60
std::vector< std::uint8_t > signature
64 bytes (Ed25519 detached), empty if unsigned
Definition revocation.hpp:61
std::string issued_utc
ISO-8601 UTC issue time (advisory).
Definition revocation.hpp:57
std::vector< std::string > revoked_artifact_hashes
lowercase-hex sha256 artifact hashes
Definition revocation.hpp:59
std::vector< std::string > revoked_signer_key_fprs
lowercase-hex Ed25519 pubkey fingerprints
Definition revocation.hpp:58
int schema_version
Definition revocation.hpp:55
State of the persisted floor for a namespace.
Definition revocation.hpp:319
std::optional< SignedRevocationList > last_known
the stored SRL when kind == Present
Definition revocation.hpp:322
enum pulp::format::reload::SrlFloorState::Kind kind
Kind
Definition revocation.hpp:320
@ Corrupt
Definition revocation.hpp:320
@ Present
Definition revocation.hpp:320
@ Absent
Definition revocation.hpp:320
std::uint64_t epoch
valid only when kind == Present
Definition revocation.hpp:321
Definition revocation.hpp:71
SrlVerify status
Definition revocation.hpp:72
bool ok() const
Definition revocation.hpp:74
std::string detail
Definition revocation.hpp:73