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,580 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// cli/core/attestation.js — the GENERIC signed-attestation ENVELOPE engine for the product family.
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS EXISTS
|
|
6
|
+
// Every verifyhash provenance product (DataLedger, ProofParcel, AttestKit later) needs the SAME thing
|
|
7
|
+
// once it has a canonical UNSIGNED identity payload: a way to WRAP that payload in a detached
|
|
8
|
+
// signature WITHOUT editing it, then re-read and cryptographically verify the wrap. The container
|
|
9
|
+
// builder/reader, the supported `scheme` list, the signer-recovery, and the wrap-don't-edit invariant
|
|
10
|
+
// are IDENTICAL across products; only the container `kind` and the in-band trust `note` differ. This
|
|
11
|
+
// module is the SINGLE, tested implementation of that envelope machinery; each product is a THIN
|
|
12
|
+
// adapter that supplies its OWN container `kind`/`note` plus its OWN unsigned payload validator +
|
|
13
|
+
// serializer (so the core stays product-agnostic and never requires a product module — no back-edge).
|
|
14
|
+
//
|
|
15
|
+
// THE SCHEME (detached, NOT EIP-712)
|
|
16
|
+
// `eip191-personal-sign` means: the signer ran `personal_sign` (EIP-191) over the EXACT canonical
|
|
17
|
+
// unsigned bytes (the UTF-8 of the embedded `attestation` string, including its single trailing
|
|
18
|
+
// newline). A detached signature — NOT EIP-712 typed data — so the signed message IS the canonical
|
|
19
|
+
// payload bytes verbatim, with no separate domain/struct encoding to drift from them.
|
|
20
|
+
//
|
|
21
|
+
// WRAP-DON'T-EDIT INVARIANT
|
|
22
|
+
// The embedded UNSIGNED payload is re-parsed and re-validated by the PRODUCT's own unsigned validator
|
|
23
|
+
// (injected as `cfg.validateUnsigned`), and the embedded string is required byte-for-byte equal to
|
|
24
|
+
// `cfg.serializeUnsigned(embedded)` — so wrapping adds a vouch, it NEVER edits the thing vouched for,
|
|
25
|
+
// and the bytes that were signed are unambiguous.
|
|
26
|
+
|
|
27
|
+
const { verifyMessage, getAddress, Wallet } = require("ethers");
|
|
28
|
+
const fs = require("fs");
|
|
29
|
+
|
|
30
|
+
// The detached signature schemes this build understands. Each is an EXPLICIT, documented value so a
|
|
31
|
+
// reader knows EXACTLY what bytes were signed and how. `eip191-personal-sign` = EIP-191 personal_sign
|
|
32
|
+
// over the canonical UNSIGNED attestation bytes (a 65-byte r||s||v secp256k1 signature). Shared across
|
|
33
|
+
// the whole product family so the supported-scheme set can never diverge between products.
|
|
34
|
+
const SIGNED_ATTESTATION_SCHEMES = Object.freeze(["eip191-personal-sign"]);
|
|
35
|
+
|
|
36
|
+
// A 0x-prefixed, 0x-only, EVEN-length, non-empty hex string for the signature. eip191-personal-sign is
|
|
37
|
+
// specifically a 65-byte (r||s||v) secp256k1 signature -> exactly 130 hex chars. Strict by scheme below.
|
|
38
|
+
//
|
|
39
|
+
// CANONICAL CASE (byte-determinism). These accept ONLY lowercase hex. The signature block is the
|
|
40
|
+
// HUMAN-supplied part of the container and the part most likely to arrive EIP-55-checksummed (mixed
|
|
41
|
+
// case) or upper-cased. If we accepted mixed case and round-tripped it verbatim, two structurally
|
|
42
|
+
// identical containers over the SAME logical signature would serialize to DIFFERENT bytes — breaking the
|
|
43
|
+
// byte-determinism a future indexer/UI keys on. We REJECT non-canonical case on read/validate (rather
|
|
44
|
+
// than silently normalizing) so the wire format ossifies with one — and only one — byte encoding.
|
|
45
|
+
const HEXSTR_RE = /^0x([0-9a-f]{2})+$/;
|
|
46
|
+
const EIP191_SIG_RE = /^0x[0-9a-f]{130}$/; // 65 bytes: r(32) || s(32) || v(1)
|
|
47
|
+
|
|
48
|
+
// A claimed 0x-address: 0x + 40 LOWERCASE hex chars. The container records the CLAIMED signer; the
|
|
49
|
+
// recovery step (recoverSigner below) derives the actual signer from the signature. Lowercase-only for
|
|
50
|
+
// the same byte-determinism reason as the signature value: an EIP-55-checksummed (mixed-case) signer is
|
|
51
|
+
// the canonical address in a DIFFERENT encoding, so accepting it verbatim would let the same signer
|
|
52
|
+
// serialize two ways. A caller holding a checksummed address lowercases it before building the container.
|
|
53
|
+
const ADDRESS_RE = /^0x[0-9a-f]{40}$/;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Internal: assert a product passed a structurally complete signed-container config. The injected
|
|
57
|
+
* `validateUnsigned`/`serializeUnsigned` are how the core enforces the wrap-don't-edit invariant WITHOUT
|
|
58
|
+
* knowing anything product-specific (so there is no `require("../dataset")` back-edge).
|
|
59
|
+
*/
|
|
60
|
+
function _requireCfg(cfg) {
|
|
61
|
+
if (!cfg || typeof cfg !== "object") {
|
|
62
|
+
throw new Error("attestation core requires a { kind, schemaVersion, supportedSchemaVersions, note, validateUnsigned, serializeUnsigned } config");
|
|
63
|
+
}
|
|
64
|
+
if (typeof cfg.kind !== "string" || cfg.kind.length === 0) {
|
|
65
|
+
throw new Error("attestation core config requires a non-empty string `kind`");
|
|
66
|
+
}
|
|
67
|
+
if (!Array.isArray(cfg.supportedSchemaVersions) || cfg.supportedSchemaVersions.length === 0) {
|
|
68
|
+
throw new Error("attestation core config requires a non-empty `supportedSchemaVersions` array");
|
|
69
|
+
}
|
|
70
|
+
if (typeof cfg.note !== "string") {
|
|
71
|
+
throw new Error("attestation core config requires a string `note` (the in-band trust caveat)");
|
|
72
|
+
}
|
|
73
|
+
if (typeof cfg.validateUnsigned !== "function" || typeof cfg.serializeUnsigned !== "function") {
|
|
74
|
+
throw new Error(
|
|
75
|
+
"attestation core config requires `validateUnsigned` and `serializeUnsigned` functions (the product's UNSIGNED payload codec)"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (typeof cfg.label !== "string" && cfg.label !== undefined) {
|
|
79
|
+
throw new Error("attestation core config `label`, when present, must be a string");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Strictly validate a parsed SIGNED-attestation container against a product's framing. Throws an Error
|
|
85
|
+
* describing the FIRST problem; never mutates and never fills defaults. REJECTS: a wrong kind/
|
|
86
|
+
* schemaVersion, a wrong `note`, a non-string embedded `attestation`, a missing/non-object `signature`
|
|
87
|
+
* block, an unknown `scheme`, a malformed `signer` address, a missing/!hex `signature` value, or an
|
|
88
|
+
* embedded `attestation` that does not re-validate as a sound UNSIGNED payload (i.e. it must STILL be
|
|
89
|
+
* signed:false/signature:null — wrapping never edits). It NEVER half-accepts.
|
|
90
|
+
*
|
|
91
|
+
* @param {any} obj
|
|
92
|
+
* @param {object} cfg the product's signed-container framing (see buildSignedAttestation)
|
|
93
|
+
* @returns {object} the same object, if valid
|
|
94
|
+
*/
|
|
95
|
+
function validateSignedAttestation(obj, cfg) {
|
|
96
|
+
_requireCfg(cfg);
|
|
97
|
+
const label = cfg.label || "signed dataset attestation";
|
|
98
|
+
if (obj == null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
99
|
+
throw new Error(`${label} must be a JSON object`);
|
|
100
|
+
}
|
|
101
|
+
if (obj.kind !== cfg.kind) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`not a verifyhash ${label} (kind: ${JSON.stringify(obj.kind)}; expected ` +
|
|
104
|
+
`${JSON.stringify(cfg.kind)})`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (!cfg.supportedSchemaVersions.includes(obj.schemaVersion)) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`unsupported ${label} schemaVersion: ${JSON.stringify(obj.schemaVersion)} ` +
|
|
110
|
+
`(this build understands ${JSON.stringify(cfg.supportedSchemaVersions)})`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (obj.note !== cfg.note) {
|
|
114
|
+
throw new Error(`${label} note must be the standing SIGNED_ATTESTATION_TRUST_NOTE`);
|
|
115
|
+
}
|
|
116
|
+
// The embedded UNSIGNED payload is carried as the EXACT canonical bytes serializeUnsigned emits — a
|
|
117
|
+
// STRING, so the signed-over bytes are unambiguous. Re-parse and re-validate it with the PRODUCT's
|
|
118
|
+
// strict unsigned validator: it must STILL be signed:false/signature:null. This is the wrap-don't-edit
|
|
119
|
+
// invariant — a signed container can never smuggle an edited or already-"signed" payload.
|
|
120
|
+
if (typeof obj.attestation !== "string") {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`${label} must embed the canonical UNSIGNED attestation as a string \`attestation\``
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
let embedded;
|
|
126
|
+
try {
|
|
127
|
+
embedded = JSON.parse(obj.attestation);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
throw new Error(`embedded attestation is not valid JSON: ${e.message}`);
|
|
130
|
+
}
|
|
131
|
+
// Re-validate the embedded payload by the PRODUCT's unsigned validator (throws on signed:true etc.).
|
|
132
|
+
cfg.validateUnsigned(embedded);
|
|
133
|
+
// Re-serialize the embedded payload and require the embedded STRING to be byte-identical to the
|
|
134
|
+
// canonical form. This pins the embedded bytes to EXACTLY what serializeUnsigned emits — the bytes
|
|
135
|
+
// that were signed over — so no insignificant-whitespace / reordered variant can sneak in.
|
|
136
|
+
if (obj.attestation !== cfg.serializeUnsigned(embedded)) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
"embedded attestation is not in canonical form (the signed-over bytes must be byte-for-byte " +
|
|
139
|
+
"serializeAttestation's output)"
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const sig = obj.signature;
|
|
144
|
+
if (sig == null || typeof sig !== "object" || Array.isArray(sig)) {
|
|
145
|
+
throw new Error(`${label} signature must be a { scheme, signer, signature } object`);
|
|
146
|
+
}
|
|
147
|
+
if (!SIGNED_ATTESTATION_SCHEMES.includes(sig.scheme)) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`unknown signature scheme: ${JSON.stringify(sig.scheme)} ` +
|
|
150
|
+
`(this build understands ${JSON.stringify(SIGNED_ATTESTATION_SCHEMES)})`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (typeof sig.signer !== "string" || !ADDRESS_RE.test(sig.signer)) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`signature signer must be a 0x-prefixed 20-byte LOWERCASE-hex address ` +
|
|
156
|
+
`(checksummed/mixed-case rejected for byte-determinism — lowercase it first), got: ${String(sig.signer)}`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
if (typeof sig.signature !== "string" || !HEXSTR_RE.test(sig.signature)) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`signature value must be a 0x-prefixed LOWERCASE-hex string ` +
|
|
162
|
+
`(mixed/upper case rejected for byte-determinism), got: ${String(sig.signature)}`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
// Per-scheme shape: eip191-personal-sign is a 65-byte r||s||v secp256k1 signature.
|
|
166
|
+
if (sig.scheme === "eip191-personal-sign" && !EIP191_SIG_RE.test(sig.signature)) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`eip191-personal-sign signature must be a 65-byte (r||s||v) 0x-hex string, got length ${sig.signature.length}`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return obj;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Assemble + validate a SIGNED-attestation container from a validated UNSIGNED payload and a detached
|
|
176
|
+
* signature triple, PARAMETERIZED by the product's container framing. PURE: it performs NO signing and
|
|
177
|
+
* NO key handling — the loop never holds a key. It embeds the EXACT canonical unsigned bytes
|
|
178
|
+
* (cfg.serializeUnsigned(attestation)) as a string so the signed-over bytes are unambiguous, then
|
|
179
|
+
* attaches { scheme, signer, signature } and strictly validates the whole container.
|
|
180
|
+
*
|
|
181
|
+
* @param {object} params
|
|
182
|
+
* @param {object} params.attestation a validated UNSIGNED payload (re-validated via cfg.validateUnsigned)
|
|
183
|
+
* @param {string} params.scheme one of SIGNED_ATTESTATION_SCHEMES (e.g. "eip191-personal-sign")
|
|
184
|
+
* @param {string} params.signer the claimed 0x-address of the signer
|
|
185
|
+
* @param {string} params.signature the 0x-hex detached signature over cfg.serializeUnsigned(attestation)
|
|
186
|
+
* @param {object} cfg the product's signed-container framing
|
|
187
|
+
* @returns {object} a validated signed-attestation container
|
|
188
|
+
*/
|
|
189
|
+
function buildSignedAttestation(params, cfg) {
|
|
190
|
+
_requireCfg(cfg);
|
|
191
|
+
if (!params || typeof params !== "object") {
|
|
192
|
+
throw new Error("buildSignedAttestation requires { attestation, scheme, signer, signature }");
|
|
193
|
+
}
|
|
194
|
+
const { attestation, scheme, signer, signature } = params;
|
|
195
|
+
// The embedded payload must itself be a sound UNSIGNED payload before we wrap it (re-validate so a
|
|
196
|
+
// programmatic caller that hand-built one is checked too). validateUnsigned rejects signed:true.
|
|
197
|
+
cfg.validateUnsigned(attestation);
|
|
198
|
+
// Embed the EXACT canonical bytes — the string serializeUnsigned emits — so the signed-over bytes are
|
|
199
|
+
// byte-for-byte unambiguous.
|
|
200
|
+
const container = {
|
|
201
|
+
kind: cfg.kind,
|
|
202
|
+
schemaVersion: cfg.schemaVersion,
|
|
203
|
+
note: cfg.note,
|
|
204
|
+
attestation: cfg.serializeUnsigned(attestation),
|
|
205
|
+
signature: { scheme, signer, signature },
|
|
206
|
+
};
|
|
207
|
+
validateSignedAttestation(container, cfg);
|
|
208
|
+
return container;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Sign a validated UNSIGNED payload with a caller-supplied signer and WRAP it into a validated signed
|
|
213
|
+
* container — the single, tested place that turns a payload + key-holder into the detached-signature
|
|
214
|
+
* envelope, parameterized by the product's framing.
|
|
215
|
+
*
|
|
216
|
+
* KEY HYGIENE (why a signer OBJECT, never a raw key). This helper takes an ethers signer-like object
|
|
217
|
+
* (exposing async `getAddress()` + `signMessage(bytes|string)` — e.g. an ethers `Wallet`). It NEVER
|
|
218
|
+
* accepts a raw private-key string, NEVER persists a key, and NEVER logs one: the key lives only inside
|
|
219
|
+
* the caller's signer object. Loading a key from a keystore/env/HSM and constructing that signer is the
|
|
220
|
+
* CLI layer's job (T-19.2); this core stays key-agnostic.
|
|
221
|
+
*
|
|
222
|
+
* WHAT IS SIGNED (byte-for-byte). It re-validates the unsigned payload via `cfg.validateUnsigned` and
|
|
223
|
+
* serializes it to the EXACT canonical bytes with `cfg.serializeUnsigned` — the SAME string
|
|
224
|
+
* `recoverSigner` later runs `verifyMessage` over (including the trailing newline). It then runs
|
|
225
|
+
* `signer.signMessage(canonicalBytes)` (EIP-191 personal_sign), reads `signer.getAddress()`, lowercases
|
|
226
|
+
* it, and routes the triple through the EXISTING `buildSignedAttestation` so the container is assembled
|
|
227
|
+
* AND strictly validated by the one shared path (no new container assembly here). The result therefore
|
|
228
|
+
* ROUND-TRIPS by construction: verifySignedAttestation recovers exactly this signer over exactly these
|
|
229
|
+
* bytes, and binding against `cfg.serializeUnsigned(attestation)` passes.
|
|
230
|
+
*
|
|
231
|
+
* The embedded UNSIGNED payload is WRAPPED, never edited — it stays signed:false/signature:null (the
|
|
232
|
+
* wrap-don't-edit invariant, enforced by buildSignedAttestation re-validating it).
|
|
233
|
+
*
|
|
234
|
+
* @param {object} params
|
|
235
|
+
* @param {object} params.attestation a validated UNSIGNED payload (re-validated via cfg.validateUnsigned)
|
|
236
|
+
* @param {object} params.signer an ethers signer-like object: async getAddress() + signMessage(bytes|string)
|
|
237
|
+
* @param {object} cfg the product's signed-container framing
|
|
238
|
+
* @returns {Promise<object>} the validated signed-attestation container
|
|
239
|
+
*/
|
|
240
|
+
async function signAttestation(params, cfg) {
|
|
241
|
+
_requireCfg(cfg);
|
|
242
|
+
if (!params || typeof params !== "object") {
|
|
243
|
+
throw new Error("signAttestation requires { attestation, signer }");
|
|
244
|
+
}
|
|
245
|
+
const { attestation, signer } = params;
|
|
246
|
+
if (!signer || (typeof signer !== "object" && typeof signer !== "function")) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
"signAttestation requires a `signer` object exposing getAddress() + signMessage() (e.g. an ethers Wallet); a raw private-key string is NOT accepted"
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (typeof signer.getAddress !== "function" || typeof signer.signMessage !== "function") {
|
|
252
|
+
throw new Error(
|
|
253
|
+
"signAttestation `signer` must expose getAddress() and signMessage() (an ethers signer-like object)"
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
// (a) Re-validate the unsigned payload and serialize it to the EXACT canonical bytes — the same string
|
|
257
|
+
// recoverSigner runs verifyMessage over (byte-for-byte, including the trailing newline). We validate
|
|
258
|
+
// FIRST so we never ask the signer to sign a malformed/already-"signed" payload.
|
|
259
|
+
cfg.validateUnsigned(attestation);
|
|
260
|
+
const canonicalBytes = cfg.serializeUnsigned(attestation);
|
|
261
|
+
// (b) EIP-191 personal_sign over exactly those bytes. The key never leaves the signer object.
|
|
262
|
+
const signature = await signer.signMessage(canonicalBytes);
|
|
263
|
+
// (c) Read the signer's address, lowercase it (the container records the CLAIMED signer in canonical
|
|
264
|
+
// lowercase), and route through the EXISTING builder so the container is assembled AND strictly
|
|
265
|
+
// validated by the one shared path — no separate container assembly here.
|
|
266
|
+
const signerAddress = (await signer.getAddress()).toLowerCase();
|
|
267
|
+
return buildSignedAttestation(
|
|
268
|
+
{ attestation, scheme: "eip191-personal-sign", signer: signerAddress, signature },
|
|
269
|
+
cfg
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Resolve a HUMAN-supplied private key from EXACTLY ONE source — an env var (`keyEnv`, read via
|
|
275
|
+
* `process.env[keyEnv]`) or a file the human created (`keyFile`, read with fs) — and construct an
|
|
276
|
+
* in-process ethers `Wallet` from it. This is the ONE place the CLI sign-path turns a caller-provisioned
|
|
277
|
+
* key into a signer object; the key is read, used to build the Wallet, and then exists ONLY inside that
|
|
278
|
+
* Wallet (the raw string is never returned, persisted, or logged).
|
|
279
|
+
*
|
|
280
|
+
* KEY HYGIENE (load-bearing). EXACTLY ONE of `keyEnv`/`keyFile` must be supplied: neither, both, a
|
|
281
|
+
* missing env var, an unreadable file, or a malformed/zero key HARD-ERRORS with a clear, actionable
|
|
282
|
+
* message — and the message NEVER includes the key material (only the SOURCE: the var name or the path).
|
|
283
|
+
* The key is trimmed of surrounding whitespace/newlines (so a key file written by `echo`/an editor works),
|
|
284
|
+
* a bare 64-hex key is accepted (0x is prefixed for it), and an all-zero key is rejected (it is not a
|
|
285
|
+
* usable signer and is a common "empty placeholder" mistake). All validation happens BEFORE any signing.
|
|
286
|
+
*
|
|
287
|
+
* The Wallet is NOT given a provider — signing an attestation is purely offline (EIP-191 personal_sign),
|
|
288
|
+
* needs no network, and must never be able to touch a chain.
|
|
289
|
+
*
|
|
290
|
+
* @param {object} params
|
|
291
|
+
* @param {string} [params.keyEnv] name of an env var holding the private key (read via process.env)
|
|
292
|
+
* @param {string} [params.keyFile] path to a file the human created holding the private key
|
|
293
|
+
* @returns {{ wallet: object, source: string }} the in-process Wallet + a human SOURCE label (no key)
|
|
294
|
+
*/
|
|
295
|
+
function loadSigningWallet(params) {
|
|
296
|
+
if (!params || typeof params !== "object") {
|
|
297
|
+
throw new Error("loadSigningWallet requires { keyEnv } or { keyFile }");
|
|
298
|
+
}
|
|
299
|
+
const { keyEnv, keyFile } = params;
|
|
300
|
+
const hasEnv = keyEnv !== undefined && keyEnv !== null;
|
|
301
|
+
const hasFile = keyFile !== undefined && keyFile !== null;
|
|
302
|
+
|
|
303
|
+
// EXACTLY ONE key source. Neither and both are BOTH hard errors (a clear, actionable message), so the
|
|
304
|
+
// human is never surprised about WHICH key signed (or that nothing was provided).
|
|
305
|
+
if (!hasEnv && !hasFile) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
"no signing key: pass EXACTLY ONE of --key-env <VAR> (read process.env[VAR]) or " +
|
|
308
|
+
"--key-file <path> (a key file YOU created). The key must be one you provisioned outside this tool."
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (hasEnv && hasFile) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
"--key-env and --key-file are mutually exclusive; pass EXACTLY ONE signing-key source"
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Read the raw key from the single chosen source. The error messages name only the SOURCE (the env var
|
|
318
|
+
// name or the file path) — NEVER the key material.
|
|
319
|
+
let raw;
|
|
320
|
+
let source;
|
|
321
|
+
if (hasEnv) {
|
|
322
|
+
if (typeof keyEnv !== "string" || keyEnv.length === 0) {
|
|
323
|
+
throw new Error("--key-env requires a non-empty environment-variable NAME");
|
|
324
|
+
}
|
|
325
|
+
source = `env:${keyEnv}`;
|
|
326
|
+
const fromEnv = process.env[keyEnv];
|
|
327
|
+
if (fromEnv === undefined || fromEnv === "") {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`environment variable ${keyEnv} is not set (or empty); it must hold the signing private key`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
raw = fromEnv;
|
|
333
|
+
} else {
|
|
334
|
+
if (typeof keyFile !== "string" || keyFile.length === 0) {
|
|
335
|
+
throw new Error("--key-file requires a non-empty file PATH");
|
|
336
|
+
}
|
|
337
|
+
source = `file:${keyFile}`;
|
|
338
|
+
try {
|
|
339
|
+
raw = fs.readFileSync(keyFile, "utf8");
|
|
340
|
+
} catch (e) {
|
|
341
|
+
// Surface the OS error (ENOENT/EACCES…) but never the key — the file was unreadable, so there is no
|
|
342
|
+
// key to leak here anyway.
|
|
343
|
+
throw new Error(`cannot read --key-file ${keyFile}: ${e.message}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Normalize: trim surrounding whitespace/newlines (a key file from `echo`/an editor has a trailing \n),
|
|
348
|
+
// and accept a bare 64-hex key by prefixing 0x. Then validate WITHOUT echoing the key on failure.
|
|
349
|
+
let key = String(raw).trim();
|
|
350
|
+
if (key.length === 0) {
|
|
351
|
+
throw new Error(`signing key from ${source} is empty after trimming whitespace`);
|
|
352
|
+
}
|
|
353
|
+
if (/^[0-9a-fA-F]{64}$/.test(key)) key = "0x" + key;
|
|
354
|
+
|
|
355
|
+
// Reject an all-zero key explicitly (a common empty-placeholder mistake; ethers would also reject it,
|
|
356
|
+
// but we give a clearer, key-free message). Compare case-insensitively, with or without 0x.
|
|
357
|
+
const stripped = key.toLowerCase().startsWith("0x") ? key.slice(2) : key;
|
|
358
|
+
if (/^0{64}$/.test(stripped)) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`signing key from ${source} is the all-zero key, which is not a usable signer ` +
|
|
361
|
+
"(provision a real key outside this tool)"
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let wallet;
|
|
366
|
+
try {
|
|
367
|
+
wallet = new Wallet(key);
|
|
368
|
+
} catch (e) {
|
|
369
|
+
// ethers' message can be verbose; it does NOT echo the key, but we replace it with a fixed, key-free
|
|
370
|
+
// message naming only the SOURCE so nothing about the key material can ever reach stderr/logs.
|
|
371
|
+
throw new Error(
|
|
372
|
+
`signing key from ${source} is not a valid private key (expected a 32-byte 0x-hex secp256k1 key)`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
// The key now lives ONLY inside `wallet`; `key`/`raw` go out of scope when this function returns.
|
|
376
|
+
return { wallet, source };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Serialize a signed-attestation container to its canonical, byte-deterministic bytes: a FIXED top-level
|
|
381
|
+
* (and signature-block) key order, NO insignificant whitespace, a single trailing newline. Two runs over
|
|
382
|
+
* the same inputs produce an identical string.
|
|
383
|
+
* @param {object} container a validated signed-attestation container
|
|
384
|
+
* @param {object} cfg the product's signed-container framing
|
|
385
|
+
* @returns {string} the canonical serialization (newline-terminated)
|
|
386
|
+
*/
|
|
387
|
+
function serializeSignedAttestation(container, cfg) {
|
|
388
|
+
validateSignedAttestation(container, cfg);
|
|
389
|
+
const canonical = {
|
|
390
|
+
kind: container.kind,
|
|
391
|
+
schemaVersion: container.schemaVersion,
|
|
392
|
+
note: container.note,
|
|
393
|
+
// The embedded canonical UNSIGNED bytes (a string) — JSON.stringify escapes it, preserving the exact
|
|
394
|
+
// bytes including the embedded trailing newline.
|
|
395
|
+
attestation: container.attestation,
|
|
396
|
+
signature: {
|
|
397
|
+
scheme: container.signature.scheme,
|
|
398
|
+
signer: container.signature.signer,
|
|
399
|
+
signature: container.signature.signature,
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
return JSON.stringify(canonical) + "\n";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Read, parse, and STRICTLY validate the signed-attestation container at `signedPath` against a
|
|
407
|
+
* product's framing. Round-trips with serializeSignedAttestation. Throws on a missing file or invalid
|
|
408
|
+
* JSON too. The `label` (default "signed dataset attestation") parameterizes only the human noun in the
|
|
409
|
+
* I/O error messages so DataLedger's strings stay byte-identical.
|
|
410
|
+
*
|
|
411
|
+
* @param {string} signedPath
|
|
412
|
+
* @param {object} cfg the product's signed-container framing
|
|
413
|
+
* @returns {object} the validated container
|
|
414
|
+
*/
|
|
415
|
+
function readSignedAttestation(signedPath, cfg) {
|
|
416
|
+
_requireCfg(cfg);
|
|
417
|
+
const fs = require("fs");
|
|
418
|
+
const label = cfg.label || "signed dataset attestation";
|
|
419
|
+
if (!signedPath || typeof signedPath !== "string") {
|
|
420
|
+
throw new Error("readSignedAttestation requires a signed attestation file path");
|
|
421
|
+
}
|
|
422
|
+
let raw;
|
|
423
|
+
try {
|
|
424
|
+
raw = fs.readFileSync(signedPath, "utf8");
|
|
425
|
+
} catch (e) {
|
|
426
|
+
throw new Error(`cannot read ${label} at ${signedPath}: ${e.message}`);
|
|
427
|
+
}
|
|
428
|
+
let obj;
|
|
429
|
+
try {
|
|
430
|
+
obj = JSON.parse(raw);
|
|
431
|
+
} catch (e) {
|
|
432
|
+
throw new Error(`${label} at ${signedPath} is not valid JSON: ${e.message}`);
|
|
433
|
+
}
|
|
434
|
+
return validateSignedAttestation(obj, cfg);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Recover the signing address from a signed-attestation container's embedded canonical bytes + signature
|
|
439
|
+
* per the declared `scheme`. PURE: no I/O, no key, no network. For `eip191-personal-sign` this is ethers'
|
|
440
|
+
* `verifyMessage(<embedded canonical bytes>, signature)` — EIP-191 personal_sign recovery over the EXACT
|
|
441
|
+
* bytes that were signed. Returns the recovered address as a LOWERCASE 0x-hex string. Throws on an
|
|
442
|
+
* unknown scheme (defense-in-depth: validateSignedAttestation already rejects one) or an unrecoverable
|
|
443
|
+
* signature. Product-agnostic — the embedded bytes are whatever the container carries.
|
|
444
|
+
*
|
|
445
|
+
* @param {object} container a validated signed-attestation container
|
|
446
|
+
* @returns {string} the recovered signer address, 0x-prefixed lowercase
|
|
447
|
+
*/
|
|
448
|
+
function recoverSigner(container) {
|
|
449
|
+
const { scheme, signature } = container.signature;
|
|
450
|
+
if (scheme === "eip191-personal-sign") {
|
|
451
|
+
// The signed message IS the embedded canonical UNSIGNED bytes verbatim (the string, including its
|
|
452
|
+
// single trailing newline). verifyMessage runs EIP-191 personal_sign recovery over exactly those bytes.
|
|
453
|
+
const recovered = verifyMessage(container.attestation, signature);
|
|
454
|
+
return recovered.toLowerCase();
|
|
455
|
+
}
|
|
456
|
+
throw new Error(
|
|
457
|
+
`cannot recover signer for unknown signature scheme: ${JSON.stringify(scheme)} ` +
|
|
458
|
+
`(this build understands ${JSON.stringify(SIGNED_ATTESTATION_SCHEMES)})`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Verify (purely, OFFLINE) a signed-attestation container: recover the signer from the embedded canonical
|
|
464
|
+
* bytes + signature and confirm it equals the container's CLAIMED `signer`; OPTIONALLY pin it to an
|
|
465
|
+
* EXPECTED signer (`expectedSigner`); OPTIONALLY confirm the signature binds a caller's own item
|
|
466
|
+
* (`expectedCanonical` — the canonical UNSIGNED bytes the caller recomputed from their own data) by
|
|
467
|
+
* requiring them byte-identical to the embedded payload. The verdict is ACCEPTED only when EVERY
|
|
468
|
+
* requested check passes.
|
|
469
|
+
*
|
|
470
|
+
* No I/O, no provider, no key, no network. Throws only on an unrecoverable signature when the scheme is
|
|
471
|
+
* unknown; a recovered address that simply doesn't match is a clean REJECTED (a normal verdict).
|
|
472
|
+
*
|
|
473
|
+
* This is the GENERIC verify core. Products supply the canonical bytes to bind against (computed from
|
|
474
|
+
* THEIR own item) rather than the core knowing how to build them.
|
|
475
|
+
*
|
|
476
|
+
* @param {object} params
|
|
477
|
+
* @param {object} params.container a validated signed-attestation container
|
|
478
|
+
* @param {string} [params.expectedSigner] OPTIONAL expected signer 0x-address; checked when present
|
|
479
|
+
* @param {string} [params.expectedCanonical] OPTIONAL canonical UNSIGNED bytes to bind; checked when present
|
|
480
|
+
* @returns {{
|
|
481
|
+
* verdict: "ACCEPTED"|"REJECTED",
|
|
482
|
+
* accepted: boolean,
|
|
483
|
+
* recoveredSigner: string,
|
|
484
|
+
* claimedSigner: string,
|
|
485
|
+
* scheme: string,
|
|
486
|
+
* checks: {
|
|
487
|
+
* signatureMatchesSigner: boolean,
|
|
488
|
+
* signerMatchesExpected: boolean|null,
|
|
489
|
+
* manifestBindsAttestation: boolean|null,
|
|
490
|
+
* },
|
|
491
|
+
* expectedSigner: string|null,
|
|
492
|
+
* manifestChecked: boolean,
|
|
493
|
+
* failedChecks: string[],
|
|
494
|
+
* }}
|
|
495
|
+
*/
|
|
496
|
+
function verifySignedAttestation(params) {
|
|
497
|
+
if (!params || typeof params !== "object") {
|
|
498
|
+
throw new Error("verifySignedAttestation requires { container, [expectedSigner], [expectedCanonical] }");
|
|
499
|
+
}
|
|
500
|
+
const { container, expectedSigner, expectedCanonical } = params;
|
|
501
|
+
|
|
502
|
+
const claimedSigner = container.signature.signer; // validated lowercase 0x-address
|
|
503
|
+
const scheme = container.signature.scheme;
|
|
504
|
+
|
|
505
|
+
// (b) Recover the signer from the embedded canonical bytes + signature, and confirm it equals the
|
|
506
|
+
// container's CLAIMED `signer`. A signature that does not recover to the claimed signer means the
|
|
507
|
+
// `signer` label is unbacked — a clean check failure (REJECTED), not an error.
|
|
508
|
+
//
|
|
509
|
+
// A TAMPERED signature can be not merely WRONG but UNRECOVERABLE: a corrupted (r,s,v) may have no
|
|
510
|
+
// valid secp256k1 point, in which case ethers' verifyMessage throws. That is still a caller-facing
|
|
511
|
+
// REJECTED verdict, NOT a crash — so we catch it and treat it as a failed signature check (the
|
|
512
|
+
// recovered signer is the explicit "(unrecoverable)" sentinel, never a real address). An unknown
|
|
513
|
+
// scheme is a different (structural) failure and is re-thrown — validateSignedAttestation already
|
|
514
|
+
// rejects it, so this is defense-in-depth that should never fire for a read container.
|
|
515
|
+
let recoveredSigner;
|
|
516
|
+
let signatureMatchesSigner;
|
|
517
|
+
try {
|
|
518
|
+
recoveredSigner = recoverSigner(container);
|
|
519
|
+
signatureMatchesSigner = recoveredSigner === claimedSigner.toLowerCase();
|
|
520
|
+
} catch (e) {
|
|
521
|
+
if (/unknown signature scheme/.test(e.message)) throw e;
|
|
522
|
+
recoveredSigner = "(unrecoverable)";
|
|
523
|
+
signatureMatchesSigner = false;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// (c) OPTIONAL pin: confirm the recovered signer equals the EXPECTED address the caller pinned.
|
|
527
|
+
// Normalize the expected address (accept checksummed/mixed-case via getAddress, then lowercase) so a
|
|
528
|
+
// caller can paste an EIP-55 address. null = not requested.
|
|
529
|
+
let signerMatchesExpected = null;
|
|
530
|
+
let normalizedExpected = null;
|
|
531
|
+
if (expectedSigner !== undefined && expectedSigner !== null) {
|
|
532
|
+
normalizedExpected = getAddress(expectedSigner).toLowerCase();
|
|
533
|
+
// Pin against the RECOVERED signer (not the merely-claimed one): the caller pins WHO actually signed.
|
|
534
|
+
signerMatchesExpected = recoveredSigner === normalizedExpected;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// (d) OPTIONAL binding: require the caller-supplied canonical UNSIGNED bytes byte-identical to the
|
|
538
|
+
// embedded (signed-over) payload. This proves the signature binds the item the caller actually
|
|
539
|
+
// holds, not some other one. null = not requested.
|
|
540
|
+
let manifestBindsAttestation = null;
|
|
541
|
+
if (expectedCanonical !== undefined && expectedCanonical !== null) {
|
|
542
|
+
manifestBindsAttestation = expectedCanonical === container.attestation;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Verdict: ACCEPTED only when EVERY REQUESTED check passes. The signature-vs-signer check is ALWAYS
|
|
546
|
+
// requested; the other two only when their flag was given (null = not requested, never fails the gate).
|
|
547
|
+
const failedChecks = [];
|
|
548
|
+
if (!signatureMatchesSigner) failedChecks.push("signatureMatchesSigner");
|
|
549
|
+
if (signerMatchesExpected === false) failedChecks.push("signerMatchesExpected");
|
|
550
|
+
if (manifestBindsAttestation === false) failedChecks.push("manifestBindsAttestation");
|
|
551
|
+
const accepted = failedChecks.length === 0;
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
verdict: accepted ? "ACCEPTED" : "REJECTED",
|
|
555
|
+
accepted,
|
|
556
|
+
recoveredSigner,
|
|
557
|
+
claimedSigner: claimedSigner.toLowerCase(),
|
|
558
|
+
scheme,
|
|
559
|
+
checks: {
|
|
560
|
+
signatureMatchesSigner,
|
|
561
|
+
signerMatchesExpected,
|
|
562
|
+
manifestBindsAttestation,
|
|
563
|
+
},
|
|
564
|
+
expectedSigner: normalizedExpected,
|
|
565
|
+
manifestChecked: manifestBindsAttestation !== null,
|
|
566
|
+
failedChecks,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
module.exports = {
|
|
571
|
+
SIGNED_ATTESTATION_SCHEMES,
|
|
572
|
+
validateSignedAttestation,
|
|
573
|
+
buildSignedAttestation,
|
|
574
|
+
signAttestation,
|
|
575
|
+
loadSigningWallet,
|
|
576
|
+
serializeSignedAttestation,
|
|
577
|
+
readSignedAttestation,
|
|
578
|
+
recoverSigner,
|
|
579
|
+
verifySignedAttestation,
|
|
580
|
+
};
|