Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
swap_pack.hpp
Go to the documentation of this file.
1#pragma once
2
17
19
20#include <choc/text/choc_JSON.h>
21
22#include <cctype>
23#include <cstdint>
24#include <filesystem>
25#include <fstream>
26#include <optional>
27#include <string>
28#include <string_view>
29#include <system_error>
30#include <vector>
31
32namespace pulp::format::reload {
33
38
39inline SwapPackKind swap_pack_kind_from_string(std::string_view s) {
40 if (s == "ui-script") return SwapPackKind::UiScript;
41 if (s == "dsp-graph") return SwapPackKind::DspGraph;
42 if (s == "node-pack") return SwapPackKind::NodePack;
43 if (s == "wasm-dsp") return SwapPackKind::WasmDsp;
45}
46
50 switch (k) {
51 case SwapPackKind::UiScript: return "ui-script";
52 case SwapPackKind::DspGraph: return "dsp-graph";
53 case SwapPackKind::NodePack: return "node-pack";
54 case SwapPackKind::WasmDsp: return "wasm-dsp";
55 case SwapPackKind::Unknown: break;
56 }
57 return "unknown";
58}
59
65
67 std::string id;
68 std::string plugin_id;
70 std::vector<SwapPackFile> files;
71 // Policy fields. These are covered by the signature (see swap_pack_signed_message)
72 // so a CDN or installer cannot swap the declared capabilities, version, or kind
73 // while keeping a valid signature โ€” the manifest is the single sanctioned place
74 // these are trusted from.
75 std::uint64_t pack_version = 0;
77 std::vector<std::string> declared_capabilities;
78 std::string update_channel;
80 std::vector<std::uint8_t> signer_public_key;
81 std::vector<std::uint8_t> signature;
82};
83
93
99
102inline std::optional<std::vector<std::uint8_t>> swap_pack_hex_decode(std::string_view hex) {
103 if (hex.size() % 2 != 0) return std::nullopt;
104 auto nibble = [](char c) -> int {
105 if (c >= '0' && c <= '9') return c - '0';
106 if (c >= 'a' && c <= 'f') return c - 'a' + 10;
107 if (c >= 'A' && c <= 'F') return c - 'A' + 10;
108 return -1;
109 };
110 std::vector<std::uint8_t> out;
111 out.reserve(hex.size() / 2);
112 for (std::size_t i = 0; i < hex.size(); i += 2) {
113 const int hi = nibble(hex[i]), lo = nibble(hex[i + 1]);
114 if (hi < 0 || lo < 0) return std::nullopt;
115 out.push_back(static_cast<std::uint8_t>((hi << 4) | lo));
116 }
117 return out;
118}
119
124inline std::optional<SwapPackManifest> parse_swap_pack_manifest(std::string_view json,
125 std::string& error) {
127 try {
128 auto v = choc::json::parse(std::string(json));
129 if (!v.isObject()) { error = "manifest is not a JSON object"; return std::nullopt; }
130 if (!v.hasObjectMember("id") || !v["id"].isString()) {
131 error = "missing/invalid string 'id'"; return std::nullopt;
132 }
133 if (!v.hasObjectMember("plugin_id") || !v["plugin_id"].isString()) {
134 error = "missing/invalid string 'plugin_id'"; return std::nullopt;
135 }
136 m.id = v["id"].getString();
137 m.plugin_id = v["plugin_id"].getString();
138 if (v.hasObjectMember("format_version") && v["format_version"].isInt())
139 m.format_version = static_cast<int>(v["format_version"].getInt64());
140 if (!v.hasObjectMember("files") || !v["files"].isArray()) {
141 error = "missing/invalid array 'files'"; return std::nullopt;
142 }
143 const auto& files = v["files"];
144 for (uint32_t i = 0; i < files.size(); ++i) {
145 const auto& f = files[i];
146 if (!f.isObject() || !f.hasObjectMember("path") || !f["path"].isString() ||
147 !f.hasObjectMember("sha256") || !f["sha256"].isString()) {
148 error = "files[" + std::to_string(i) + "] missing string 'path'/'sha256'";
149 return std::nullopt;
150 }
151 SwapPackFile pf;
152 pf.path = f["path"].getString();
153 pf.sha256_hex = f["sha256"].getString();
154 pf.kind = f.hasObjectMember("kind") && f["kind"].isString()
155 ? swap_pack_kind_from_string(f["kind"].getString())
157 m.files.push_back(std::move(pf));
158 }
159 // Policy fields. All optional with safe defaults so an older manifest still
160 // parses; whatever is present is bound into the signed message below.
161 if (v.hasObjectMember("pack_version") && v["pack_version"].isInt())
162 m.pack_version = static_cast<std::uint64_t>(v["pack_version"].getInt64());
163 if (v.hasObjectMember("pack_type") && v["pack_type"].isString())
164 m.pack_type = swap_pack_kind_from_string(v["pack_type"].getString());
165 if (v.hasObjectMember("update_channel") && v["update_channel"].isString())
166 m.update_channel = v["update_channel"].getString();
167 if (v.hasObjectMember("min_host_version") && v["min_host_version"].isInt())
168 m.min_host_version = static_cast<int>(v["min_host_version"].getInt64());
169 if (v.hasObjectMember("capabilities") && v["capabilities"].isArray()) {
170 const auto& caps = v["capabilities"];
171 for (uint32_t i = 0; i < caps.size(); ++i)
172 if (caps[i].isString()) m.declared_capabilities.emplace_back(caps[i].getString());
173 }
174 // Optional signature fields (hex). Absent โ†’ unsigned manifest (empty
175 // vectors); a malformed hex string is left empty so verification fails
176 // closed rather than parsing to garbage bytes.
177 if (v.hasObjectMember("signer") && v["signer"].isString()) {
178 if (auto k = swap_pack_hex_decode(v["signer"].getString())) m.signer_public_key = std::move(*k);
179 }
180 if (v.hasObjectMember("signature") && v["signature"].isString()) {
181 if (auto s = swap_pack_hex_decode(v["signature"].getString())) m.signature = std::move(*s);
182 }
183 } catch (const std::exception& e) {
184 error = std::string("manifest JSON parse failed: ") + e.what();
185 return std::nullopt;
186 }
187 return m;
188}
189
193inline SwapPackVerifyResult verify_swap_pack_integrity(const std::filesystem::path& root,
194 const SwapPackManifest& manifest) {
195 auto to_lower = [](std::string s) {
196 for (auto& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
197 return s;
198 };
199 for (const auto& f : manifest.files) {
200 const auto full = root / f.path;
201 std::error_code ec;
202 if (!std::filesystem::exists(full, ec) || ec) {
203 return {SwapPackVerify::MissingFile, f.path};
204 }
205 std::ifstream in(full, std::ios::binary);
206 if (!in) return {SwapPackVerify::ReadError, f.path};
207 std::vector<std::uint8_t> bytes((std::istreambuf_iterator<char>(in)),
208 std::istreambuf_iterator<char>());
209 if (in.bad()) return {SwapPackVerify::ReadError, f.path};
210 const std::string actual = runtime::sha256_hex(bytes.data(), bytes.size());
211 if (to_lower(actual) != to_lower(f.sha256_hex)) {
212 return {SwapPackVerify::HashMismatch, f.path};
213 }
214 }
215 return {SwapPackVerify::Ok, ""};
216}
217
228inline std::vector<std::uint8_t> swap_pack_signed_message(const SwapPackManifest& m) {
229 std::vector<std::uint8_t> out;
230 auto put_u32 = [&out](std::uint32_t n) {
231 out.push_back(static_cast<std::uint8_t>(n & 0xFF));
232 out.push_back(static_cast<std::uint8_t>((n >> 8) & 0xFF));
233 out.push_back(static_cast<std::uint8_t>((n >> 16) & 0xFF));
234 out.push_back(static_cast<std::uint8_t>((n >> 24) & 0xFF));
235 };
236 auto put_field = [&](std::string_view s) {
237 put_u32(static_cast<std::uint32_t>(s.size()));
238 out.insert(out.end(), s.begin(), s.end());
239 };
240 auto put_u64 = [&out](std::uint64_t n) {
241 for (int i = 0; i < 8; ++i) out.push_back(static_cast<std::uint8_t>((n >> (8 * i)) & 0xFF));
242 };
243 // v2 binds the policy fields (version, kind, capabilities, channel, min-host)
244 // into the signature so a CDN/installer cannot alter them while keeping a valid
245 // signature. The tag bump also means a v1 signature can never verify as v2.
246 put_field("pulp-swap-pack-v2");
247 put_field(m.id);
248 put_field(m.plugin_id);
249 put_u32(static_cast<std::uint32_t>(m.format_version));
250 put_u64(m.pack_version);
251 put_field(swap_pack_kind_to_string(m.pack_type));
252 put_field(m.update_channel);
253 put_u32(static_cast<std::uint32_t>(m.min_host_version));
254 put_u32(static_cast<std::uint32_t>(m.declared_capabilities.size())); // bind the count
255 for (const auto& c : m.declared_capabilities) put_field(c);
256 put_u32(static_cast<std::uint32_t>(m.files.size())); // bind the file count
257 for (const auto& f : m.files) {
258 put_field(f.path);
259 put_field(f.sha256_hex);
260 put_field(swap_pack_kind_to_string(f.kind));
261 }
262 return out;
263}
264
270 const SwapPackManifest& manifest,
271 const std::vector<std::uint8_t>& trusted_public_key) {
272 // Trust root: the signer key must be exactly the trusted key. (Key
273 // distribution / a multi-key trust store is an owner-steer decision โ€” see the
274 // Phase-3 scoping note; this takes a single trusted key by value.)
276 trusted_public_key.size() != runtime::ed25519_public_key_size ||
277 manifest.signer_public_key != trusted_public_key) {
278 return {SwapPackVerify::UntrustedSigner, "signer key is not the trusted key"};
279 }
280 if (manifest.signature.size() != runtime::ed25519_signature_size) {
281 return {SwapPackVerify::BadSignature, "signature size invalid"};
282 }
283 const auto message = swap_pack_signed_message(manifest);
284 if (!runtime::ed25519_verify(manifest.signer_public_key.data(),
285 manifest.signer_public_key.size(),
286 manifest.signature.data(), manifest.signature.size(),
287 message.data(), message.size())) {
288 return {SwapPackVerify::BadSignature, "Ed25519 verification failed"};
289 }
290 return {SwapPackVerify::Ok, ""};
291}
292
301 const std::filesystem::path& root, const SwapPackManifest& manifest,
302 const std::vector<std::uint8_t>& trusted_public_key) {
303 auto sig = verify_swap_pack_signature(manifest, trusted_public_key);
304 if (!sig.ok()) return sig;
305 return verify_swap_pack_integrity(root, manifest);
306}
307
309inline std::string swap_pack_hex_encode(const std::vector<std::uint8_t>& bytes) {
310 return runtime::hex_encode(bytes);
311}
312
314inline std::string swap_pack_json_escape(std::string_view s) {
315 std::string out;
316 out.reserve(s.size() + 2);
317 for (char c : s) {
318 switch (c) {
319 case '"': out += "\\\""; break;
320 case '\\': out += "\\\\"; break;
321 case '\n': out += "\\n"; break;
322 case '\r': out += "\\r"; break;
323 case '\t': out += "\\t"; break;
324 default:
325 if (static_cast<unsigned char>(c) < 0x20) {
326 static const char* d = "0123456789abcdef";
327 out += "\\u00";
328 out.push_back(d[(c >> 4) & 0xF]);
329 out.push_back(d[c & 0xF]);
330 } else {
331 out.push_back(c);
332 }
333 }
334 }
335 return out;
336}
337
341inline std::string serialize_swap_pack_manifest(const SwapPackManifest& m) {
342 auto q = [](std::string_view s) { return "\"" + swap_pack_json_escape(s) + "\""; };
343 std::string j = "{\n";
344 j += " \"id\": " + q(m.id) + ",\n";
345 j += " \"plugin_id\": " + q(m.plugin_id) + ",\n";
346 j += " \"format_version\": " + std::to_string(m.format_version) + ",\n";
347 j += " \"pack_version\": " + std::to_string(m.pack_version) + ",\n";
348 j += " \"pack_type\": " + q(swap_pack_kind_to_string(m.pack_type)) + ",\n";
349 j += " \"update_channel\": " + q(m.update_channel) + ",\n";
350 j += " \"min_host_version\": " + std::to_string(m.min_host_version) + ",\n";
351 j += " \"capabilities\": [";
352 for (std::size_t i = 0; i < m.declared_capabilities.size(); ++i)
353 j += (i ? ", " : "") + q(m.declared_capabilities[i]);
354 j += "],\n";
355 j += " \"files\": [\n";
356 for (std::size_t i = 0; i < m.files.size(); ++i) {
357 const auto& f = m.files[i];
358 j += " {\"path\": " + q(f.path) + ", \"sha256\": " + q(f.sha256_hex) +
359 ", \"kind\": " + q(swap_pack_kind_to_string(f.kind)) + "}";
360 j += (i + 1 < m.files.size()) ? ",\n" : "\n";
361 }
362 j += " ]";
363 if (!m.signer_public_key.empty())
364 j += ",\n \"signer\": " + q(swap_pack_hex_encode(m.signer_public_key));
365 if (!m.signature.empty())
366 j += ",\n \"signature\": " + q(swap_pack_hex_encode(m.signature));
367 j += "\n}\n";
368 return j;
369}
370
371} // namespace pulp::format::reload
Definition build_fingerprint.hpp:23
SwapPackKind swap_pack_kind_from_string(std::string_view s)
Definition swap_pack.hpp:39
@ 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
SwapPackVerifyResult verify_swap_pack(const std::filesystem::path &root, const SwapPackManifest &manifest, const std::vector< std::uint8_t > &trusted_public_key)
Full fail-closed verify: signature FIRST (binds the file hashes without reading the files),...
Definition swap_pack.hpp:300
std::string swap_pack_json_escape(std::string_view s)
Minimal JSON string escaper for the fields the manifest emits.
Definition swap_pack.hpp:314
const char * swap_pack_kind_to_string(SwapPackKind k)
Stable canonical spelling of a kind, for the signed message (never localized / reformatted).
Definition swap_pack.hpp:49
std::optional< std::vector< std::uint8_t > > swap_pack_hex_decode(std::string_view hex)
Decode a lowercase/uppercase hex string to bytes.
Definition swap_pack.hpp:102
std::vector< std::uint8_t > swap_pack_signed_message(const SwapPackManifest &m)
Deterministic canonical payload the pack signature covers.
Definition swap_pack.hpp:228
std::optional< SwapPackManifest > parse_swap_pack_manifest(std::string_view json, std::string &error)
Parse a swap-pack manifest from JSON.
Definition swap_pack.hpp:124
SwapPackVerifyResult verify_swap_pack_signature(const SwapPackManifest &manifest, const std::vector< std::uint8_t > &trusted_public_key)
Verify the pack's Ed25519 signature.
Definition swap_pack.hpp:269
SwapPackKind
Payload kind for one file in a swap pack.
Definition swap_pack.hpp:37
@ DspGraph
Definition swap_pack.hpp:37
@ UiScript
Definition swap_pack.hpp:37
@ Unknown
Definition swap_pack.hpp:37
@ NodePack
Definition swap_pack.hpp:37
@ WasmDsp
Definition swap_pack.hpp:37
@ Ok
Definition pack_install.hpp:27
SwapPackVerify
Definition swap_pack.hpp:84
@ HashMismatch
a file's SHA-256 != the manifest's declared hash
Definition swap_pack.hpp:89
@ Ok
Definition swap_pack.hpp:85
@ UntrustedSigner
the manifest's signer key is not the trusted key
Definition swap_pack.hpp:90
@ MissingFile
a declared file is absent under the pack root
Definition swap_pack.hpp:87
@ ManifestParseError
manifest JSON malformed or missing required fields
Definition swap_pack.hpp:86
@ BadSignature
Ed25519 verification failed (or malformed sig/key sizes).
Definition swap_pack.hpp:91
@ ReadError
a declared file could not be read
Definition swap_pack.hpp:88
std::string serialize_swap_pack_manifest(const SwapPackManifest &m)
Serialize a manifest to JSON that parse_swap_pack_manifest reads back verbatim (round-trips).
Definition swap_pack.hpp:341
std::string swap_pack_hex_encode(const std::vector< std::uint8_t > &bytes)
Lowercase-hex encode bytes (for signer/signature fields on the way out).
Definition swap_pack.hpp:309
SwapPackVerifyResult verify_swap_pack_integrity(const std::filesystem::path &root, const SwapPackManifest &manifest)
Verify every declared file exists under root and its bytes hash to the declared SHA-256.
Definition swap_pack.hpp:193
@ 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
Definition swap_pack.hpp:60
std::string sha256_hex
expected lowercase hex SHA-256 of the file bytes
Definition swap_pack.hpp:62
std::string path
path relative to the pack root
Definition swap_pack.hpp:61
SwapPackKind kind
Definition swap_pack.hpp:63
Definition swap_pack.hpp:66
std::vector< std::string > declared_capabilities
capability names the UI is granted
Definition swap_pack.hpp:77
int format_version
Definition swap_pack.hpp:69
std::vector< std::uint8_t > signer_public_key
32 bytes (Ed25519), empty if unsigned
Definition swap_pack.hpp:80
std::vector< SwapPackFile > files
Definition swap_pack.hpp:70
int min_host_version
minimum host ABI/app version that may load it
Definition swap_pack.hpp:79
std::string plugin_id
the plugin this pack targets
Definition swap_pack.hpp:68
std::string id
pack identity
Definition swap_pack.hpp:67
std::string update_channel
delivery channel (also namespaces revocation)
Definition swap_pack.hpp:78
SwapPackKind pack_type
overall pack kind (gates delivery lanes)
Definition swap_pack.hpp:76
std::vector< std::uint8_t > signature
64 bytes (Ed25519 detached), empty if unsigned
Definition swap_pack.hpp:81
std::uint64_t pack_version
monotonic; a loader rejects a downgrade
Definition swap_pack.hpp:75
std::string detail
which file / what went wrong (diagnostics)
Definition swap_pack.hpp:96
SwapPackVerify status
Definition swap_pack.hpp:95
bool ok() const
Definition swap_pack.hpp:97