verifyhash 0.1.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/LICENSE +201 -0
- package/README.md +883 -0
- package/cli/abi/ContributionRegistry.json +881 -0
- package/cli/agent.js +2173 -0
- package/cli/anchor-artifact.js +853 -0
- package/cli/anchor.js +400 -0
- package/cli/claim.js +881 -0
- package/cli/core/agent-commit.js +448 -0
- package/cli/core/agent-session.js +598 -0
- package/cli/core/anchor-binding.js +663 -0
- package/cli/core/attestation.js +580 -0
- package/cli/core/evidence-plans.js +495 -0
- package/cli/core/fixtures/evidence-plans/baseline.json +19 -0
- package/cli/core/fulfill-intake.js +1082 -0
- package/cli/core/go-live-preflight.js +481 -0
- package/cli/core/license.js +534 -0
- package/cli/core/manifest.js +243 -0
- package/cli/core/packetseal.js +591 -0
- package/cli/core/registryArtifact.js +49 -0
- package/cli/core/revocation.js +539 -0
- package/cli/core/rfc3161.js +389 -0
- package/cli/core/timestamp.js +482 -0
- package/cli/core/trust-asof.js +479 -0
- package/cli/dataset.js +2950 -0
- package/cli/evidence.js +2227 -0
- package/cli/fulfill-webhook-http.js +438 -0
- package/cli/git.js +220 -0
- package/cli/hash.js +550 -0
- package/cli/identity.js +1072 -0
- package/cli/journal-cli.js +1110 -0
- package/cli/journal-log.js +454 -0
- package/cli/journal.js +334 -0
- package/cli/lineage.js +447 -0
- package/cli/list.js +287 -0
- package/cli/parcel.js +1509 -0
- package/cli/proof.js +578 -0
- package/cli/prove.js +300 -0
- package/cli/receipt.js +631 -0
- package/cli/registry.js +331 -0
- package/cli/reputation.js +344 -0
- package/cli/revocation.js +495 -0
- package/cli/serve-verify-http.js +298 -0
- package/cli/serve-verify.js +333 -0
- package/cli/show.js +339 -0
- package/cli/verify.js +383 -0
- package/cli/vh.js +3927 -0
- package/docs/ADOPT.md +183 -0
- package/docs/ADOPTION.json +11 -0
- package/docs/AGENTTRACE.md +247 -0
- package/docs/ANCHORING.md +167 -0
- package/docs/AUDIT.md +55 -0
- package/docs/CONFORMANCE.md +107 -0
- package/docs/DATALEDGER.md +638 -0
- package/docs/DECIDE.md +47 -0
- package/docs/DECISIONS-PENDING.md +27 -0
- package/docs/DEPLOY-PUBLIC-SITE.md +301 -0
- package/docs/ENGINE-LEDGER.json +12 -0
- package/docs/EVIDENCE.md +519 -0
- package/docs/GO-LIVE.md +66 -0
- package/docs/IDENTITY.md +123 -0
- package/docs/INDEPENDENT-VERIFICATION.md +377 -0
- package/docs/INTEGRITY-JOURNAL.md +337 -0
- package/docs/KEY-LIFECYCLE.md +179 -0
- package/docs/LICENSING.md +46 -0
- package/docs/LINEAGE.md +307 -0
- package/docs/LOOP-AUDIT-2026-07-03.json +580 -0
- package/docs/LOOP-HARDENING-PLAN.md +44 -0
- package/docs/MERKLE-LEAVES.md +113 -0
- package/docs/METRICS.jsonl +31 -0
- package/docs/MORNING.md +204 -0
- package/docs/PILOT.md +444 -0
- package/docs/PROOFPARCEL.md +227 -0
- package/docs/PROOFS.md +262 -0
- package/docs/RECEIPTS.md +341 -0
- package/docs/REPUTATION.md +158 -0
- package/docs/SDK.md +301 -0
- package/docs/STRATEGY-ARCHIVE.md +5055 -0
- package/docs/SUPERVISOR-RUNBOOK.md +52 -0
- package/docs/TRUST-BOUNDARIES.md +335 -0
- package/docs/TRUSTLEDGER.md +1976 -0
- package/docs/USAGE-BUDGET.json +121 -0
- package/docs/VERIFY-SERVICE.md +168 -0
- package/index.js +160 -0
- package/package.json +41 -0
- package/trustledger/build-standalone.js +796 -0
- package/trustledger/cli.js +3179 -0
- package/trustledger/close.js +391 -0
- package/trustledger/corpus.js +159 -0
- package/trustledger/dist/BUILD-PROVENANCE.json +99 -0
- package/trustledger/dist/trustledger-standalone.html +6197 -0
- package/trustledger/dist/trustledger-standalone.html.sha256 +1 -0
- package/trustledger/door-core.js +442 -0
- package/trustledger/fixtures/bank.csv +7 -0
- package/trustledger/fixtures/bank.malformed.csv +3 -0
- package/trustledger/fixtures/bank.noalias.csv +5 -0
- package/trustledger/fixtures/bank.ofx +34 -0
- package/trustledger/fixtures/bank.real.csv +5 -0
- package/trustledger/fixtures/corpus/_shared/prior-close.json +22 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/inputs.json +14 -0
- package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/continuity-break--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/inputs.json +15 -0
- package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/inputs.json +16 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/meta.json +7 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/inputs.json +13 -0
- package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/meta.json +7 -0
- package/trustledger/fixtures/e2e/bank.aliased.csv +4 -0
- package/trustledger/fixtures/e2e/bank.csv +4 -0
- package/trustledger/fixtures/e2e/bank.nsf.csv +4 -0
- package/trustledger/fixtures/e2e/quickbooks.csv +6 -0
- package/trustledger/fixtures/e2e/quickbooks.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.csv +6 -0
- package/trustledger/fixtures/e2e/rentroll.nsf.csv +8 -0
- package/trustledger/fixtures/e2e/rentroll.short.csv +5 -0
- package/trustledger/fixtures/plans/baseline.json +25 -0
- package/trustledger/fixtures/plans/price-binding.example.json +27 -0
- package/trustledger/fixtures/policy/ambiguous-deposit-example.json +12 -0
- package/trustledger/fixtures/policy/baseline.json +19 -0
- package/trustledger/fixtures/policy/ca-example.json +12 -0
- package/trustledger/fixtures/policy/negative-tenant-ledger-example.json +12 -0
- package/trustledger/fixtures/policy/owner-overdraw-example.json +12 -0
- package/trustledger/fixtures/quickbooks.csv +7 -0
- package/trustledger/fixtures/quickbooks.real.csv +5 -0
- package/trustledger/fixtures/rentroll.csv +6 -0
- package/trustledger/fixtures/rentroll.real.csv +4 -0
- package/trustledger/ingest.js +1163 -0
- package/trustledger/lib/policy-bundled-loader.js +44 -0
- package/trustledger/lib/sha256-vendored.js +227 -0
- package/trustledger/license.js +563 -0
- package/trustledger/match.js +551 -0
- package/trustledger/plans.js +551 -0
- package/trustledger/policy.js +398 -0
- package/trustledger/public/index.html +512 -0
- package/trustledger/reconcile.js +1486 -0
- package/trustledger/report.js +887 -0
- package/trustledger/seal.js +854 -0
- package/trustledger/server.js +391 -0
- package/trustledger/valueproof.js +350 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// cli/serve-verify-http.js — the loopback-only HTTP transport for `vh serve-verify` (T-59.2).
|
|
4
|
+
//
|
|
5
|
+
// WHAT THIS IS
|
|
6
|
+
// A tiny, dependency-free (Node-core `http` ONLY) HTTP server that fronts the PURE, transport-agnostic
|
|
7
|
+
// verify core (`cli/serve-verify.js › verifyRequest`). It transports a request body IN over HTTP, hands
|
|
8
|
+
// the already-parsed object to `verifyRequest`, and maps the returned verdict to a CI-mappable HTTP
|
|
9
|
+
// status + JSON body OUT. It invents NO verify logic, NO crypto, NO verdict vocabulary — every ACCEPT/
|
|
10
|
+
// REJECT/ERROR answer is the core's, byte-for-byte in `detail`.
|
|
11
|
+
//
|
|
12
|
+
// VERIFY-ONLY / NO KEY / NO FILE WRITES
|
|
13
|
+
// This server VERIFIES; it never SIGNS. It holds NO private key (it never constructs a Wallet, never
|
|
14
|
+
// reads a key, never calls signMessage/signSealWith). It writes NOTHING to disk — the whole request path
|
|
15
|
+
// (readBody -> JSON.parse -> verifyRequest -> sendJson) is I/O-free except the network socket. A verify
|
|
16
|
+
// therefore leaves the filesystem untouched, and the process holds no secret at any point.
|
|
17
|
+
//
|
|
18
|
+
// LOOPBACK BY DEFAULT (a human deploy step to expose)
|
|
19
|
+
// The default bind host is `127.0.0.1` (loopback). A request arriving on a non-loopback interface is NOT
|
|
20
|
+
// served by the default bind — the socket simply is not listening there. Exposing this publicly (behind
|
|
21
|
+
// YOUR nginx/Cloudflare, on YOUR domain, with TLS) is an explicit HUMAN deploy step; it is NEVER
|
|
22
|
+
// auto-deployed and NEVER binds a public interface on its own. (An operator MAY pass `--host 0.0.0.0` to
|
|
23
|
+
// bind all interfaces — that is their explicit, deliberate choice, printed back with a warning.)
|
|
24
|
+
//
|
|
25
|
+
// STATUS MAPPING (CI-mappable — a build can gate on the code alone)
|
|
26
|
+
// POST /verify:
|
|
27
|
+
// verdict ACCEPTED -> 200 (the seal/container verified)
|
|
28
|
+
// verdict REJECTED -> 422 (a well-formed request that did NOT verify — Unprocessable Entity)
|
|
29
|
+
// verdict ERROR -> 400 (a malformed/unknown request the core evaluated to ERROR — the body is bad)
|
|
30
|
+
// A wire body larger than --max-body is refused with 413 (Payload Too Large) BEFORE it is fully buffered —
|
|
31
|
+
// a DISTINCT code from the 400 bucket, so a CI gate can tell "too big" apart from "malformed/unknown".
|
|
32
|
+
// GET /healthz -> 200 { ok:true }
|
|
33
|
+
// Anything else (wrong method / wrong path) -> 404 (or 405 for a known path, wrong method).
|
|
34
|
+
//
|
|
35
|
+
// FAIL CLOSED
|
|
36
|
+
// Invalid JSON, an oversized body, an unknown route, or ANY unexpected internal error becomes a clean
|
|
37
|
+
// named JSON error with a 4xx/5xx status — NEVER a stack trace, NEVER a silent 200, NEVER a crash of the
|
|
38
|
+
// process. A caller that cannot be verified is never treated as verified.
|
|
39
|
+
|
|
40
|
+
const http = require("http");
|
|
41
|
+
|
|
42
|
+
const serveVerify = require("./serve-verify");
|
|
43
|
+
|
|
44
|
+
// The default bind host is LOOPBACK: a non-loopback interface is NOT served unless the operator explicitly
|
|
45
|
+
// asks for a different --host. The default port is arbitrary-but-memorable and does NOT collide with the
|
|
46
|
+
// TrustLedger browser door (4173).
|
|
47
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
48
|
+
const DEFAULT_PORT = 4180;
|
|
49
|
+
|
|
50
|
+
// The wire-level body cap enforced AS bytes arrive, so a hostile/accidental giant body is refused (413)
|
|
51
|
+
// before it is ever fully buffered. Defaults to the CORE's own byte cap so the two layers agree; an
|
|
52
|
+
// operator may lower it via --max-body. (The core ALSO re-checks the parsed size — belt and braces.)
|
|
53
|
+
const DEFAULT_MAX_BODY_BYTES = serveVerify.MAX_BODY_BYTES;
|
|
54
|
+
|
|
55
|
+
// The single verify route + the health route.
|
|
56
|
+
const VERIFY_PATH = "/verify";
|
|
57
|
+
const HEALTH_PATH = "/healthz";
|
|
58
|
+
|
|
59
|
+
// HTTP status codes we map onto. 422 (Unprocessable Entity) is the RIGHT code for "well-formed request the
|
|
60
|
+
// server understood, but the content did NOT verify" — distinct from 400 ("the request itself is bad").
|
|
61
|
+
const STATUS = Object.freeze({
|
|
62
|
+
OK: 200,
|
|
63
|
+
BAD_REQUEST: 400,
|
|
64
|
+
NOT_FOUND: 404,
|
|
65
|
+
METHOD_NOT_ALLOWED: 405,
|
|
66
|
+
UNPROCESSABLE: 422,
|
|
67
|
+
PAYLOAD_TOO_LARGE: 413,
|
|
68
|
+
INTERNAL: 500,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// The verbatim caveat block the CLI banner AND (a machine-readable copy) the /healthz body carry. These
|
|
72
|
+
// four lines are the acceptance-pinned posture: verify-only, loopback, P-3, human-deploy. Kept as an array
|
|
73
|
+
// of exact strings so a test can assert each line VERBATIM and the wording can never silently drift.
|
|
74
|
+
const CAVEATS = Object.freeze([
|
|
75
|
+
"verify-only: this server VERIFIES seals; it never signs, holds NO private key, and writes NO file.",
|
|
76
|
+
"loopback by default: it binds 127.0.0.1 — a non-loopback interface is NOT served unless you pass --host.",
|
|
77
|
+
"NOT a timestamp: a verified seal proves set-membership / a signer vouched, NOT \"sealed since date T\" (P-3).",
|
|
78
|
+
"exposing it publicly is a HUMAN deploy step (your nginx/Cloudflare, your domain, TLS) — never auto-deployed.",
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------------------------------
|
|
82
|
+
// Body reader with a HARD size cap enforced as bytes arrive (never buffers past the cap).
|
|
83
|
+
// ---------------------------------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
class HttpError extends Error {
|
|
86
|
+
constructor(status, code, message) {
|
|
87
|
+
super(message);
|
|
88
|
+
this.name = "HttpError";
|
|
89
|
+
this.status = status;
|
|
90
|
+
this.code = code;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A hard ceiling on how many bytes we will DRAIN past the cap before forcibly tearing the socket down. Once
|
|
95
|
+
// the body exceeds `maxBytes` we STOP buffering (memory stays bounded at the cap) but keep DRAINING incoming
|
|
96
|
+
// chunks so we can still send a clean 400 back to a merely-too-big (not hostile) client. If a truly hostile
|
|
97
|
+
// client keeps streaming far beyond the cap, we destroy the socket rather than drain forever.
|
|
98
|
+
const OVERSIZE_DRAIN_LIMIT = 4 * 1024 * 1024;
|
|
99
|
+
|
|
100
|
+
// Read the request body with a HARD byte cap enforced AS chunks arrive, so an oversized body never pins
|
|
101
|
+
// unbounded memory. Resolves to `{ body }` on a body within the cap, or `{ tooLarge:true }` once the cap is
|
|
102
|
+
// exceeded (the caller maps that to a clean 413 — never a silent ACCEPT). Rejects with an HttpError only on
|
|
103
|
+
// a transport error or a hostile client that keeps streaming far past the cap (socket torn down, also 413).
|
|
104
|
+
function readBody(req, maxBytes) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const chunks = [];
|
|
107
|
+
let size = 0;
|
|
108
|
+
let over = false;
|
|
109
|
+
let done = false;
|
|
110
|
+
const finish = (fn, arg) => {
|
|
111
|
+
if (done) return;
|
|
112
|
+
done = true;
|
|
113
|
+
fn(arg);
|
|
114
|
+
};
|
|
115
|
+
req.on("data", (chunk) => {
|
|
116
|
+
size += chunk.length;
|
|
117
|
+
if (!over && size > maxBytes) {
|
|
118
|
+
// Cap exceeded: stop buffering (drop what we have so memory can't grow past the cap) and mark the
|
|
119
|
+
// request too-large. We keep draining to `end` so a clean 413 reaches a merely-oversized client.
|
|
120
|
+
over = true;
|
|
121
|
+
chunks.length = 0;
|
|
122
|
+
}
|
|
123
|
+
if (over) {
|
|
124
|
+
if (size > maxBytes + OVERSIZE_DRAIN_LIMIT) {
|
|
125
|
+
// A hostile client streaming far past the cap: tear the socket down rather than drain forever.
|
|
126
|
+
req.destroy();
|
|
127
|
+
finish(reject, new HttpError(STATUS.PAYLOAD_TOO_LARGE, "payload_too_large", `request body exceeds the ${maxBytes}-byte limit`));
|
|
128
|
+
}
|
|
129
|
+
return; // drop the chunk; do not buffer
|
|
130
|
+
}
|
|
131
|
+
chunks.push(chunk);
|
|
132
|
+
});
|
|
133
|
+
req.on("end", () =>
|
|
134
|
+
finish(resolve, over ? { tooLarge: true, maxBytes } : { body: Buffer.concat(chunks).toString("utf8") })
|
|
135
|
+
);
|
|
136
|
+
req.on("error", (e) => finish(reject, new HttpError(STATUS.BAD_REQUEST, "request_error", e.message)));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Write a JSON response with a stable shape. The body is ALWAYS JSON (never an HTML page or a stack trace),
|
|
141
|
+
// so a programmatic CI client can always parse it. `nosniff` keeps it from being reinterpreted.
|
|
142
|
+
function sendJson(res, status, obj) {
|
|
143
|
+
const body = JSON.stringify(obj);
|
|
144
|
+
res.writeHead(status, {
|
|
145
|
+
"content-type": "application/json; charset=utf-8",
|
|
146
|
+
"content-length": Buffer.byteLength(body),
|
|
147
|
+
"x-content-type-options": "nosniff",
|
|
148
|
+
});
|
|
149
|
+
res.end(body);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Send a NAMED JSON error — never a stack trace. An HttpError carries its own status + code; anything else
|
|
153
|
+
// is a defensive 500 with a generic message (its .message is NOT leaked, since it could be unexpected).
|
|
154
|
+
function sendError(res, err) {
|
|
155
|
+
if (err instanceof HttpError) {
|
|
156
|
+
sendJson(res, err.status, { error: err.code, message: err.message });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
sendJson(res, STATUS.INTERNAL, { error: "internal_error", message: "an internal error occurred" });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Map the core verdict envelope to a CI-mappable HTTP status. ACCEPTED->200, REJECTED->422, ERROR->400.
|
|
163
|
+
// Anything unexpected (should be impossible — verifyRequest returns only those three) is a defensive 500.
|
|
164
|
+
function statusForVerdict(verdict) {
|
|
165
|
+
switch (verdict) {
|
|
166
|
+
case serveVerify.VERDICT.ACCEPTED:
|
|
167
|
+
return STATUS.OK;
|
|
168
|
+
case serveVerify.VERDICT.REJECTED:
|
|
169
|
+
return STATUS.UNPROCESSABLE;
|
|
170
|
+
case serveVerify.VERDICT.ERROR:
|
|
171
|
+
return STATUS.BAD_REQUEST;
|
|
172
|
+
default:
|
|
173
|
+
return STATUS.INTERNAL;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------------------------------
|
|
178
|
+
// The request handler. Routes on method + path; delegates ALL verify logic to the pure core.
|
|
179
|
+
// ---------------------------------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
function makeHandler(opts = {}) {
|
|
182
|
+
const maxBytes =
|
|
183
|
+
opts.maxBodyBytes != null && Number.isFinite(opts.maxBodyBytes)
|
|
184
|
+
? opts.maxBodyBytes
|
|
185
|
+
: DEFAULT_MAX_BODY_BYTES;
|
|
186
|
+
|
|
187
|
+
return function handler(req, res) {
|
|
188
|
+
// Route on the PATH only (ignore any query string).
|
|
189
|
+
const url = req.url || "/";
|
|
190
|
+
const pathOnly = url.split("?")[0];
|
|
191
|
+
const method = req.method || "GET";
|
|
192
|
+
|
|
193
|
+
// GET /healthz -> 200 { ok:true } (+ the machine-readable service metadata + caveats). A liveness/
|
|
194
|
+
// readiness probe (k8s, a CI healthcheck) hits this; it holds no key and touches nothing.
|
|
195
|
+
if (pathOnly === HEALTH_PATH) {
|
|
196
|
+
if (method !== "GET") {
|
|
197
|
+
sendError(
|
|
198
|
+
res,
|
|
199
|
+
new HttpError(STATUS.METHOD_NOT_ALLOWED, "method_not_allowed", `${HEALTH_PATH} accepts only GET`)
|
|
200
|
+
);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
sendJson(res, STATUS.OK, {
|
|
204
|
+
ok: true,
|
|
205
|
+
service: serveVerify.SERVICE_NAME,
|
|
206
|
+
schema: serveVerify.VERIFY_REQUEST_SCHEMA,
|
|
207
|
+
verifyOnly: true,
|
|
208
|
+
holdsKey: false,
|
|
209
|
+
writesFiles: false,
|
|
210
|
+
caveats: CAVEATS,
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// POST /verify -> run the pure core, map the verdict to a status.
|
|
216
|
+
if (pathOnly === VERIFY_PATH) {
|
|
217
|
+
if (method !== "POST") {
|
|
218
|
+
sendError(
|
|
219
|
+
res,
|
|
220
|
+
new HttpError(STATUS.METHOD_NOT_ALLOWED, "method_not_allowed", `${VERIFY_PATH} accepts only POST`)
|
|
221
|
+
);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
readBody(req, maxBytes)
|
|
225
|
+
.then((raw) => {
|
|
226
|
+
// An oversized body is a clean 413 (Payload Too Large) — a distinct, CI-gateable code, never a
|
|
227
|
+
// crash, never an ACCEPT. 413 (not 400) is the RIGHT status: the request was well-formed at the
|
|
228
|
+
// HTTP layer but its body exceeded the operator's --max-body cap.
|
|
229
|
+
if (raw.tooLarge) {
|
|
230
|
+
throw new HttpError(
|
|
231
|
+
STATUS.PAYLOAD_TOO_LARGE,
|
|
232
|
+
"payload_too_large",
|
|
233
|
+
`request body exceeds the ${raw.maxBytes}-byte limit`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
let body;
|
|
237
|
+
try {
|
|
238
|
+
body = JSON.parse(raw.body);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
// Malformed JSON is a clean 400 named error — never a crash, never a silent ACCEPT. (The core
|
|
241
|
+
// never sees it; a non-parseable body is a request-layer fault, mapped like an ERROR verdict.)
|
|
242
|
+
throw new HttpError(STATUS.BAD_REQUEST, "invalid_json", `request body is not valid JSON: ${e.message}`);
|
|
243
|
+
}
|
|
244
|
+
// The pure core does ALL verify work: it NEVER throws, holds no key, touches no fs/net. We only
|
|
245
|
+
// map its top-level verdict to a status and send its envelope back VERBATIM.
|
|
246
|
+
const verdict = serveVerify.verifyRequest(body);
|
|
247
|
+
sendJson(res, statusForVerdict(verdict.verdict), verdict);
|
|
248
|
+
})
|
|
249
|
+
.catch((err) => sendError(res, err));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Any other path/method: a named 404 (JSON, never an HTML error page).
|
|
254
|
+
sendError(res, new HttpError(STATUS.NOT_FOUND, "not_found", `no route for ${method} ${pathOnly}`));
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Build (but do NOT listen on) an http.Server. Keeping creation and listening separate lets a test bind an
|
|
259
|
+
// EPHEMERAL port (0) on 127.0.0.1 and close cleanly, and lets a real deploy choose its own port/host.
|
|
260
|
+
function createServer(opts = {}) {
|
|
261
|
+
return http.createServer(makeHandler(opts));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// The verbatim CLI banner printed when the server starts. LEADS with the verify-only + loopback + P-3 +
|
|
265
|
+
// human-deploy caveats (each line VERBATIM from CAVEATS) so an operator sees the posture before use.
|
|
266
|
+
function banner(url, host) {
|
|
267
|
+
const browseHint =
|
|
268
|
+
host === "0.0.0.0" || host === "::"
|
|
269
|
+
? ` (${host} binds ALL interfaces — you chose to expose it; browse via your machine's own address.)\n`
|
|
270
|
+
: "";
|
|
271
|
+
return (
|
|
272
|
+
`vh serve-verify listening on ${url}\n` +
|
|
273
|
+
browseHint +
|
|
274
|
+
` POST ${VERIFY_PATH} -> JSON verdict (200 ACCEPTED / 422 REJECTED / 400 bad request / 413 too large)\n` +
|
|
275
|
+
` GET ${HEALTH_PATH} -> { ok:true }\n` +
|
|
276
|
+
CAVEATS.map((c) => ` - ${c}`).join("\n") +
|
|
277
|
+
"\n" +
|
|
278
|
+
" Press Ctrl-C to stop.\n"
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = {
|
|
283
|
+
createServer,
|
|
284
|
+
makeHandler,
|
|
285
|
+
banner,
|
|
286
|
+
readBody,
|
|
287
|
+
sendJson,
|
|
288
|
+
sendError,
|
|
289
|
+
statusForVerdict,
|
|
290
|
+
HttpError,
|
|
291
|
+
CAVEATS,
|
|
292
|
+
DEFAULT_HOST,
|
|
293
|
+
DEFAULT_PORT,
|
|
294
|
+
DEFAULT_MAX_BODY_BYTES,
|
|
295
|
+
VERIFY_PATH,
|
|
296
|
+
HEALTH_PATH,
|
|
297
|
+
STATUS,
|
|
298
|
+
};
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// cli/serve-verify.js — the PURE, transport-agnostic verify CORE for the `vh serve-verify` service (T-59.1).
|
|
4
|
+
//
|
|
5
|
+
// WHAT THIS IS
|
|
6
|
+
// `verifyRequest(body)` takes a single, already-parsed request OBJECT (whatever a transport — an HTTP
|
|
7
|
+
// handler, a queue worker, a test — hands it) and returns a stable, versioned verdict OBJECT. It NEVER
|
|
8
|
+
// throws, NEVER touches the network, NEVER touches the filesystem, NEVER signs, and holds NO key. It is
|
|
9
|
+
// the composition layer that lets a CI pipeline / another microservice send "here is a seal (or a signed
|
|
10
|
+
// container), tell me ACCEPT/REJECT" over ANY transport, by dispatching on the request's `kind` to the
|
|
11
|
+
// already-green, already-tested SDK verify cores:
|
|
12
|
+
//
|
|
13
|
+
// kind "verify-seal" -> evidence.verifySeal(seal, entries) (UNSIGNED tamper-evidence)
|
|
14
|
+
// kind "verify-signed-seal" -> evidence.verifySignedSeal({container, ...}) (SIGNED / vendor-pinned)
|
|
15
|
+
//
|
|
16
|
+
// It re-uses the EXISTING verdict field names VERBATIM (it spreads the core verdict into `verdict.detail`
|
|
17
|
+
// unchanged) so a downstream consumer depends on ONE stable shape across the CLI, the SDK, and this
|
|
18
|
+
// service. It adds only a THIN, versioned request-level envelope (`schema`, `service`, `outcome`) around
|
|
19
|
+
// that core verdict — it invents NO new crypto, NO new verify logic, NO new verdict vocabulary.
|
|
20
|
+
//
|
|
21
|
+
// PURITY (asserted by the tests — a grep proves NONE of these appear in this module)
|
|
22
|
+
// NO `require("http")` / `require("https")` / `require("net")` / `require("fs")` / `require("dns")`.
|
|
23
|
+
// NO key material: it never calls `Wallet.createRandom`, never reads a private key, never signs. The
|
|
24
|
+
// ONLY dependency is `./evidence` (the SDK cores), and the ONLY core functions it calls are the PURE,
|
|
25
|
+
// filesystem-free verify/parse/build helpers (verifySeal / verifySignedSeal / readSeal /
|
|
26
|
+
// validateSignedSeal / buildSeal / serializeSeal) — none of which read a file, open a socket, or need a
|
|
27
|
+
// key. The function is therefore drivable with the filesystem unavailable/irrelevant.
|
|
28
|
+
//
|
|
29
|
+
// TRANSPORT-AGNOSTIC (why the ENVELOPE, not raw bytes)
|
|
30
|
+
// The transport (HTTP body, queue message, …) delivers TEXT/JSON, so `entries[].content` arrives as a
|
|
31
|
+
// STRING with an explicit `encoding` ("utf8" | "base64" | "hex"). This module decodes that back into the
|
|
32
|
+
// Buffer the byte-based cores require, RE-DERIVING the Merkle root from the caller-supplied bytes — never
|
|
33
|
+
// from the seal's own stored hashes. So a one-byte tamper in `content` still flips ACCEPTED -> REJECTED,
|
|
34
|
+
// exactly as the CLI does.
|
|
35
|
+
//
|
|
36
|
+
// FAIL CLOSED (never a silent/false ACCEPT)
|
|
37
|
+
// Every malformed body, oversized body, unknown `kind`, undecodable entry, or unexpected internal error
|
|
38
|
+
// maps to `verdict:"ERROR"` with a machine-readable `code` and a human `message` — NEVER `"ACCEPTED"`,
|
|
39
|
+
// NEVER a thrown exception that could crash a server or be swallowed into a 200. An ERROR is the safe
|
|
40
|
+
// default: a caller that cannot be verified must not be treated as verified.
|
|
41
|
+
|
|
42
|
+
const evidence = require("./evidence");
|
|
43
|
+
|
|
44
|
+
// The versioned envelope contract. Bump SCHEMA only on a breaking change to the ENVELOPE fields (never for
|
|
45
|
+
// a change inside `detail`, which tracks the core verify verdicts' own shape).
|
|
46
|
+
const VERIFY_REQUEST_SCHEMA = "vh.verify-request/1";
|
|
47
|
+
const SERVICE_NAME = "vh-serve-verify";
|
|
48
|
+
|
|
49
|
+
// The request `kind`s this core dispatches on (the REQUEST kind — distinct from the seal/container `kind`
|
|
50
|
+
// values `evidence.SEAL_KIND` / `evidence.SIGNED_SEAL_KIND` those payloads carry INSIDE the request).
|
|
51
|
+
const KIND_VERIFY_SEAL = "verify-seal";
|
|
52
|
+
const KIND_VERIFY_SIGNED_SEAL = "verify-signed-seal";
|
|
53
|
+
const SUPPORTED_KINDS = Object.freeze([KIND_VERIFY_SEAL, KIND_VERIFY_SIGNED_SEAL]);
|
|
54
|
+
|
|
55
|
+
// The three top-level verdicts. ERROR is NOT a content verdict — it means "the request could not be
|
|
56
|
+
// evaluated", strictly separate from a content REJECTED (which IS an evaluated, negative answer).
|
|
57
|
+
const VERDICT = Object.freeze({ ACCEPTED: "ACCEPTED", REJECTED: "REJECTED", ERROR: "ERROR" });
|
|
58
|
+
|
|
59
|
+
// Machine-readable ERROR codes (stable; a caller can branch on these without parsing prose).
|
|
60
|
+
const ERR = Object.freeze({
|
|
61
|
+
NOT_OBJECT: "ERR_BODY_NOT_OBJECT",
|
|
62
|
+
TOO_LARGE: "ERR_BODY_TOO_LARGE",
|
|
63
|
+
UNKNOWN_KIND: "ERR_UNKNOWN_KIND",
|
|
64
|
+
MISSING_SEAL: "ERR_MISSING_SEAL",
|
|
65
|
+
BAD_SEAL: "ERR_BAD_SEAL",
|
|
66
|
+
BAD_ENTRIES: "ERR_BAD_ENTRIES",
|
|
67
|
+
MISSING_CONTAINER: "ERR_MISSING_CONTAINER",
|
|
68
|
+
BAD_CONTAINER: "ERR_BAD_CONTAINER",
|
|
69
|
+
BAD_EXPECTED_SIGNER: "ERR_BAD_EXPECTED_SIGNER",
|
|
70
|
+
INTERNAL: "ERR_INTERNAL",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// A generous upper bound on the request size (bytes of the estimated payload), so a hostile/accidental
|
|
74
|
+
// giant body is a clean ERROR rather than an OOM. The transport can (and should) also cap the wire size;
|
|
75
|
+
// this is the core's own belt-and-braces. 8 MiB comfortably fits a large seal + its inlined content while
|
|
76
|
+
// staying far below anything that would strain the process.
|
|
77
|
+
const MAX_BODY_BYTES = 8 * 1024 * 1024;
|
|
78
|
+
|
|
79
|
+
const SUPPORTED_ENTRY_ENCODINGS = Object.freeze(["utf8", "base64", "hex"]);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The stable, versioned ERROR verdict envelope. Never contains an ACCEPTED/REJECTED content answer — an
|
|
83
|
+
* ERROR means the request itself could not be evaluated. Pure; allocates a plain object only.
|
|
84
|
+
* @param {string} code one of ERR.*
|
|
85
|
+
* @param {string} message a human-readable, non-sensitive reason
|
|
86
|
+
* @returns {object}
|
|
87
|
+
*/
|
|
88
|
+
function errorVerdict(code, message) {
|
|
89
|
+
return {
|
|
90
|
+
schema: VERIFY_REQUEST_SCHEMA,
|
|
91
|
+
service: SERVICE_NAME,
|
|
92
|
+
verdict: VERDICT.ERROR,
|
|
93
|
+
code: String(code),
|
|
94
|
+
message: String(message),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The stable, versioned OK envelope wrapping a core verify verdict VERBATIM. `outcome` mirrors the core's
|
|
100
|
+
* own `verdict` ("ACCEPTED"|"REJECTED") so a transport can map it to a status code without reading `detail`;
|
|
101
|
+
* `detail` is the UNCHANGED core verdict object (its field names are the contract the CLI/SDK already ship).
|
|
102
|
+
* @param {string} kind the request kind that was dispatched
|
|
103
|
+
* @param {object} detail the core verdict from verifySeal / verifySignedSeal (spread unchanged)
|
|
104
|
+
* @returns {object}
|
|
105
|
+
*/
|
|
106
|
+
function okVerdict(kind, detail) {
|
|
107
|
+
// `detail.verdict` is the core's own field ("ACCEPTED"|"REJECTED"). We surface it at the top level as the
|
|
108
|
+
// envelope's `verdict` too, so the top-level answer and the detail's answer are ALWAYS the same string —
|
|
109
|
+
// a transport reads the top level; an auditor reads `detail`. We NEVER recompute or override it.
|
|
110
|
+
const outcome = detail.verdict;
|
|
111
|
+
return {
|
|
112
|
+
schema: VERIFY_REQUEST_SCHEMA,
|
|
113
|
+
service: SERVICE_NAME,
|
|
114
|
+
verdict: outcome, // ACCEPTED | REJECTED — copied from the core verdict, never re-derived
|
|
115
|
+
kind,
|
|
116
|
+
detail, // the EXISTING core verdict shape, byte-for-byte (field names unchanged)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Rough byte-size of an already-parsed body without re-serializing giant nested structures repeatedly. We
|
|
121
|
+
// only need an UPPER-bound guard, so a single JSON.stringify is acceptable and stays pure (string in memory,
|
|
122
|
+
// no I/O). Guarded in a try/catch: a body with a circular ref or a BigInt can't be stringified — that is
|
|
123
|
+
// itself a malformed request, reported as an oversized/invalid error rather than a throw.
|
|
124
|
+
function estimateBodyBytes(body) {
|
|
125
|
+
const json = JSON.stringify(body);
|
|
126
|
+
// JSON.stringify returns undefined for a value that serializes to nothing (e.g. a function); treat that as
|
|
127
|
+
// 0 here — the shape checks below will reject a non-object body anyway.
|
|
128
|
+
return json === undefined ? 0 : Buffer.byteLength(json, "utf8");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Decode one transport entry `{ relPath, content, encoding }` into the `{ relPath, bytes: Buffer }` shape
|
|
133
|
+
* the byte-based seal core consumes. PURE: string -> Buffer, no I/O. Throws a plain Error (caught by the
|
|
134
|
+
* caller and mapped to a clean ERROR verdict) on any malformed entry — NEVER partially accepts.
|
|
135
|
+
*/
|
|
136
|
+
function decodeEntry(entry, index) {
|
|
137
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
138
|
+
throw new Error(`entries[${index}] must be an object { relPath, content, encoding }`);
|
|
139
|
+
}
|
|
140
|
+
const { relPath, content } = entry;
|
|
141
|
+
if (typeof relPath !== "string" || relPath.length === 0) {
|
|
142
|
+
throw new Error(`entries[${index}].relPath must be a non-empty string`);
|
|
143
|
+
}
|
|
144
|
+
if (typeof content !== "string") {
|
|
145
|
+
throw new Error(`entries[${index}].content must be a string (the ${index}th entry's bytes, encoded)`);
|
|
146
|
+
}
|
|
147
|
+
const encoding = entry.encoding === undefined ? "utf8" : entry.encoding;
|
|
148
|
+
if (!SUPPORTED_ENTRY_ENCODINGS.includes(encoding)) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`entries[${index}].encoding must be one of ${SUPPORTED_ENTRY_ENCODINGS.join("|")} (got ${JSON.stringify(encoding)})`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
// For base64/hex, Buffer.from is lenient (it drops invalid chars) — so we round-trip and require the
|
|
154
|
+
// re-encoding to match, catching a corrupt/garbage payload instead of silently hashing truncated bytes.
|
|
155
|
+
const bytes = Buffer.from(content, encoding);
|
|
156
|
+
if (encoding === "base64" || encoding === "hex") {
|
|
157
|
+
const reEncoded = bytes.toString(encoding);
|
|
158
|
+
const normalizedInput = encoding === "hex" ? content.toLowerCase() : content;
|
|
159
|
+
const normalizedOut = encoding === "hex" ? reEncoded.toLowerCase() : reEncoded;
|
|
160
|
+
if (normalizedOut !== normalizedInput) {
|
|
161
|
+
throw new Error(`entries[${index}].content is not valid ${encoding}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { relPath, bytes };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Decode a transport `entries[]` array into the byte-based entry list. Throws on any malformed member. */
|
|
168
|
+
function decodeEntries(rawEntries) {
|
|
169
|
+
if (!Array.isArray(rawEntries)) {
|
|
170
|
+
throw new Error("`entries` must be an array of { relPath, content, encoding }");
|
|
171
|
+
}
|
|
172
|
+
return rawEntries.map((e, i) => decodeEntry(e, i));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* verifyRequest(body) — the PURE, transport-agnostic verify dispatcher.
|
|
177
|
+
*
|
|
178
|
+
* @param {object} body an already-parsed request object:
|
|
179
|
+
* UNSIGNED: { kind: "verify-seal", seal: <object|string>, entries: [{relPath, content, encoding}] }
|
|
180
|
+
* SIGNED: { kind: "verify-signed-seal", container: <object|string>,
|
|
181
|
+
* expectedSigner?: <0x-address>, entries?: [{relPath, content, encoding}] }
|
|
182
|
+
* (when `entries` is supplied on the SIGNED path, it is BOUND: the canonical seal bytes are
|
|
183
|
+
* recomputed from those entries and required byte-identical to the signed payload; a set that
|
|
184
|
+
* does NOT match the signed bytes is a clean REJECTED, never an ACCEPT.)
|
|
185
|
+
* @returns {object} a stable versioned envelope:
|
|
186
|
+
* OK: { schema, service, verdict:"ACCEPTED"|"REJECTED", kind, detail:<core verdict, fields unchanged> }
|
|
187
|
+
* ERROR: { schema, service, verdict:"ERROR", code, message }
|
|
188
|
+
*
|
|
189
|
+
* NEVER throws. NEVER returns a false ACCEPT for a malformed/oversized/unknown request.
|
|
190
|
+
*/
|
|
191
|
+
function verifyRequest(body) {
|
|
192
|
+
try {
|
|
193
|
+
// --- Envelope-level guards (fail closed) --------------------------------------------------------
|
|
194
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
195
|
+
return errorVerdict(ERR.NOT_OBJECT, "request body must be a JSON object");
|
|
196
|
+
}
|
|
197
|
+
let size;
|
|
198
|
+
try {
|
|
199
|
+
size = estimateBodyBytes(body);
|
|
200
|
+
} catch (_) {
|
|
201
|
+
// Un-stringifiable (circular ref / BigInt / etc.) — a malformed request, reported as too-large-ish.
|
|
202
|
+
return errorVerdict(ERR.TOO_LARGE, "request body could not be measured (not plain JSON)");
|
|
203
|
+
}
|
|
204
|
+
if (size > MAX_BODY_BYTES) {
|
|
205
|
+
return errorVerdict(
|
|
206
|
+
ERR.TOO_LARGE,
|
|
207
|
+
`request body is ${size} bytes; the limit is ${MAX_BODY_BYTES} bytes`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const kind = body.kind;
|
|
212
|
+
if (kind === KIND_VERIFY_SEAL) return verifySealRequest(body);
|
|
213
|
+
if (kind === KIND_VERIFY_SIGNED_SEAL) return verifySignedSealRequest(body);
|
|
214
|
+
return errorVerdict(
|
|
215
|
+
ERR.UNKNOWN_KIND,
|
|
216
|
+
`unknown request kind ${JSON.stringify(kind)}; expected one of ${SUPPORTED_KINDS.map((k) => JSON.stringify(k)).join(", ")}`
|
|
217
|
+
);
|
|
218
|
+
} catch (e) {
|
|
219
|
+
// Defense-in-depth: anything unexpected inside a handler becomes a clean ERROR, never a crash or a
|
|
220
|
+
// silent ACCEPT. The message is the error's own text (non-sensitive — no key/file/network here).
|
|
221
|
+
return errorVerdict(ERR.INTERNAL, e && e.message ? String(e.message) : "internal error");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Handle a { kind:"verify-seal", seal, entries } request. Pure — decodes entries, calls verifySeal. */
|
|
226
|
+
function verifySealRequest(body) {
|
|
227
|
+
if (body.seal === undefined || body.seal === null) {
|
|
228
|
+
return errorVerdict(ERR.MISSING_SEAL, "`seal` is required for kind \"verify-seal\"");
|
|
229
|
+
}
|
|
230
|
+
// Strictly parse+validate the seal FIRST (readSeal accepts a JSON string OR an object; it rejects a
|
|
231
|
+
// corrupt/foreign/wrong-kind seal before any byte work — never half-accepted).
|
|
232
|
+
let seal;
|
|
233
|
+
try {
|
|
234
|
+
seal = evidence.readSeal(body.seal);
|
|
235
|
+
} catch (e) {
|
|
236
|
+
return errorVerdict(ERR.BAD_SEAL, `invalid seal: ${e.message}`);
|
|
237
|
+
}
|
|
238
|
+
let entries;
|
|
239
|
+
try {
|
|
240
|
+
entries = decodeEntries(body.entries === undefined ? [] : body.entries);
|
|
241
|
+
} catch (e) {
|
|
242
|
+
return errorVerdict(ERR.BAD_ENTRIES, e.message);
|
|
243
|
+
}
|
|
244
|
+
// The authoritative, PURE verify — RE-DERIVES the root from the supplied bytes (a one-byte tamper flips it).
|
|
245
|
+
const detail = evidence.verifySeal(seal, entries);
|
|
246
|
+
return okVerdict(KIND_VERIFY_SEAL, detail);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Handle a { kind:"verify-signed-seal", container, expectedSigner?, entries? } request. Pure — validates
|
|
251
|
+
* the container, optionally recomputes the bound canonical bytes from `entries`, calls verifySignedSeal.
|
|
252
|
+
*/
|
|
253
|
+
function verifySignedSealRequest(body) {
|
|
254
|
+
if (body.container === undefined || body.container === null) {
|
|
255
|
+
return errorVerdict(ERR.MISSING_CONTAINER, "`container` is required for kind \"verify-signed-seal\"");
|
|
256
|
+
}
|
|
257
|
+
// Parse the container if it arrived as a JSON string; then STRICT-validate it (rejects a tampered/foreign
|
|
258
|
+
// container — but NOT a forged signature, which the verify core catches by recovering the signer).
|
|
259
|
+
let container = body.container;
|
|
260
|
+
if (typeof container === "string") {
|
|
261
|
+
try {
|
|
262
|
+
container = JSON.parse(container);
|
|
263
|
+
} catch (e) {
|
|
264
|
+
return errorVerdict(ERR.BAD_CONTAINER, `container is not valid JSON: ${e.message}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (container === null || typeof container !== "object" || Array.isArray(container)) {
|
|
268
|
+
return errorVerdict(ERR.BAD_CONTAINER, "`container` must be a signed-seal object or its JSON string");
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
evidence.validateSignedSeal(container);
|
|
272
|
+
} catch (e) {
|
|
273
|
+
return errorVerdict(ERR.BAD_CONTAINER, `invalid signed container: ${e.message}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// OPTIONAL pin: an expected signer address the recovered signer must equal. We pass it straight to the
|
|
277
|
+
// core (which normalizes/validates the address); a malformed address is a clean ERROR here rather than a
|
|
278
|
+
// throw from deep in the core.
|
|
279
|
+
let expectedSigner;
|
|
280
|
+
if (body.expectedSigner !== undefined && body.expectedSigner !== null) {
|
|
281
|
+
if (typeof body.expectedSigner !== "string") {
|
|
282
|
+
return errorVerdict(ERR.BAD_EXPECTED_SIGNER, "`expectedSigner` must be a 0x-address string");
|
|
283
|
+
}
|
|
284
|
+
expectedSigner = body.expectedSigner;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// OPTIONAL binding: when `entries` is supplied, recompute the canonical UNSIGNED seal bytes from THOSE
|
|
288
|
+
// entries (the SAME serializeSeal(buildSeal(...)) the seal path embeds) and require them byte-identical to
|
|
289
|
+
// the signed payload. So an `entries` set that does NOT match the signed bytes is a clean REJECTED — never
|
|
290
|
+
// an ACCEPT. Absent `entries` = binding NOT requested (Check runs signature+optional-pin only).
|
|
291
|
+
let expectedCanonical;
|
|
292
|
+
if (body.entries !== undefined && body.entries !== null) {
|
|
293
|
+
let entries;
|
|
294
|
+
try {
|
|
295
|
+
entries = decodeEntries(body.entries);
|
|
296
|
+
} catch (e) {
|
|
297
|
+
return errorVerdict(ERR.BAD_ENTRIES, e.message);
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
expectedCanonical = evidence.serializeSeal(evidence.buildSeal(entries));
|
|
301
|
+
} catch (e) {
|
|
302
|
+
// A set that can't even be sealed (e.g. duplicate relPaths) is a bad-entries request, not an ACCEPT.
|
|
303
|
+
return errorVerdict(ERR.BAD_ENTRIES, `could not seal supplied entries: ${e.message}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let detail;
|
|
308
|
+
try {
|
|
309
|
+
detail = evidence.verifySignedSeal({ container, expectedSigner, expectedCanonical });
|
|
310
|
+
} catch (e) {
|
|
311
|
+
// The core throws only on an unknown signature scheme (validateSignedSeal already rejects one, so this
|
|
312
|
+
// is defense-in-depth) or a malformed expectedSigner address. Either is a request error, never an ACCEPT.
|
|
313
|
+
return errorVerdict(ERR.BAD_EXPECTED_SIGNER, e.message);
|
|
314
|
+
}
|
|
315
|
+
return okVerdict(KIND_VERIFY_SIGNED_SEAL, detail);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
module.exports = {
|
|
319
|
+
verifyRequest,
|
|
320
|
+
// Constants a transport / test can depend on without hard-coding strings.
|
|
321
|
+
VERIFY_REQUEST_SCHEMA,
|
|
322
|
+
SERVICE_NAME,
|
|
323
|
+
KIND_VERIFY_SEAL,
|
|
324
|
+
KIND_VERIFY_SIGNED_SEAL,
|
|
325
|
+
SUPPORTED_KINDS,
|
|
326
|
+
VERDICT,
|
|
327
|
+
ERR,
|
|
328
|
+
MAX_BODY_BYTES,
|
|
329
|
+
SUPPORTED_ENTRY_ENCODINGS,
|
|
330
|
+
// Exposed for tests / advanced transports that pre-decode; NOT part of the request envelope contract.
|
|
331
|
+
decodeEntry,
|
|
332
|
+
decodeEntries,
|
|
333
|
+
};
|