magenta-canon 0.4.0 → 0.6.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 +66 -0
- package/bin/magenta-canon.mjs +8 -1
- package/dist/MANIFEST.json +15 -5
- package/dist/scripts/demo-control-plane.js +25 -0
- package/dist/scripts/magenta-sentinel.js +192 -0
- package/dist/scripts/magenta-verify.js +76 -2
- package/dist/server/file-ledger-store.js +86 -1
- package/dist/server/sentinel/artifact-sentinel.js +286 -0
- package/dist/server/sentinel/promotion.js +58 -0
- package/dist/server/sentinel/repository-sentinel.js +204 -0
- package/dist/server/sentinel/rules.js +114 -0
- package/dist/server/sentinel/tar-reader.js +116 -0
- package/dist/server/sentinel/violation.js +73 -0
- package/dist/server/sentinel/witness-sentinel.js +408 -0
- package/dist/server/transparency-log.js +42 -7
- package/dist/server/witness-identity.js +406 -0
- package/dist/server/witness.js +113 -1
- package/dist/shared/invariants.js +755 -0
- package/docs/NPM_PACKAGING.md +26 -17
- package/docs/SECURITY_MODEL.md +9 -0
- package/docs/WITNESS_IDENTITY.md +134 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ npx magenta-canon demo # full local proof loop: allow · block
|
|
|
22
22
|
npx magenta-canon verify --self-test # verifier self-check: all verdict levels incl. a tamper FAIL
|
|
23
23
|
npx magenta-canon gateway <config.json> # the stdio MCP capability gateway
|
|
24
24
|
npx magenta-canon mirror --self-test # external STH mirror: catch operator history rewrites
|
|
25
|
+
npx magenta-canon sentinel pack # Sentinel Mesh: invariant-check the packed artifact
|
|
25
26
|
```
|
|
26
27
|
|
|
27
28
|
Magenta Canon is a proven **reference implementation**, not production-hosted infra
|
|
@@ -190,6 +191,71 @@ This is single-writer local/reference durability — see
|
|
|
190
191
|
[`docs/FILE_LEDGER.md`](docs/FILE_LEDGER.md) for the format, lock recovery,
|
|
191
192
|
crash-tail policy, backup guidance, and limitations.
|
|
192
193
|
|
|
194
|
+
### Witness identity & authorized key rotation — new in 0.5.0
|
|
195
|
+
|
|
196
|
+
Instead of raw env keys, the witness can hold its identity in an **encrypted
|
|
197
|
+
keystore** (scrypt → AES-256-GCM, header-bound AAD) and **rotate its signing
|
|
198
|
+
key under cryptographic authorization** — the outgoing key signs the rotation,
|
|
199
|
+
the incoming key countersigns, and the signed rotation record is committed
|
|
200
|
+
**into the evidence ledger itself** before the new key signs anything:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
MAGENTA_LEDGER_FILE=/var/lib/magenta/ledger.jsonl \
|
|
204
|
+
MAGENTA_WITNESS_KEYFILE=/var/lib/magenta/witness.keystore \
|
|
205
|
+
MAGENTA_WITNESS_PASSPHRASE=<operator passphrase> \
|
|
206
|
+
<your magenta process>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Key material alone never grants authority — a validated, durably committed
|
|
210
|
+
rotation record does.** Verifiers replay the rotation chain from a pinned
|
|
211
|
+
anchor: historical checkpoints validate against the key that was authorized
|
|
212
|
+
for their epoch, substitution/rollback/forks are refused, and
|
|
213
|
+
`--expected-witness-key` still earns `ORIGIN AND INTEGRITY VERIFIED` across
|
|
214
|
+
rotations from the *original* pinned key. A crash at any point in the rotation
|
|
215
|
+
ceremony reconciles deterministically at next boot (recover the committed
|
|
216
|
+
fact, abandon the orphaned key, or fail closed — never silent activation).
|
|
217
|
+
|
|
218
|
+
**Scope honesty:** this is local/reference custody (file keystore, not
|
|
219
|
+
KMS/HSM), and it covers the **witness** identity only — founder/receipt-issuer
|
|
220
|
+
key custody is a separate forthcoming lane (in file-ledger mode the issuer key
|
|
221
|
+
still regenerates on restart; old evidence keeps verifying). Keystore format,
|
|
222
|
+
rotation protocol, boot-reconciliation table, and operator procedures:
|
|
223
|
+
[`docs/WITNESS_IDENTITY.md`](docs/WITNESS_IDENTITY.md) (ships in the package).
|
|
224
|
+
|
|
225
|
+
### Sentinel Mesh foundation — new in 0.6.0
|
|
226
|
+
|
|
227
|
+
Magenta Canon 0.6.0 introduces an **executable Sentinel Mesh** that protects
|
|
228
|
+
repository state, npm artifacts, dependency boundaries, release promotion,
|
|
229
|
+
witness identity, and cryptographically authorized witness-key rotation:
|
|
230
|
+
versioned invariants, deterministic repository / artifact / witness
|
|
231
|
+
evaluation, evidence-preserving violations, and scoped release-promotion
|
|
232
|
+
blocking.
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
magenta-canon sentinel repository # invariant-check a git repository
|
|
236
|
+
magenta-canon sentinel artifact <tgz|dir> # invariant-check a packed artifact
|
|
237
|
+
magenta-canon sentinel pack # npm pack, then check the result
|
|
238
|
+
magenta-canon sentinel witness --ledger ledger.jsonl # witness rotation-authority check
|
|
239
|
+
magenta-canon sentinel invariants # list the invariant registry
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The **Witness & Rotation Sentinel** (MC-WIT-001…011) watches the 0.5.0
|
|
243
|
+
rotation-authority model itself: pinned-anchor continuity, dual-signed
|
|
244
|
+
rotation validity, exact epoch boundaries, epoch-correct STH signing,
|
|
245
|
+
retired-key resurrection / pre-activation refusal, and keystore↔ledger
|
|
246
|
+
agreement — re-derived independently from the published wire formats, so it
|
|
247
|
+
can disagree with a wrong (or compromised) server.
|
|
248
|
+
|
|
249
|
+
Sentinels are read-only, redacting (paths + fingerprints, never secret
|
|
250
|
+
bodies), deterministic, network-free, and emit `magenta-sentinel-violation/1`
|
|
251
|
+
evidence records; exit codes map to a scoped promotion decision (`eligible` /
|
|
252
|
+
`blocked` / `requires-independent-review`). Records at this layer are
|
|
253
|
+
**unsigned local diagnostics**, and the freeze is a release/build promotion
|
|
254
|
+
gate — **not** a production kill switch, a hosted Sentinel service, or
|
|
255
|
+
complete Mesh coverage (ledger/execution/configuration sentinels are future
|
|
256
|
+
lanes). Honest scope and the full model:
|
|
257
|
+
[`docs/SENTINEL_MESH.md`](docs/SENTINEL_MESH.md).
|
|
258
|
+
|
|
193
259
|
### From the repo vs. as a CLI
|
|
194
260
|
|
|
195
261
|
- **From a repo checkout** (above): `npm install` then `npm run demo`.
|
package/bin/magenta-canon.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* magenta-canon — CLI entry point for the open-source reference implementation.
|
|
4
4
|
*
|
|
@@ -48,6 +48,8 @@ Usage:
|
|
|
48
48
|
verify <mirror>, check <mirror> <bundle>, --self-test.
|
|
49
49
|
magenta-canon mirror-feed ... One feed cycle for a mirror operator:
|
|
50
50
|
(--bundle <file> | --url <evidence-url>) --mirror <file>.
|
|
51
|
+
magenta-canon sentinel <cmd> Sentinel Mesh: repository | artifact <tgz|dir> |
|
|
52
|
+
pack | invariants (deterministic invariant checks).
|
|
51
53
|
magenta-canon --help Show this help.
|
|
52
54
|
magenta-canon --version Print the version.
|
|
53
55
|
|
|
@@ -110,6 +112,11 @@ switch (sub) {
|
|
|
110
112
|
// Plain-Node operator helper; delegates all mirror semantics to `mirror`.
|
|
111
113
|
run(process.execPath, [path.join(ROOT, "scripts", "mirror-feed.mjs"), ...rest], { cwd: process.cwd() });
|
|
112
114
|
break;
|
|
115
|
+
case "sentinel":
|
|
116
|
+
// Sentinel Mesh foundation: deterministic invariant evaluation over a
|
|
117
|
+
// repository or a packed artifact (read-only; unsigned local diagnostics).
|
|
118
|
+
runTs("scripts/magenta-sentinel.ts", rest, { cwd: process.cwd() });
|
|
119
|
+
break;
|
|
113
120
|
case "-v":
|
|
114
121
|
case "--version":
|
|
115
122
|
case "version":
|
package/dist/MANIFEST.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
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-
|
|
6
|
+
"scripts/magenta-sentinel.js": "0280d7584524201551c61a93beb1f2ee4964e76b0e44eb7c9d15a0f5a95cafdc",
|
|
7
|
+
"scripts/magenta-verify.js": "0bb65ef6ff1350789eecc4f0434ca94dcf3a89930f8ef1d62cc38100e18094ba",
|
|
7
8
|
"scripts/mcp-gateway.js": "335923004b73511fe42ea0fc6f2e567afe8018dc95464a79027e4c2537e30de1",
|
|
8
9
|
"server/agent-policy.js": "20dd9150f8244c33aaf14ecdf3a09f471210960d246cda9ab8d5abe0e8433518",
|
|
9
10
|
"server/agent-record.js": "790bbfa2a10601c2bdcd98873e6e41babdf96d1df7c7ca34f19791de4c8b860f",
|
|
10
11
|
"server/crypto.js": "ebbcb2929e7b3651e4ec04c9fbf3dcb656e3ae0d30bf8864f3642f72f3be2f39",
|
|
11
12
|
"server/execution-receipts.js": "171a506df7d1e99f3370de9a41c1a4f0b21b38d1f02edc78d9c44e68c93dbee4",
|
|
12
|
-
"server/file-ledger-store.js": "
|
|
13
|
+
"server/file-ledger-store.js": "216fe644e1753a5527bec6ccf58175d22ec695c006e1927dc3090be11df9c124",
|
|
13
14
|
"server/intake/categories.js": "e9860eee0e2e0fe96c7ade165e5e068fae0874de2c7d39ff796efc91e713013b",
|
|
14
15
|
"server/intake/engine.js": "abb8feacd925520cbf01acc2e932d9ebb037a3d016b053b7b10deaf8f545a605",
|
|
15
16
|
"server/intake/gateway-intake.js": "40514fa489b031c29725e9b837e83998217bbc1ae84cf9259154a16b1faae2e8",
|
|
@@ -24,12 +25,21 @@
|
|
|
24
25
|
"server/ledger-store.js": "fc8a46f925908764bac7a076cffa08053af4c1f93237bee408eb0ed1e61eeca2",
|
|
25
26
|
"server/ledger.js": "9bf8f132e08d636070d2ede568b2eaf75810be245c90ecd5979d2d4de69af934",
|
|
26
27
|
"server/persistence.js": "2e6b1d0b1c74babbd581780b028c2593256c5ec96c2d60f4c25159e3f54e4e3f",
|
|
28
|
+
"server/sentinel/artifact-sentinel.js": "93bae917af313c9247f8062af148e7c25ccfd16d0507fa85e755c64056715f8d",
|
|
29
|
+
"server/sentinel/promotion.js": "68ac5e54eb8c07d5c75ba0d2325d595b1583557840cc46322b2053dd9924a0be",
|
|
30
|
+
"server/sentinel/repository-sentinel.js": "722e973c8860cf281b29db8c4a22ec3cf473afcb088247916e8278d5e11638cf",
|
|
31
|
+
"server/sentinel/rules.js": "ab24435df4f6f5b10f8b721fde37fe7fa894453648f17f04923cab56020c459f",
|
|
32
|
+
"server/sentinel/tar-reader.js": "ded811eefebf3e4afa332d1bc0a48318cc22ad824222eb15b2dc08eb51048272",
|
|
33
|
+
"server/sentinel/violation.js": "70d44ae5eb43a560fada399b31eac1a2beccb66916340e1f3e9b549731776b38",
|
|
34
|
+
"server/sentinel/witness-sentinel.js": "f024dbe9fb505c7520ec466ed31bf9b3284c3c3066dbd287121d2e4397d3fb0a",
|
|
27
35
|
"server/storage.js": "7a7556a6d15d736f028698b6094072daa71eed2230427594edac9d29b531081a",
|
|
28
|
-
"server/transparency-log.js": "
|
|
36
|
+
"server/transparency-log.js": "7f238edc5cad4edc2c70a7b3e1977197cf2087e77789d181ffae0035f948391b",
|
|
29
37
|
"server/trust-bootstrap.js": "57a53a3ee9cd630ada2867e8b087a34b069ba26f86a696d3ead74888dd7e8d5f",
|
|
30
|
-
"server/witness.js": "
|
|
38
|
+
"server/witness-identity.js": "9dd5b6e63aefa00171a838d1cd27e3c09ddcb7d39512427ebe537763fe5fc42e",
|
|
39
|
+
"server/witness.js": "b1a8209d4dae4ffafc3ca4a158474b369250472ba80cef03bd92048282be91a4",
|
|
31
40
|
"shared/canonical.js": "476cee4b5c5702e8a2a198e79c2639cf8ff84000ffb92f68a04433ed2a2f5484",
|
|
32
41
|
"shared/certificates.js": "7844e71a38e1b1324586c7d054b2f5c15222b86446318d1e6dea9bab504a4a73",
|
|
42
|
+
"shared/invariants.js": "414a2aac034c9b28d0e32fc4afefb9b9ab723ed465b4aba1f8da98dab08363d4",
|
|
33
43
|
"shared/schema.js": "f89190990e3826e44c722d5e8f5b39fd59b4d3fb9ec047229930a17604e4646c",
|
|
34
44
|
"shared/terminating-kernel.js": "8e2aa68d47d6780e4a70822482e3e410924e9d1af843f301a487da0af37ed047"
|
|
35
45
|
}
|
|
@@ -34,6 +34,8 @@ 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
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");
|
|
37
39
|
const trust_bootstrap_1 = require("../server/trust-bootstrap");
|
|
38
40
|
const PORT = Number(process.env.PORT ?? 0);
|
|
39
41
|
const HOST = "127.0.0.1";
|
|
@@ -153,12 +155,35 @@ const server = (0, node_http_1.createServer)(async (req, res) => {
|
|
|
153
155
|
// ── GET /api/trust/evidence ──────────────────────────────────────────────
|
|
154
156
|
if (method === "GET" && url === "/api/trust/evidence") {
|
|
155
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() : [];
|
|
156
159
|
return sendJson(res, 200, {
|
|
157
160
|
witness_pubkey: witness_1.witnessLog.witnessPublicKey,
|
|
158
161
|
sth: witness_1.witnessLog.latestSTH() ?? null,
|
|
159
162
|
receipts,
|
|
163
|
+
...(rotations.length > 0 ? { rotations } : {}),
|
|
160
164
|
});
|
|
161
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
|
+
}
|
|
162
187
|
sendJson(res, 404, { error: "not_found", message: `${method} ${url} is not served by the demo control plane` });
|
|
163
188
|
}
|
|
164
189
|
catch (e) {
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
20
|
+
if (mod && mod.__esModule) return mod;
|
|
21
|
+
var result = {};
|
|
22
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
23
|
+
__setModuleDefault(result, mod);
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
/**
|
|
28
|
+
* magenta-sentinel — Sentinel Mesh CLI (foundation layer).
|
|
29
|
+
*
|
|
30
|
+
* magenta-canon sentinel repository [--root <dir>] [--json]
|
|
31
|
+
* magenta-canon sentinel artifact <tgz|package-dir> [--expected-version <v>] [--json]
|
|
32
|
+
* magenta-canon sentinel pack [--json] (npm pack, then evaluate the tarball)
|
|
33
|
+
*
|
|
34
|
+
* Exit codes: 0 eligible/diagnostic-only · 1 blocked · 2 requires-independent-review
|
|
35
|
+
* 3 usage / subject unreadable.
|
|
36
|
+
*
|
|
37
|
+
* Honesty: all records emitted here are UNSIGNED LOCAL DIAGNOSTICS. They are
|
|
38
|
+
* structured findings, not third-party-verifiable attestations.
|
|
39
|
+
*/
|
|
40
|
+
const node_child_process_1 = require("node:child_process");
|
|
41
|
+
const node_fs_1 = require("node:fs");
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
43
|
+
const repository_sentinel_1 = require("../server/sentinel/repository-sentinel");
|
|
44
|
+
const artifact_sentinel_1 = require("../server/sentinel/artifact-sentinel");
|
|
45
|
+
const witness_sentinel_1 = require("../server/sentinel/witness-sentinel");
|
|
46
|
+
const promotion_1 = require("../server/sentinel/promotion");
|
|
47
|
+
const invariants_1 = require("../shared/invariants");
|
|
48
|
+
const USAGE = `magenta-sentinel — deterministic invariant evaluation (Sentinel Mesh foundation)
|
|
49
|
+
|
|
50
|
+
Usage:
|
|
51
|
+
sentinel repository [--root <dir>] [--json]
|
|
52
|
+
sentinel artifact <tarball.tgz | package-dir> [--expected-version <v>] [--json]
|
|
53
|
+
sentinel pack [--json]
|
|
54
|
+
sentinel witness --ledger <file> [--keystore <file>] [--passphrase-env <VAR>]
|
|
55
|
+
[--anchor <pubkey-hex>] [--json]
|
|
56
|
+
sentinel invariants [--json]
|
|
57
|
+
|
|
58
|
+
Exit: 0 eligible/diagnostic-only · 1 blocked · 2 requires review · 3 usage/unreadable
|
|
59
|
+
`;
|
|
60
|
+
function flag(args, name) {
|
|
61
|
+
const i = args.indexOf(name);
|
|
62
|
+
if (i >= 0) {
|
|
63
|
+
args.splice(i, 1);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
function option(args, name) {
|
|
69
|
+
const i = args.indexOf(name);
|
|
70
|
+
if (i >= 0 && i + 1 < args.length) {
|
|
71
|
+
const v = args[i + 1];
|
|
72
|
+
args.splice(i, 2);
|
|
73
|
+
return v;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
function printHuman(kind, subject, result, promo) {
|
|
78
|
+
console.log(`magenta-sentinel ${kind}`);
|
|
79
|
+
console.log(` subject: ${subject}`);
|
|
80
|
+
console.log(` invariants evaluated: ${result.evaluated_invariants.length} — passed ${result.passed.length}, failed ${result.failed.length}`);
|
|
81
|
+
for (const f of result.findings) {
|
|
82
|
+
console.log(` [FAIL] ${f.invariant_id} (${f.severity}, ${f.disposition})`);
|
|
83
|
+
console.log(` observed: ${f.observed}`);
|
|
84
|
+
console.log(` paths: ${f.paths.join(", ") || "-"}`);
|
|
85
|
+
}
|
|
86
|
+
for (const v of result.violations) {
|
|
87
|
+
console.log(` evidence: ${v.invariant_id} record ${v.record_hash.slice(0, 16)}… (${v.assurance})`);
|
|
88
|
+
}
|
|
89
|
+
if (result.findings.length === 0)
|
|
90
|
+
console.log(" all evaluated invariants hold");
|
|
91
|
+
console.log(` PROMOTION: ${promo.decision.toUpperCase()}${promo.evidence_complete ? "" : " (evidence INCOMPLETE)"}`);
|
|
92
|
+
}
|
|
93
|
+
function exitFor(promo) {
|
|
94
|
+
switch (promo.decision) {
|
|
95
|
+
case "eligible":
|
|
96
|
+
case "diagnostic-only": return 0;
|
|
97
|
+
case "blocked": return 1;
|
|
98
|
+
case "requires-independent-review": return 2;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const argv = process.argv.slice(2);
|
|
102
|
+
const sub = argv.shift();
|
|
103
|
+
const json = flag(argv, "--json");
|
|
104
|
+
try {
|
|
105
|
+
if (sub === "repository") {
|
|
106
|
+
const root = option(argv, "--root") ?? process.cwd();
|
|
107
|
+
const res = (0, repository_sentinel_1.runRepositorySentinel)({ repoRoot: path.resolve(root) });
|
|
108
|
+
const promo = (0, promotion_1.decidePromotion)({ subject: `repository:${res.subject_identity}`, findings: res.findings, violations: res.violations });
|
|
109
|
+
if (json)
|
|
110
|
+
console.log(JSON.stringify({ result: res, promotion: promo }, null, 2));
|
|
111
|
+
else
|
|
112
|
+
printHuman("repository", res.subject_identity, res, promo);
|
|
113
|
+
process.exit(exitFor(promo));
|
|
114
|
+
}
|
|
115
|
+
else if (sub === "artifact") {
|
|
116
|
+
const target = argv.find((a) => !a.startsWith("-"));
|
|
117
|
+
if (!target) {
|
|
118
|
+
console.error("sentinel artifact: missing <tarball|dir>\n\n" + USAGE);
|
|
119
|
+
process.exit(3);
|
|
120
|
+
}
|
|
121
|
+
const expectedVersion = option(argv, "--expected-version");
|
|
122
|
+
const res = (0, artifact_sentinel_1.runArtifactSentinel)(path.resolve(target), { expectedVersion });
|
|
123
|
+
const promo = (0, promotion_1.decidePromotion)({ subject: `artifact:${res.subject_identity}`, findings: res.findings, violations: res.violations });
|
|
124
|
+
if (json)
|
|
125
|
+
console.log(JSON.stringify({ result: res, promotion: promo }, null, 2));
|
|
126
|
+
else
|
|
127
|
+
printHuman("artifact", `${res.artifact_name ?? "?"}@${res.artifact_version ?? "?"} (${res.subject_identity.slice(0, 19)}…, ${res.file_count} files)`, res, promo);
|
|
128
|
+
process.exit(exitFor(promo));
|
|
129
|
+
}
|
|
130
|
+
else if (sub === "pack") {
|
|
131
|
+
const out = (0, node_child_process_1.execFileSync)("npm", ["pack", "--silent"], { cwd: process.cwd(), encoding: "utf8", shell: process.platform === "win32" });
|
|
132
|
+
const tgz = out.trim().split(/\r?\n/).pop();
|
|
133
|
+
if (!(0, node_fs_1.existsSync)(tgz)) {
|
|
134
|
+
console.error(`sentinel pack: npm pack did not produce ${tgz}`);
|
|
135
|
+
process.exit(3);
|
|
136
|
+
}
|
|
137
|
+
const res = (0, artifact_sentinel_1.runArtifactSentinel)(path.resolve(tgz));
|
|
138
|
+
const promo = (0, promotion_1.decidePromotion)({ subject: `artifact:${res.subject_identity}`, findings: res.findings, violations: res.violations });
|
|
139
|
+
if (json)
|
|
140
|
+
console.log(JSON.stringify({ result: res, promotion: promo, tarball: tgz }, null, 2));
|
|
141
|
+
else {
|
|
142
|
+
console.log(` packed: ${tgz}`);
|
|
143
|
+
printHuman("artifact", `${res.artifact_name}@${res.artifact_version}`, res, promo);
|
|
144
|
+
}
|
|
145
|
+
process.exit(exitFor(promo));
|
|
146
|
+
}
|
|
147
|
+
else if (sub === "witness") {
|
|
148
|
+
const ledger = option(argv, "--ledger");
|
|
149
|
+
if (!ledger) {
|
|
150
|
+
console.error("sentinel witness: missing --ledger <file>\n\n" + USAGE);
|
|
151
|
+
process.exit(3);
|
|
152
|
+
}
|
|
153
|
+
const keystore = option(argv, "--keystore");
|
|
154
|
+
const res = (0, witness_sentinel_1.runWitnessSentinel)({
|
|
155
|
+
ledgerPath: path.resolve(ledger),
|
|
156
|
+
keystorePath: keystore ? path.resolve(keystore) : undefined,
|
|
157
|
+
passphraseEnv: option(argv, "--passphrase-env"),
|
|
158
|
+
anchorPubkey: option(argv, "--anchor"),
|
|
159
|
+
});
|
|
160
|
+
const promo = (0, promotion_1.decidePromotion)({ subject: `witness:${res.subject_identity}`, findings: res.findings, violations: res.violations });
|
|
161
|
+
if (json)
|
|
162
|
+
console.log(JSON.stringify({ result: res, promotion: promo }, null, 2));
|
|
163
|
+
else {
|
|
164
|
+
printHuman("witness", `${res.subject_identity.slice(0, 27)}… (${res.ledger_sth_count} STH, ${res.ledger_rotation_count} rotation(s), tip ${res.tip_fingerprint ?? "-"})`, res, promo);
|
|
165
|
+
for (const n of res.not_evaluated)
|
|
166
|
+
console.log(` [NOT EVALUATED] ${n.invariant_id}: ${n.reason}`);
|
|
167
|
+
if (res.incomplete_tail_ignored)
|
|
168
|
+
console.log(" note: incomplete trailing ledger line ignored (crash-tail semantics)");
|
|
169
|
+
}
|
|
170
|
+
process.exit(exitFor(promo));
|
|
171
|
+
}
|
|
172
|
+
else if (sub === "invariants") {
|
|
173
|
+
if (json)
|
|
174
|
+
console.log(JSON.stringify(invariants_1.INVARIANTS, null, 2));
|
|
175
|
+
else
|
|
176
|
+
for (const i of invariants_1.INVARIANTS)
|
|
177
|
+
console.log(`${i.id} v${i.version} ${i.status.padEnd(9)} ${i.severity.padEnd(8)} ${i.disposition.padEnd(26)} ${i.name}`);
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
process.stdout.write(USAGE);
|
|
182
|
+
process.exit(sub === undefined || sub === "--help" || sub === "-h" ? 0 : 3);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
if (e instanceof artifact_sentinel_1.TarError) {
|
|
187
|
+
console.error(`sentinel: artifact unreadable (refusing to guess): ${e.message}`);
|
|
188
|
+
process.exit(3);
|
|
189
|
+
}
|
|
190
|
+
console.error(`sentinel: ${e.message}`);
|
|
191
|
+
process.exit(3);
|
|
192
|
+
}
|
|
@@ -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
|
}
|
|
@@ -86,6 +86,8 @@ const node_crypto_1 = require("node:crypto");
|
|
|
86
86
|
const canonical_1 = require("../shared/canonical");
|
|
87
87
|
const crypto_1 = require("./crypto");
|
|
88
88
|
const transparency_log_1 = require("./transparency-log");
|
|
89
|
+
const witness_identity_1 = require("./witness-identity");
|
|
90
|
+
const canonical_2 = require("../shared/canonical");
|
|
89
91
|
const FORMAT_VERSION = 1;
|
|
90
92
|
const MAX_LINE = 1024 * 1024; // 1 MiB per record — bounded, fail-closed
|
|
91
93
|
class LedgerCorruptionError extends Error {
|
|
@@ -121,6 +123,10 @@ class FileLedgerStore {
|
|
|
121
123
|
lastHash;
|
|
122
124
|
sths = [];
|
|
123
125
|
sthBySize = new Map(); // tree_size -> root_hash (one root per size)
|
|
126
|
+
rotations = [];
|
|
127
|
+
activeWitnessKey; // anchored by first STH; advanced by rotations
|
|
128
|
+
activeWitnessVersion_ = 1;
|
|
129
|
+
lastRotationHash = canonical_2.GENESIS_HASH;
|
|
124
130
|
leaves = []; // replay-validation view only
|
|
125
131
|
fd;
|
|
126
132
|
lockPath;
|
|
@@ -227,6 +233,9 @@ class FileLedgerStore {
|
|
|
227
233
|
if (typeof sth.tree_size !== "number" || sth.tree_size < 0) {
|
|
228
234
|
throw new Error("file-ledger: STH tree_size malformed — refused");
|
|
229
235
|
}
|
|
236
|
+
if (this.activeWitnessKey !== undefined && sth.witness_pubkey !== this.activeWitnessKey) {
|
|
237
|
+
throw new Error("file-ledger: STH signed by a key that is not the active witness epoch — unauthorized substitution refused (commit a rotation record first)");
|
|
238
|
+
}
|
|
230
239
|
const existing = this.sthBySize.get(sth.tree_size);
|
|
231
240
|
if (existing !== undefined && existing !== sth.root_hash) {
|
|
232
241
|
throw new Error(`file-ledger: conflicting STH at tree_size ${sth.tree_size} — equivocation refused`);
|
|
@@ -235,6 +244,57 @@ class FileLedgerStore {
|
|
|
235
244
|
this.writeRecord({ v: FORMAT_VERSION, t: "sth", payload, payload_hash: (0, canonical_1.canonicalHash)(payload) });
|
|
236
245
|
this.sths.push(sth);
|
|
237
246
|
this.sthBySize.set(sth.tree_size, sth.root_hash);
|
|
247
|
+
if (this.activeWitnessKey === undefined)
|
|
248
|
+
this.activeWitnessKey = sth.witness_pubkey;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Durably commit a rotation continuity record. The record becomes part of
|
|
252
|
+
* the SAME append-only evidence ledger (single authority). Validation here
|
|
253
|
+
* is the write-side gate; replay re-validates the full chain.
|
|
254
|
+
*/
|
|
255
|
+
appendRotation(record) {
|
|
256
|
+
if (this.activeWitnessKey === undefined) {
|
|
257
|
+
throw new Error("file-ledger: cannot rotate before any STH establishes the witness anchor");
|
|
258
|
+
}
|
|
259
|
+
const v = (0, witness_identity_1.validateRotationRecord)(record, {
|
|
260
|
+
activePubkey: this.activeWitnessKey,
|
|
261
|
+
activeVersion: this.activeWitnessVersion_,
|
|
262
|
+
previousRotationHash: this.lastRotationHash,
|
|
263
|
+
identityId: record.identity_id, // identity binding is enforced by the keystore + chain hashes
|
|
264
|
+
});
|
|
265
|
+
if (!v.valid)
|
|
266
|
+
throw new Error(`file-ledger: rotation refused — ${v.reason}`);
|
|
267
|
+
if (record.effective_tree_size !== this.leaves.length) {
|
|
268
|
+
throw new Error(`file-ledger: rotation boundary ${record.effective_tree_size} does not match the current ledger size ${this.leaves.length}`);
|
|
269
|
+
}
|
|
270
|
+
const latest = this.latestSth();
|
|
271
|
+
if (latest && record.last_old_sth_root !== latest.root_hash) {
|
|
272
|
+
throw new Error("file-ledger: rotation does not preserve the final old-key checkpoint root");
|
|
273
|
+
}
|
|
274
|
+
const payload = { ...record };
|
|
275
|
+
this.writeRecord({ v: FORMAT_VERSION, t: "rotation", payload, payload_hash: (0, canonical_1.canonicalHash)(payload) });
|
|
276
|
+
this.rotations.push(record);
|
|
277
|
+
this.activeWitnessKey = record.new_pubkey;
|
|
278
|
+
this.activeWitnessVersion_ = record.new_version;
|
|
279
|
+
this.lastRotationHash = record.record_hash;
|
|
280
|
+
}
|
|
281
|
+
allRotations() {
|
|
282
|
+
return this.rotations.slice();
|
|
283
|
+
}
|
|
284
|
+
activeWitnessPubkey() {
|
|
285
|
+
return this.activeWitnessKey;
|
|
286
|
+
}
|
|
287
|
+
activeWitnessVersion() {
|
|
288
|
+
return this.activeWitnessVersion_;
|
|
289
|
+
}
|
|
290
|
+
/** The validated epoch table (anchor + rotation boundaries). The single
|
|
291
|
+
* source of "which key is authorized for which tree size" at this node. */
|
|
292
|
+
witnessEpochs() {
|
|
293
|
+
if (this.sths.length === 0)
|
|
294
|
+
return undefined;
|
|
295
|
+
const anchor = this.sths[0].witness_pubkey;
|
|
296
|
+
const w = (0, witness_identity_1.walkRotationChain)(anchor, this.rotations);
|
|
297
|
+
return w.valid ? w.epochs : undefined; // replay already validated → valid by construction
|
|
238
298
|
}
|
|
239
299
|
// ── reads (same copy semantics as the memory store) ───────────────────────
|
|
240
300
|
async getReceiptsNewestFirst(limit) {
|
|
@@ -373,6 +433,30 @@ class FileLedgerStore {
|
|
|
373
433
|
this.leaves.push((0, transparency_log_1.hashLeaf)(rc.receipt_hash));
|
|
374
434
|
continue;
|
|
375
435
|
}
|
|
436
|
+
if (rec.t === "rotation") {
|
|
437
|
+
const rot = rec.payload;
|
|
438
|
+
if (witnessKey === undefined)
|
|
439
|
+
throw new LedgerCorruptionError(recNo, "rotation before any STH anchor");
|
|
440
|
+
const rv = (0, witness_identity_1.validateRotationRecord)(rot, {
|
|
441
|
+
activePubkey: witnessKey,
|
|
442
|
+
activeVersion: this.activeWitnessVersion_,
|
|
443
|
+
previousRotationHash: this.lastRotationHash,
|
|
444
|
+
identityId: rot.identity_id,
|
|
445
|
+
});
|
|
446
|
+
if (!rv.valid)
|
|
447
|
+
throw new LedgerCorruptionError(recNo, `rotation record invalid: ${rv.reason}`);
|
|
448
|
+
if (rot.effective_tree_size !== this.leaves.length)
|
|
449
|
+
throw new LedgerCorruptionError(recNo, "rotation boundary does not match ledger position");
|
|
450
|
+
const latestAtRotation = this.sths[this.sths.length - 1];
|
|
451
|
+
if (latestAtRotation && rot.last_old_sth_root !== latestAtRotation.root_hash) {
|
|
452
|
+
throw new LedgerCorruptionError(recNo, "rotation does not preserve the final old-key checkpoint");
|
|
453
|
+
}
|
|
454
|
+
this.rotations.push(rot);
|
|
455
|
+
witnessKey = rot.new_pubkey;
|
|
456
|
+
this.activeWitnessVersion_ = rot.new_version;
|
|
457
|
+
this.lastRotationHash = rot.record_hash;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
376
460
|
if (rec.t === "sth") {
|
|
377
461
|
const sth = rec.payload;
|
|
378
462
|
if (!(0, transparency_log_1.verifySTH)(sth))
|
|
@@ -380,7 +464,7 @@ class FileLedgerStore {
|
|
|
380
464
|
if (witnessKey === undefined)
|
|
381
465
|
witnessKey = sth.witness_pubkey;
|
|
382
466
|
else if (sth.witness_pubkey !== witnessKey)
|
|
383
|
-
throw new LedgerCorruptionError(recNo, "
|
|
467
|
+
throw new LedgerCorruptionError(recNo, "STH signed by a key outside the authorized rotation chain (silent substitution)");
|
|
384
468
|
if (sth.tree_size > this.leaves.length)
|
|
385
469
|
throw new LedgerCorruptionError(recNo, `STH tree_size ${sth.tree_size} exceeds receipts seen (${this.leaves.length})`);
|
|
386
470
|
const existing = this.sthBySize.get(sth.tree_size);
|
|
@@ -397,6 +481,7 @@ class FileLedgerStore {
|
|
|
397
481
|
}
|
|
398
482
|
if (lines.length > 0 && !headerSeen)
|
|
399
483
|
throw new LedgerCorruptionError(1, "missing header record");
|
|
484
|
+
this.activeWitnessKey = witnessKey;
|
|
400
485
|
}
|
|
401
486
|
quarantineTail(tail, offset) {
|
|
402
487
|
const qPath = `${this.filePath}.tail-quarantine-${Date.now()}`;
|