magenta-canon 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -0
- package/dist/MANIFEST.json +9 -5
- package/dist/scripts/demo-control-plane.js +33 -0
- package/dist/scripts/magenta-verify.js +76 -2
- package/dist/server/file-ledger-store.js +496 -0
- package/dist/server/ledger-store.js +118 -0
- package/dist/server/ledger.js +38 -0
- package/dist/server/storage.js +40 -28
- package/dist/server/transparency-log.js +68 -6
- package/dist/server/witness-identity.js +406 -0
- package/dist/server/witness.js +122 -2
- package/docs/FILE_LEDGER.md +111 -0
- package/docs/NPM_PACKAGING.md +21 -17
- package/docs/SECURITY_MODEL.md +9 -0
- package/docs/WITNESS_IDENTITY.md +134 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -170,6 +170,57 @@ npx magenta-canon verify bundle.json --expected-witness-key witness.pub
|
|
|
170
170
|
|
|
171
171
|
Full walkthrough: [`docs/MCP_GATEWAY.md`](docs/MCP_GATEWAY.md).
|
|
172
172
|
|
|
173
|
+
### Durable evidence (file ledger) — new in 0.4.0
|
|
174
|
+
|
|
175
|
+
By default evidence lives in memory (the demo's fresh-universe behavior). For
|
|
176
|
+
evidence that **survives a process restart**, point the gateway/control plane
|
|
177
|
+
at an append-only file ledger and pin the witness identity:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
MAGENTA_LEDGER_FILE=/var/lib/magenta/ledger.jsonl \
|
|
181
|
+
MAGENTA_WITNESS_SECRET=<hex> MAGENTA_WITNESS_PUBLIC=<hex> \
|
|
182
|
+
<your magenta process>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Every receipt and signed tree head is fsync-committed before it is reported
|
|
186
|
+
recorded; on restart the ledger is strictly replayed (chain, issuer
|
|
187
|
+
signatures, Merkle roots, checkpoint history) or startup fails loudly. One
|
|
188
|
+
exclusive writer per ledger; corruption is refused, never repaired silently.
|
|
189
|
+
This is single-writer local/reference durability — see
|
|
190
|
+
[`docs/FILE_LEDGER.md`](docs/FILE_LEDGER.md) for the format, lock recovery,
|
|
191
|
+
crash-tail policy, backup guidance, and limitations.
|
|
192
|
+
|
|
193
|
+
### Witness identity & authorized key rotation — new in 0.5.0
|
|
194
|
+
|
|
195
|
+
Instead of raw env keys, the witness can hold its identity in an **encrypted
|
|
196
|
+
keystore** (scrypt → AES-256-GCM, header-bound AAD) and **rotate its signing
|
|
197
|
+
key under cryptographic authorization** — the outgoing key signs the rotation,
|
|
198
|
+
the incoming key countersigns, and the signed rotation record is committed
|
|
199
|
+
**into the evidence ledger itself** before the new key signs anything:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
MAGENTA_LEDGER_FILE=/var/lib/magenta/ledger.jsonl \
|
|
203
|
+
MAGENTA_WITNESS_KEYFILE=/var/lib/magenta/witness.keystore \
|
|
204
|
+
MAGENTA_WITNESS_PASSPHRASE=<operator passphrase> \
|
|
205
|
+
<your magenta process>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Key material alone never grants authority — a validated, durably committed
|
|
209
|
+
rotation record does.** Verifiers replay the rotation chain from a pinned
|
|
210
|
+
anchor: historical checkpoints validate against the key that was authorized
|
|
211
|
+
for their epoch, substitution/rollback/forks are refused, and
|
|
212
|
+
`--expected-witness-key` still earns `ORIGIN AND INTEGRITY VERIFIED` across
|
|
213
|
+
rotations from the *original* pinned key. A crash at any point in the rotation
|
|
214
|
+
ceremony reconciles deterministically at next boot (recover the committed
|
|
215
|
+
fact, abandon the orphaned key, or fail closed — never silent activation).
|
|
216
|
+
|
|
217
|
+
**Scope honesty:** this is local/reference custody (file keystore, not
|
|
218
|
+
KMS/HSM), and it covers the **witness** identity only — founder/receipt-issuer
|
|
219
|
+
key custody is a separate forthcoming lane (in file-ledger mode the issuer key
|
|
220
|
+
still regenerates on restart; old evidence keeps verifying). Keystore format,
|
|
221
|
+
rotation protocol, boot-reconciliation table, and operator procedures:
|
|
222
|
+
[`docs/WITNESS_IDENTITY.md`](docs/WITNESS_IDENTITY.md) (ships in the package).
|
|
223
|
+
|
|
173
224
|
### From the repo vs. as a CLI
|
|
174
225
|
|
|
175
226
|
- **From a repo checkout** (above): `npm install` then `npm run demo`.
|
package/dist/MANIFEST.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package.json": "8005a3491db7d92f36ac66369861589f9c47123d3a7c71e643fc2c06168cd45a",
|
|
3
|
-
"scripts/demo-control-plane.js": "
|
|
3
|
+
"scripts/demo-control-plane.js": "3a559f39d17c1a403988e731e053d0d071cbd41ab595fd6246910f852337ab2f",
|
|
4
4
|
"scripts/intake-cli.js": "189014db22d6fccb034ed1a93ec11efe8905be820bc468366c68bb2cabaf8a97",
|
|
5
5
|
"scripts/magenta-mirror.js": "cf701065f7a6e20f7f44a9b815396aeb5fe031c3f003bcd793b8014ea8801b85",
|
|
6
|
-
"scripts/magenta-verify.js": "
|
|
6
|
+
"scripts/magenta-verify.js": "0bb65ef6ff1350789eecc4f0434ca94dcf3a89930f8ef1d62cc38100e18094ba",
|
|
7
7
|
"scripts/mcp-gateway.js": "335923004b73511fe42ea0fc6f2e567afe8018dc95464a79027e4c2537e30de1",
|
|
8
8
|
"server/agent-policy.js": "20dd9150f8244c33aaf14ecdf3a09f471210960d246cda9ab8d5abe0e8433518",
|
|
9
9
|
"server/agent-record.js": "790bbfa2a10601c2bdcd98873e6e41babdf96d1df7c7ca34f19791de4c8b860f",
|
|
10
10
|
"server/crypto.js": "ebbcb2929e7b3651e4ec04c9fbf3dcb656e3ae0d30bf8864f3642f72f3be2f39",
|
|
11
11
|
"server/execution-receipts.js": "171a506df7d1e99f3370de9a41c1a4f0b21b38d1f02edc78d9c44e68c93dbee4",
|
|
12
|
+
"server/file-ledger-store.js": "216fe644e1753a5527bec6ccf58175d22ec695c006e1927dc3090be11df9c124",
|
|
12
13
|
"server/intake/categories.js": "e9860eee0e2e0fe96c7ade165e5e068fae0874de2c7d39ff796efc91e713013b",
|
|
13
14
|
"server/intake/engine.js": "abb8feacd925520cbf01acc2e932d9ebb037a3d016b053b7b10deaf8f545a605",
|
|
14
15
|
"server/intake/gateway-intake.js": "40514fa489b031c29725e9b837e83998217bbc1ae84cf9259154a16b1faae2e8",
|
|
@@ -20,11 +21,14 @@
|
|
|
20
21
|
"server/intake/rationale.js": "3ea64a1430828ae5a06b41f4e93d06cdd667850d6a2ff9a1e266f98bdfc6c404",
|
|
21
22
|
"server/intake/types.js": "014891c6c8360648d6954b3185ae92d56be625484bf2f0d0f71dac271a59e23b",
|
|
22
23
|
"server/intake/witness.js": "588ad166f4870d65a6ed334c70aaa2deb1d6d96ec97d889902a9eda7528bd34f",
|
|
24
|
+
"server/ledger-store.js": "fc8a46f925908764bac7a076cffa08053af4c1f93237bee408eb0ed1e61eeca2",
|
|
25
|
+
"server/ledger.js": "9bf8f132e08d636070d2ede568b2eaf75810be245c90ecd5979d2d4de69af934",
|
|
23
26
|
"server/persistence.js": "2e6b1d0b1c74babbd581780b028c2593256c5ec96c2d60f4c25159e3f54e4e3f",
|
|
24
|
-
"server/storage.js": "
|
|
25
|
-
"server/transparency-log.js": "
|
|
27
|
+
"server/storage.js": "7a7556a6d15d736f028698b6094072daa71eed2230427594edac9d29b531081a",
|
|
28
|
+
"server/transparency-log.js": "7f238edc5cad4edc2c70a7b3e1977197cf2087e77789d181ffae0035f948391b",
|
|
26
29
|
"server/trust-bootstrap.js": "57a53a3ee9cd630ada2867e8b087a34b069ba26f86a696d3ead74888dd7e8d5f",
|
|
27
|
-
"server/witness.js": "
|
|
30
|
+
"server/witness-identity.js": "9dd5b6e63aefa00171a838d1cd27e3c09ddcb7d39512427ebe537763fe5fc42e",
|
|
31
|
+
"server/witness.js": "b1a8209d4dae4ffafc3ca4a158474b369250472ba80cef03bd92048282be91a4",
|
|
28
32
|
"shared/canonical.js": "476cee4b5c5702e8a2a198e79c2639cf8ff84000ffb92f68a04433ed2a2f5484",
|
|
29
33
|
"shared/certificates.js": "7844e71a38e1b1324586c7d054b2f5c15222b86446318d1e6dea9bab504a4a73",
|
|
30
34
|
"shared/schema.js": "f89190990e3826e44c722d5e8f5b39fd59b4d3fb9ec047229930a17604e4646c",
|
|
@@ -33,6 +33,9 @@ const node_crypto_1 = require("node:crypto");
|
|
|
33
33
|
const storage_1 = require("../server/storage");
|
|
34
34
|
const witness_1 = require("../server/witness");
|
|
35
35
|
const agent_record_1 = require("../server/agent-record");
|
|
36
|
+
const witness_2 = require("../server/witness");
|
|
37
|
+
const file_ledger_store_1 = require("../server/file-ledger-store");
|
|
38
|
+
const ledger_1 = require("../server/ledger");
|
|
36
39
|
const trust_bootstrap_1 = require("../server/trust-bootstrap");
|
|
37
40
|
const PORT = Number(process.env.PORT ?? 0);
|
|
38
41
|
const HOST = "127.0.0.1";
|
|
@@ -152,18 +155,48 @@ const server = (0, node_http_1.createServer)(async (req, res) => {
|
|
|
152
155
|
// ── GET /api/trust/evidence ──────────────────────────────────────────────
|
|
153
156
|
if (method === "GET" && url === "/api/trust/evidence") {
|
|
154
157
|
const receipts = [...(await storage_1.storage.getExecutionReceipts())].reverse(); // creation order
|
|
158
|
+
const rotations = ledger_1.sharedLedgerStore instanceof file_ledger_store_1.FileLedgerStore ? ledger_1.sharedLedgerStore.allRotations() : [];
|
|
155
159
|
return sendJson(res, 200, {
|
|
156
160
|
witness_pubkey: witness_1.witnessLog.witnessPublicKey,
|
|
157
161
|
sth: witness_1.witnessLog.latestSTH() ?? null,
|
|
158
162
|
receipts,
|
|
163
|
+
...(rotations.length > 0 ? { rotations } : {}),
|
|
159
164
|
});
|
|
160
165
|
}
|
|
166
|
+
// ── POST /internal/witness/rotate ────────────────────────────────────────
|
|
167
|
+
// Full continuity protocol (keystore + durable ledger required): mint →
|
|
168
|
+
// durably commit the signed rotation record → activate → switch epoch.
|
|
169
|
+
if (method === "POST" && url === "/internal/witness/rotate") {
|
|
170
|
+
if (!internalAuthOk(req, res))
|
|
171
|
+
return;
|
|
172
|
+
let body;
|
|
173
|
+
try {
|
|
174
|
+
body = (await readBody(req));
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
return sendJson(res, 400, { error: "bad_request", message: e.message });
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const record = (0, witness_2.rotateWitness)(body?.reason ?? "operator-initiated rotation");
|
|
181
|
+
return sendJson(res, 200, { rotated: true, rotation_id: record.rotation_id, new_version: record.new_version, new_pubkey: record.new_pubkey });
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
return sendJson(res, 409, { error: "rotation_refused", message: e.message });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
161
187
|
sendJson(res, 404, { error: "not_found", message: `${method} ${url} is not served by the demo control plane` });
|
|
162
188
|
}
|
|
163
189
|
catch (e) {
|
|
164
190
|
sendJson(res, 500, { error: "internal_error", message: e instanceof Error ? e.message : String(e) });
|
|
165
191
|
}
|
|
166
192
|
});
|
|
193
|
+
// File-ledger restart support: rebuild the witness's derived leaf view from
|
|
194
|
+
// any receipts the durable store replayed (no-op in the default memory mode).
|
|
195
|
+
void (async () => {
|
|
196
|
+
const restored = await storage_1.storage.getExecutionReceipts();
|
|
197
|
+
if (restored.length > 0)
|
|
198
|
+
(0, witness_2.rebuildWitnessFromReceipts)([...restored].reverse().map((r) => r.receipt_hash));
|
|
199
|
+
})();
|
|
167
200
|
server.listen(PORT, HOST, () => {
|
|
168
201
|
const addr = server.address();
|
|
169
202
|
const port = typeof addr === "object" && addr ? addr.port : PORT;
|
|
@@ -18,7 +18,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
18
18
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
19
|
};
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.verifyBundle = exports.verifyReceiptIssuerSignature = exports.verifyConsistency = exports.verifyReceiptChain = exports.verifySTH = exports.verifyInclusion = exports.merkleRoot = exports.hashLeaf = exports.chainHash = exports.canonicalHash = exports.canonicalize = exports.VERSION = void 0;
|
|
21
|
+
exports.verifyBundle = exports.verifyReceiptIssuerSignature = exports.verifyRotationChain = exports.verifyConsistency = exports.verifyReceiptChain = exports.verifySTH = exports.verifyInclusion = exports.merkleRoot = exports.hashLeaf = exports.chainHash = exports.canonicalHash = exports.canonicalize = exports.VERSION = void 0;
|
|
22
22
|
const node_crypto_1 = require("node:crypto");
|
|
23
23
|
const node_fs_1 = require("node:fs");
|
|
24
24
|
const tweetnacl_1 = __importDefault(require("tweetnacl"));
|
|
@@ -109,6 +109,59 @@ function verifyConsistency(allLeaves, older, newer) {
|
|
|
109
109
|
merkleRoot(allLeaves.slice(0, newer.tree_size)) === newer.root_hash);
|
|
110
110
|
}
|
|
111
111
|
exports.verifyConsistency = verifyConsistency;
|
|
112
|
+
/** Validate a rotation chain from an anchor key. Pure re-implementation of
|
|
113
|
+
* spec magenta-rotation/1 (sigs are Ed25519 over utf8(canonicalHash(body))). */
|
|
114
|
+
function verifyRotationChain(anchorPubkey, rotations) {
|
|
115
|
+
const GENESIS = "0".repeat(64);
|
|
116
|
+
let active = anchorPubkey;
|
|
117
|
+
let activeVersion = rotations.length ? rotations[0].old_version : 1;
|
|
118
|
+
let prev = GENESIS;
|
|
119
|
+
let lastBoundary = -1;
|
|
120
|
+
const seen = new Set();
|
|
121
|
+
const epochs = [{ pubkey: anchorPubkey, fromExclusive: -1 }];
|
|
122
|
+
for (const r of rotations) {
|
|
123
|
+
if (r.spec !== "magenta-rotation/1")
|
|
124
|
+
return { valid: false, reason: `unsupported rotation spec '${r.spec}'` };
|
|
125
|
+
if (seen.has(r.rotation_id))
|
|
126
|
+
return { valid: false, reason: "duplicate rotation_id" };
|
|
127
|
+
seen.add(r.rotation_id);
|
|
128
|
+
if (r.old_pubkey !== active)
|
|
129
|
+
return { valid: false, reason: "rotation old_pubkey is not the active key" };
|
|
130
|
+
if (r.old_version !== activeVersion)
|
|
131
|
+
return { valid: false, reason: "rotation old_version mismatch" };
|
|
132
|
+
if (r.new_version !== activeVersion + 1)
|
|
133
|
+
return { valid: false, reason: "skipped key version" };
|
|
134
|
+
if (r.previous_rotation_hash !== prev)
|
|
135
|
+
return { valid: false, reason: "rotation chain hash broken" };
|
|
136
|
+
if (r.effective_tree_size <= lastBoundary)
|
|
137
|
+
return { valid: false, reason: "rotation boundaries must strictly increase" };
|
|
138
|
+
const body = {};
|
|
139
|
+
for (const k of Object.keys(r)) {
|
|
140
|
+
if (k !== "old_key_signature" && k !== "new_key_countersignature" && k !== "record_hash")
|
|
141
|
+
body[k] = r[k];
|
|
142
|
+
}
|
|
143
|
+
const h = (0, exports.canonicalHash)(body);
|
|
144
|
+
try {
|
|
145
|
+
if (!tweetnacl_1.default.sign.detached.verify(utf8(h), fromHex(r.old_key_signature), fromHex(r.old_pubkey)))
|
|
146
|
+
return { valid: false, reason: "old-key authorization signature invalid" };
|
|
147
|
+
if (!tweetnacl_1.default.sign.detached.verify(utf8(h), fromHex(r.new_key_countersignature), fromHex(r.new_pubkey)))
|
|
148
|
+
return { valid: false, reason: "new-key countersignature invalid" };
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return { valid: false, reason: "malformed rotation signature material" };
|
|
152
|
+
}
|
|
153
|
+
if ((0, exports.canonicalHash)({ ...body, old_key_signature: r.old_key_signature, new_key_countersignature: r.new_key_countersignature }) !== r.record_hash) {
|
|
154
|
+
return { valid: false, reason: "rotation record_hash mismatch" };
|
|
155
|
+
}
|
|
156
|
+
active = r.new_pubkey;
|
|
157
|
+
activeVersion = r.new_version;
|
|
158
|
+
prev = r.record_hash;
|
|
159
|
+
lastBoundary = r.effective_tree_size;
|
|
160
|
+
epochs.push({ pubkey: r.new_pubkey, fromExclusive: r.effective_tree_size });
|
|
161
|
+
}
|
|
162
|
+
return { valid: true, epochs, tipPubkey: active };
|
|
163
|
+
}
|
|
164
|
+
exports.verifyRotationChain = verifyRotationChain;
|
|
112
165
|
const HEX64 = /^[0-9a-f]{64}$/;
|
|
113
166
|
/** Receipt issuer signature (spec §2): Ed25519 over the canonical receipt body
|
|
114
167
|
* (all fields except receipt_signature), message = UTF-8 of the hex hash. */
|
|
@@ -150,10 +203,31 @@ function verifyBundle(b, opts = {}) {
|
|
|
150
203
|
if (b.sth) {
|
|
151
204
|
add("STH signature verifies", verifySTH(b.sth), `root ${b.sth.root_hash.slice(0, 16)}… size ${b.sth.tree_size}`);
|
|
152
205
|
// Internal coherence: the bundle's top-level key must agree with the STH's.
|
|
206
|
+
// (With rotations, the top-level key is the chain TIP by construction.)
|
|
153
207
|
if (b.witness_pubkey !== undefined) {
|
|
154
208
|
add("bundle witness_pubkey == STH witness key", b.witness_pubkey === b.sth.witness_pubkey, b.witness_pubkey === b.sth.witness_pubkey ? "bundle is internally coherent" : "BUNDLE KEY FIELDS DISAGREE");
|
|
155
209
|
}
|
|
156
|
-
if (
|
|
210
|
+
if (b.rotations && b.rotations.length > 0) {
|
|
211
|
+
// Rotation continuity: the pinned key (or, integrity-only, the chain's
|
|
212
|
+
// own anchor) must chain by signed authorization+countersignature to
|
|
213
|
+
// the key that signed this STH at its tree size.
|
|
214
|
+
const anchor = (witnessKeySource === "cli" && HEX64.test(opts.expectedWitnessKey))
|
|
215
|
+
? opts.expectedWitnessKey
|
|
216
|
+
: b.rotations[0].old_pubkey;
|
|
217
|
+
const chain = verifyRotationChain(anchor, b.rotations);
|
|
218
|
+
add(`rotation chain validates from ${witnessKeySource === "cli" ? "PINNED anchor" : "bundle anchor"} (${b.rotations.length} rotation(s))`, chain.valid, chain.valid ? `tip key …${chain.tipPubkey.slice(-12)}` : `ROTATION CHAIN INVALID: ${chain.reason}`);
|
|
219
|
+
if (chain.valid) {
|
|
220
|
+
let expected = chain.epochs[0].pubkey;
|
|
221
|
+
for (const e of chain.epochs)
|
|
222
|
+
if (b.sth.tree_size > e.fromExclusive)
|
|
223
|
+
expected = e.pubkey;
|
|
224
|
+
add("STH signed by the authorized key for its epoch", b.sth.witness_pubkey === expected, b.sth.witness_pubkey === expected ? "epoch boundary respected" : "STH KEY OUTSIDE THE AUTHORIZED ROTATION CHAIN");
|
|
225
|
+
}
|
|
226
|
+
if (witnessKeySource === "bundle") {
|
|
227
|
+
skip("witness key trust", "rotation chain anchored to the bundle's own first key — supply --expected-witness-key (the version-1 anchor) for origin assurance");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else if (witnessKeySource === "cli" && HEX64.test(opts.expectedWitnessKey)) {
|
|
157
231
|
const match = b.sth.witness_pubkey === opts.expectedWitnessKey;
|
|
158
232
|
add("STH witness key matches INDEPENDENTLY supplied key", match, match ? "key supplied via --expected-witness-key matches" : "WITNESS KEY MISMATCH — bundle was NOT signed by the trusted witness");
|
|
159
233
|
}
|