@entpoind/verify 0.1.0 → 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ Added
6
+ - `verifyLedgerProof(proof, keys, { receipt?, witnessLog? })`: checks a write-once receipt-ledger proof offline. The leaf is
7
+ rebuilt from your receipt, the path shape and fold are checked against the signed batch root, and the root's signature is
8
+ verified with the regional key. No network unless you ask for it.
9
+ - The public witness. A verified ledger proof reports `result.witness` (`absent`, `off`, `pending`, `logged` or `malformed`).
10
+ A `logged` witness is checked offline against the root (hashes, Ed25519ph signature by the root's own key, the copied
11
+ Rekor entry and its inclusion proof) and, with `witnessLog`, against the log itself. The witness never changes `result.ok`.
12
+ - `verifyEd25519ph` (RFC 8032, SHA-512 prehash), `merkleLeafHash`, `verifyMerkleInclusionFromHash`, `ledgerRootMessage`,
13
+ `WITNESS_KIND`, `DEFAULT_REKOR_URL`.
14
+
15
+ Unchanged
16
+ - Receipt, artifact and bundle verification, the `verifyReceiptOnline` default key URL, and the zero-dependency, WebCrypto-only design.
17
+
18
+ Notes
19
+ - ESM only (`"type": "module"`), Node 20 or later. `require()` works from Node 22.12.
20
+
21
+ ## 0.1.0
22
+
23
+ First release: `verifyReceipt`, `verifyReceiptOnline`, `verifyArtifact`, `verifyBundle`, `verifyMerkleInclusion`,
24
+ `fetchToolsKeys`, `canonicalJcs`, `ed25519Supported`.
package/README.md CHANGED
@@ -23,6 +23,8 @@ wrapper the API returns.
23
23
 
24
24
  ## Where it runs
25
25
 
26
+ The package is ESM only (`import`), on Node 20 or later; `require()` works from Node 22.12. See `CHANGELOG.md` for what each version adds.
27
+
26
28
  Any runtime whose WebCrypto verifies Ed25519: Node 20 or later (tested on
27
29
  Node 22), Deno, Bun, Cloudflare Workers, and current browsers (Chrome and Edge
28
30
  137+, Firefox 129+, Safari 17+). Browsers expose WebCrypto only on secure
@@ -55,13 +57,17 @@ its `keys` to `verifyReceipt`.
55
57
  | `verifyArtifact(input, keys)` | Verify a standalone artifact envelope. |
56
58
  | `verifyBundle(input, keys)` | Signature, plus Merkle inclusion when anchored. |
57
59
  | `verifyMerkleInclusion(leaf, proof, root)` | Fold an inclusion proof locally. |
60
+ | `verifyLedgerProof(proof, keys, { receipt?, witnessLog? })` | Verify a write-once-ledger proof and report its public witness (below). No network unless `witnessLog` is set. |
61
+ | `verifyEd25519ph(publicKey, prehash, signature)` | The raw Ed25519ph (RFC 8032, SHA-512 prehash) check, as a boolean. |
58
62
  | `canonicalJcs(value)` | The RFC 8785 canonical string. |
59
63
  | `ed25519Supported()` | Whether this runtime can verify Ed25519. |
60
64
  | `verifySignature(key, message, signature)` | The raw Ed25519 check, as a boolean. |
61
65
 
62
66
  The `verify*` functions never throw on hostile input. `fetchToolsKeys` and
63
67
  `verifyReceiptOnline` throw when the keys cannot be fetched, so "could not
64
- check" is never reported as a verdict.
68
+ check" is never reported as a verdict. The witness log check does not throw: an
69
+ unreachable log is reported as `checked: false`, beside a ledger proof that has
70
+ already been judged.
65
71
 
66
72
  ## Failure reasons
67
73
 
@@ -76,6 +82,126 @@ check" is never reported as a verdict.
76
82
  A bundle without an anchor verifies with `anchored: false`; that is a valid
77
83
  result, not a failure.
78
84
 
85
+ ## Ledger proofs
86
+
87
+ A receipt that has been copied to the write-once ledger has a proof at
88
+ `GET /api/v1/tools/receipts/{receiptId}/ledger` (404 `not_anchored_yet` until the
89
+ copy is made). `verifyLedgerProof(proof, keys, { receipt })` checks, offline:
90
+
91
+ 1. **the leaf**: the envelope that was locked is recomputed from your saved receipt
92
+ (the SHA-256 of its canonical form, with its signature and key id) and must be
93
+ the leaf the proof starts from (`receiptBound: true`). Without `receipt` the
94
+ envelope in the proof, or just its leaf hash, is used;
95
+ 2. **the path**: it has the shape its leaf index and tree size require, and folds,
96
+ leaf to root;
97
+ 3. **the root**: it carries a valid signature by the region's receipt key (the same
98
+ published keys that verify your receipts) over a message that names the batch,
99
+ the tree size and the date the lock lapses.
100
+
101
+ This proves the copy was filed under that signed root. That the stored object is
102
+ still locked is checked at the storage service, with read access to the bucket,
103
+ using `s3.key` and `s3.versionId` from the proof.
104
+
105
+ | `reason` | Meaning |
106
+ |---|---|
107
+ | `malformed_proof` | Not a complete ledger proof. |
108
+ | `not_this_receipt` | The proof is for another receipt id. |
109
+ | `receipt_not_in_batch` | Your receipt does not hash to the leaf that was locked (it was altered, or the proof is for different bytes). |
110
+ | `proof_shape_mismatch` | The path does not fit the claimed leaf index and tree size. |
111
+ | `not_in_root` | The path does not reach the root. |
112
+ | `unknown_key` / `bad_root_signature` | The root's signing key is not in the set, or its signature does not verify. |
113
+
114
+ ### The public witness
115
+
116
+ A proof may carry `witness`: the batch root's entry in Sigstore's public Rekor log
117
+ (`hashedrekord` v0.0.1). The batch's root message is hashed, `A = SHA-512(message)`,
118
+ and `A` is signed in Ed25519ph mode by the **same** regional key that signed the root,
119
+ so the log records `data.hash = SHA-512(A)` (`witness.rekorSha512`).
120
+
121
+ A verified ledger proof always reports the witness in `result.witness`. The witness
122
+ never changes `result.ok`: the ledger proof stands or falls on its own.
123
+
124
+ | `witness.status` | Meaning |
125
+ |---|---|
126
+ | `absent` | The proof has no witness: the batch has not been submitted yet, or the witness is not switched on. |
127
+ | `off` | Not witnessed, by the workspace's choice: an owner or admin had turned the public witness off (it is on by default) before the receipt was batched, so the batch root was never submitted. Not a failure; the log is never asked. |
128
+ | `pending` | Submitted, not in the log yet; `since`, `attempts`, `lastError` and `nextAttemptAt` as the proof states them. |
129
+ | `logged` | `consistent: true` with `problems: []`, or `consistent: false` with the exact `problems` (below). |
130
+ | `malformed` | A `witness` whose status is none of `off`, `pending` and `logged`. |
131
+
132
+ **Offline (always).** No network. Checked against the root message rebuilt from the
133
+ proof and the root's published key:
134
+
135
+ - `artifactSha512` is SHA-512 of the root message, and `rekorSha512` is SHA-512 of
136
+ that (`artifact_hash_mismatch`, `rekor_hash_mismatch`); `kind` is
137
+ `rekor.hashedrekord.v0.0.1` (`unknown_witness_kind`);
138
+ - `signatureKeyId` is the root's key id (`not_the_root_key`), and `signature`
139
+ verifies as Ed25519ph with the root's key over the **recomputed** hash
140
+ (`bad_witness_signature`);
141
+ - the copy of the log's answer in the proof is that entry: `body` is a `hashedrekord`
142
+ with `data.hash = { sha512, <recomputed hash> }`, the root's public key and the same
143
+ signature (`entry_body_malformed`, `entry_hash_mismatch`, `entry_key_mismatch`,
144
+ `entry_signature_mismatch`); `entryUuid` ends in the RFC 6962 leaf hash of `body`
145
+ (`entry_uuid_mismatch`); the copied `inclusionProof`, when present, folds that leaf
146
+ to the root hash it names (`inclusion_proof_invalid`); `integratedAt` is the instant
147
+ `integratedTime` names (`integrated_at_mismatch`). A missing field is
148
+ `malformed_witness`.
149
+
150
+ That copy came from Entpoind, so the offline check shows the witness is consistent
151
+ with the root, not that the log holds it.
152
+
153
+ **Online (opt-in).** Pass `witnessLog: true`, or options:
154
+
155
+ ```ts
156
+ const r = await verifyLedgerProof(proof, keys, {
157
+ receipt,
158
+ witnessLog: { logPublicKey: rekorPem }, // or `true`; also logUrl, fetch, timeoutMs
159
+ });
160
+ if (r.ok && r.witness.status === "logged") console.log(r.witness.consistent, r.witness.logCheck);
161
+ ```
162
+
163
+ The entry is fetched from the log itself, `GET {log}/api/v1/log/entries/{entryUuid}`,
164
+ where `{log}` is `logUrl`, else the proof's `witness.log`, else
165
+ `https://rekor.sigstore.dev`. Only `https:` is used, and redirects are refused. On the
166
+ log's answer it checks:
167
+
168
+ - the entry holds the recomputed hash and the root's key, and its signature verifies
169
+ as Ed25519ph with that key; the UUID is the entry's leaf hash; its `logIndex` and
170
+ `integratedTime` are the ones the proof states;
171
+ - its **inclusion proof** folds the entry to the tree root it names (RFC 6962);
172
+ - its **checkpoint** (the signed note the inclusion proof ends in) names that tree size
173
+ and root, and is signed by the log;
174
+ - its **signed entry timestamp** is the log's signature over
175
+ `{ body, integratedTime, logID, logIndex }` (RFC 8785).
176
+
177
+ The log's signatures are checked with its public key: `logPublicKey` (SPKI PEM) if you
178
+ pin it (`logKey: "pinned"`), otherwise the key fetched from the same log at
179
+ `/api/v1/log/publicKey?treeID=…` (`logKey: "fetched"`). Either way the key must hash
180
+ (SHA-256) to the entry's `logID`. A fetched key is trust on first use: a log that is not
181
+ Sigstore's could serve its own key. Pin Sigstore's key (from its trust root, for example
182
+ the TUF repository the Sigstore clients use) for the strongest check.
183
+
184
+ `witness.logCheck` is `{ checked: false, reason }` when nothing was judged
185
+ (`log_url_not_https`, `no_entry_uuid`, `log_unreachable`, `log_error` with `status`),
186
+ or `{ checked: true, ok, problems, logKey, integratedTime, inclusionProof, checkpoint,
187
+ signedEntryTimestamp }`, each part `verified`, `failed` or `not_checked`. `ok` is true
188
+ only when all three parts are `verified` and there is no problem. A 404 from the log is
189
+ the finding `entry_not_in_log`.
190
+
191
+ **What is not checked.**
192
+
193
+ - Log keys other than ECDSA P-256 (`log_key_unsupported`). rekor.sigstore.dev uses
194
+ P-256.
195
+ - That the log is consistent over time: no consistency proof between checkpoints, no
196
+ checkpoint gossip or witness cosignatures. A log that showed different trees to
197
+ different people would need that to be caught; Sigstore's monitors do it.
198
+ - The copy's `signedEntryTimestamp` and checkpoint offline: they are checked only on
199
+ the log's fresh answer.
200
+ - Rekor v2 (tile-based) logs: this speaks the v1 API that the ledger submits to.
201
+ - What the witness cannot prove at all: that every receipt was put in a batch, or that
202
+ what a receipt says is true. It shows the root existed, signed by our key, no later
203
+ than the log's `integratedTime`.
204
+
79
205
  ## Documentation
80
206
 
81
207
  Receipt format, verification procedure and key rotation:
@@ -16,17 +16,17 @@ export class NonCanonicalizableError extends Error {
16
16
  this.name = "NonCanonicalizableError";
17
17
  }
18
18
  }
19
- function canonicalizeValue(value, path) {
19
+ function serializeValue(value, path) {
20
20
  if (value === null)
21
- return null;
21
+ return "null";
22
22
  const t = typeof value;
23
23
  if (t === "boolean" || t === "string")
24
- return value;
24
+ return JSON.stringify(value);
25
25
  if (t === "number") {
26
26
  if (!Number.isFinite(value)) {
27
27
  throw new NonCanonicalizableError(`non-finite number (${String(value)}) is not valid JSON`, path);
28
28
  }
29
- return value;
29
+ return JSON.stringify(value);
30
30
  }
31
31
  if (t === "bigint") {
32
32
  throw new NonCanonicalizableError("bigint cannot be canonicalized (no JSON representation)", path);
@@ -35,23 +35,24 @@ function canonicalizeValue(value, path) {
35
35
  throw new NonCanonicalizableError(`${t} cannot be canonicalized`, path);
36
36
  }
37
37
  if (Array.isArray(value)) {
38
- return value.map((v, i) => canonicalizeValue(v, `${path}[${i}]`));
38
+ return `[${value.map((v, i) => serializeValue(v, `${path}[${i}]`)).join(",")}]`;
39
39
  }
40
40
  const proto = Object.getPrototypeOf(value);
41
41
  if (proto !== Object.prototype && proto !== null) {
42
42
  throw new NonCanonicalizableError(`non-plain object (${value.constructor?.name ?? "unknown"}) cannot be canonicalized; pass plain JSON`, path);
43
43
  }
44
44
  const obj = value;
45
- const out = {};
45
+ // Written as text: an object would reorder integer-like keys and swallow a `__proto__` key.
46
+ const members = [];
46
47
  for (const key of Object.keys(obj).sort()) {
47
48
  const child = obj[key];
48
49
  if (child === undefined)
49
50
  continue;
50
- out[key] = canonicalizeValue(child, path ? `${path}.${key}` : key);
51
+ members.push(`${JSON.stringify(key)}:${serializeValue(child, path ? `${path}.${key}` : key)}`);
51
52
  }
52
- return out;
53
+ return `{${members.join(",")}}`;
53
54
  }
54
55
  /** The RFC 8785 canonical string for `value` — the bytes a signature covers. */
55
56
  export function canonicalJcs(value) {
56
- return JSON.stringify(canonicalizeValue(value, ""));
57
+ return serializeValue(value, "");
57
58
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @entpoind/verify — Ed25519ph (RFC 8032 §5.1, "HashEdDSA" with SHA-512) signature verification.
3
+ *
4
+ * Needed for one thing: the receipt ledger's public witness. Rekor accepts an Ed25519 key in a `hashedrekord` entry only as
5
+ * Ed25519ph, so the witness signature is Ed25519ph by the region's receipt key, and WebCrypto has no Ed25519ph. This is
6
+ * the RFC's verification procedure (§5.1.7) in BigInt arithmetic, with WebCrypto supplying SHA-512 and nothing else:
7
+ *
8
+ * k = SHA-512( dom2(1, "") ‖ R ‖ A ‖ PH(M) ) mod L, and accept iff encode([S]B − [k]A) == R
9
+ *
10
+ * the cofactorless check, with S < L and A and R decoded strictly (§5.1.3), as Go's crypto/ed25519 does. Tested against
11
+ * the RFC 8032 §7.3 vector and against signatures made by Go's crypto/ed25519.
12
+ *
13
+ * Verification only touches public values, so the arithmetic need not be constant-time. `ed25519phSign` exists for test
14
+ * fixtures; it is not exported from the package and must never see a real key.
15
+ */
16
+ /**
17
+ * Ed25519ph verification over an already computed prehash `ph` = SHA-512(M) (64 bytes), as Rekor and Go's
18
+ * `ed25519.VerifyWithOptions(pub, ph, sig, &ed25519.Options{Hash: crypto.SHA512})` take it. Never throws.
19
+ */
20
+ export declare function verifyEd25519ph(publicKey: Uint8Array, ph: Uint8Array, signature: Uint8Array): Promise<boolean>;
21
+ /** TEST FIXTURES ONLY: an Ed25519ph signature (RFC 8032 §5.1.6) by the 32-byte `seed` over the prehash `ph`. */
22
+ export declare function ed25519phSign(seed: Uint8Array, ph: Uint8Array): Promise<{
23
+ publicKey: Uint8Array;
24
+ signature: Uint8Array;
25
+ }>;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * @entpoind/verify — Ed25519ph (RFC 8032 §5.1, "HashEdDSA" with SHA-512) signature verification.
3
+ *
4
+ * Needed for one thing: the receipt ledger's public witness. Rekor accepts an Ed25519 key in a `hashedrekord` entry only as
5
+ * Ed25519ph, so the witness signature is Ed25519ph by the region's receipt key, and WebCrypto has no Ed25519ph. This is
6
+ * the RFC's verification procedure (§5.1.7) in BigInt arithmetic, with WebCrypto supplying SHA-512 and nothing else:
7
+ *
8
+ * k = SHA-512( dom2(1, "") ‖ R ‖ A ‖ PH(M) ) mod L, and accept iff encode([S]B − [k]A) == R
9
+ *
10
+ * the cofactorless check, with S < L and A and R decoded strictly (§5.1.3), as Go's crypto/ed25519 does. Tested against
11
+ * the RFC 8032 §7.3 vector and against signatures made by Go's crypto/ed25519.
12
+ *
13
+ * Verification only touches public values, so the arithmetic need not be constant-time. `ed25519phSign` exists for test
14
+ * fixtures; it is not exported from the package and must never see a real key.
15
+ */
16
+ const P = 2n ** 255n - 19n;
17
+ const L = 2n ** 252n + 27742317777372353535851937790883648493n;
18
+ const D = mod(-121665n * inv(121666n));
19
+ const SQRT_M1 = pow(2n, (P - 1n) / 4n);
20
+ const BASE = (() => {
21
+ const y = mod(4n * inv(5n));
22
+ const x = 15112221349535400772501151409588531511454012693041857206046113283949847762202n;
23
+ return { x, y, z: 1n, t: mod(x * y) };
24
+ })();
25
+ const IDENTITY = { x: 0n, y: 1n, z: 1n, t: 0n };
26
+ /** dom2(phflag = 1, context = ""), RFC 8032 §2. */
27
+ const DOM2_PH = new Uint8Array([...new TextEncoder().encode("SigEd25519 no Ed25519 collisions"), 1, 0]);
28
+ function mod(a, m = P) {
29
+ const r = a % m;
30
+ return r >= 0n ? r : r + m;
31
+ }
32
+ function pow(b, e) {
33
+ let r = 1n;
34
+ let x = mod(b);
35
+ while (e > 0n) {
36
+ if (e & 1n)
37
+ r = mod(r * x);
38
+ x = mod(x * x);
39
+ e >>= 1n;
40
+ }
41
+ return r;
42
+ }
43
+ function inv(a) {
44
+ return pow(a, P - 2n);
45
+ }
46
+ // RFC 8032 §5.1.4, extended coordinates; the addition is complete, so it doubles too.
47
+ function add(p, q) {
48
+ const a = mod((p.y - p.x) * (q.y - q.x));
49
+ const b = mod((p.y + p.x) * (q.y + q.x));
50
+ const c = mod(2n * D * p.t * q.t);
51
+ const d = mod(2n * p.z * q.z);
52
+ const e = b - a;
53
+ const f = d - c;
54
+ const g = d + c;
55
+ const h = b + a;
56
+ return { x: mod(e * f), y: mod(g * h), z: mod(f * g), t: mod(e * h) };
57
+ }
58
+ function mul(p, n) {
59
+ let r = IDENTITY;
60
+ let q = p;
61
+ while (n > 0n) {
62
+ if (n & 1n)
63
+ r = add(r, q);
64
+ q = add(q, q);
65
+ n >>= 1n;
66
+ }
67
+ return r;
68
+ }
69
+ function negate(p) {
70
+ return { x: mod(-p.x), y: p.y, z: p.z, t: mod(-p.t) };
71
+ }
72
+ function leInt(b) {
73
+ let n = 0n;
74
+ for (let i = b.length - 1; i >= 0; i--)
75
+ n = (n << 8n) | BigInt(b[i]);
76
+ return n;
77
+ }
78
+ function leBytes(n, len) {
79
+ const out = new Uint8Array(len);
80
+ for (let i = 0; i < len; i++) {
81
+ out[i] = Number(n & 0xffn);
82
+ n >>= 8n;
83
+ }
84
+ return out;
85
+ }
86
+ function encode(p) {
87
+ const zi = inv(p.z);
88
+ const x = mod(p.x * zi);
89
+ const y = mod(p.y * zi);
90
+ const out = leBytes(y, 32);
91
+ if (x & 1n)
92
+ out[31] = out[31] | 0x80;
93
+ return out;
94
+ }
95
+ /** RFC 8032 §5.1.3. Null for anything that is not the canonical encoding of a curve point. */
96
+ function decode(b) {
97
+ if (b.length !== 32)
98
+ return null;
99
+ const x0 = (b[31] & 0x80) !== 0;
100
+ const yb = b.slice();
101
+ yb[31] = yb[31] & 0x7f;
102
+ const y = leInt(yb);
103
+ if (y >= P)
104
+ return null;
105
+ const u = mod(y * y - 1n);
106
+ const v = mod(D * y * y + 1n);
107
+ let x = mod(u * pow(v, 3n) * pow(u * pow(v, 7n), (P - 5n) / 8n));
108
+ const vx2 = mod(v * x * x);
109
+ if (vx2 === u) {
110
+ /* x is a root */
111
+ }
112
+ else if (vx2 === mod(-u)) {
113
+ x = mod(x * SQRT_M1);
114
+ }
115
+ else {
116
+ return null;
117
+ }
118
+ if (x === 0n && x0)
119
+ return null;
120
+ if ((x & 1n) !== (x0 ? 1n : 0n))
121
+ x = mod(-x);
122
+ return { x, y, z: 1n, t: mod(x * y) };
123
+ }
124
+ async function sha512(...parts) {
125
+ const all = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
126
+ let off = 0;
127
+ for (const p of parts) {
128
+ all.set(p, off);
129
+ off += p.length;
130
+ }
131
+ return new Uint8Array(await crypto.subtle.digest("SHA-512", all));
132
+ }
133
+ const eq = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
134
+ /**
135
+ * Ed25519ph verification over an already computed prehash `ph` = SHA-512(M) (64 bytes), as Rekor and Go's
136
+ * `ed25519.VerifyWithOptions(pub, ph, sig, &ed25519.Options{Hash: crypto.SHA512})` take it. Never throws.
137
+ */
138
+ export async function verifyEd25519ph(publicKey, ph, signature) {
139
+ try {
140
+ if (publicKey.length !== 32 || ph.length !== 64 || signature.length !== 64)
141
+ return false;
142
+ const a = decode(publicKey);
143
+ if (!a)
144
+ return false;
145
+ const rBytes = signature.slice(0, 32);
146
+ const s = leInt(signature.slice(32));
147
+ if (s >= L)
148
+ return false;
149
+ const k = mod(leInt(await sha512(DOM2_PH, rBytes, publicKey, ph)), L);
150
+ return eq(encode(add(mul(BASE, s), mul(negate(a), k))), rBytes);
151
+ }
152
+ catch {
153
+ return false;
154
+ }
155
+ }
156
+ /** TEST FIXTURES ONLY: an Ed25519ph signature (RFC 8032 §5.1.6) by the 32-byte `seed` over the prehash `ph`. */
157
+ export async function ed25519phSign(seed, ph) {
158
+ const h = await sha512(seed);
159
+ const sb = h.slice(0, 32);
160
+ sb[0] = sb[0] & 248;
161
+ sb[31] = (sb[31] & 127) | 64;
162
+ const s = leInt(sb);
163
+ const publicKey = encode(mul(BASE, s));
164
+ const r = mod(leInt(await sha512(DOM2_PH, h.slice(32), ph)), L);
165
+ const rBytes = encode(mul(BASE, r));
166
+ const k = mod(leInt(await sha512(DOM2_PH, rBytes, publicKey, ph)), L);
167
+ const signature = new Uint8Array(64);
168
+ signature.set(rBytes, 0);
169
+ signature.set(leBytes(mod(r + k * s, L), 32), 32);
170
+ return { publicKey, signature };
171
+ }
package/dist/index.d.ts CHANGED
@@ -15,8 +15,11 @@
15
15
  * Deno, Bun, and Cloudflare Workers.
16
16
  */
17
17
  import { type MerkleProofStep } from "./merkle.js";
18
+ import { type LedgerWitness, type WitnessLogOptions, type WitnessReport } from "./witness.js";
18
19
  export { canonicalJcs, NonCanonicalizableError, type JsonValue } from "./canonical-json.js";
19
- export { merkleLeafHash, verifyMerkleInclusion, type MerkleProofStep } from "./merkle.js";
20
+ export { merkleLeafHash, verifyMerkleInclusion, verifyMerkleInclusionFromHash, type MerkleProofStep } from "./merkle.js";
21
+ export { verifyEd25519ph } from "./ed25519ph.js";
22
+ export { DEFAULT_REKOR_URL, WITNESS_KIND, type LedgerWitness, type WitnessLogCheck, type WitnessLogOptions, type WitnessLogProblem, type WitnessPart, type WitnessProblem, type WitnessReport, } from "./witness.js";
20
23
  /** A public verification key as published in the Tools JWK Set. */
21
24
  export type PublishedKey = {
22
25
  kid: string;
@@ -129,3 +132,91 @@ export declare function verifyBundle(input: unknown, publicKeys: ReadonlyArray<P
129
132
  * check" never reads as a verdict on the receipt.
130
133
  */
131
134
  export declare function verifyReceiptOnline(input: unknown, keysUrl?: string): Promise<ReceiptVerification>;
135
+ /** The envelope a ledger batch locks for one receipt: hashes and the signature, never the body. */
136
+ export type LedgerEnvelope = {
137
+ v: 1;
138
+ receipt_id: string;
139
+ kind: string;
140
+ receipt_hash: string;
141
+ signature: string;
142
+ signature_key_id: string;
143
+ issued_at: string;
144
+ region: string;
145
+ retain_until: string;
146
+ };
147
+ /** What `GET /api/v1/tools/receipts/{id}/ledger` answers (the 200 body). */
148
+ export type LedgerProof = {
149
+ kind: "entpoind.tools.receipt-ledger.proof";
150
+ v: 1;
151
+ receiptId: string;
152
+ batchId: string;
153
+ leafIndex: number;
154
+ treeSize: number;
155
+ leafHash: string;
156
+ /** The envelope that leaf hashes; null for a redacted receipt. */
157
+ envelope: LedgerEnvelope | null;
158
+ proof: MerkleProofStep[];
159
+ root: string;
160
+ rootSignature: string;
161
+ signatureKeyId: string;
162
+ region: string;
163
+ retentionClass: string;
164
+ bucketHour: string;
165
+ retainUntil: string;
166
+ anchoredAt: string;
167
+ s3: {
168
+ bucket: string | null;
169
+ key: string;
170
+ versionId: string | null;
171
+ };
172
+ /**
173
+ * The batch root's entry in the public Sigstore Rekor log; null (or absent, before LED-5) until it has been submitted;
174
+ * `{ status: "off" }` when the workspace had turned the public witness off before the receipt was batched.
175
+ */
176
+ witness?: LedgerWitness | null;
177
+ };
178
+ export type LedgerFailureReason = "malformed_proof" | "malformed_envelope" | "not_this_receipt" | "receipt_not_in_batch" | "proof_shape_mismatch" | "not_in_root" | "unknown_key" | "bad_root_signature" | "unsupported_runtime";
179
+ export type LedgerVerification = {
180
+ ok: true;
181
+ /** True when the proof was tied to the receipt you supplied (or its envelope was recomputed from it). False when only the leaf hash was folded. */
182
+ receiptBound: boolean;
183
+ batchId: string;
184
+ leafIndex: number;
185
+ treeSize: number;
186
+ root: string;
187
+ signatureKeyId: string;
188
+ /** When the compliance lock on the stored copy lapses. */
189
+ retainUntil: string;
190
+ /**
191
+ * The public witness, judged on its own: `absent` / `off` / `pending` / `malformed`, or `logged` with `consistent` and the exact
192
+ * `problems`. It never changes `ok`: the ledger proof above stands or falls without it.
193
+ */
194
+ witness: WitnessReport;
195
+ } | {
196
+ ok: false;
197
+ reason: LedgerFailureReason;
198
+ };
199
+ /** The bytes the region's receipt key signed over a batch root. */
200
+ export declare function ledgerRootMessage(p: Pick<LedgerProof, "region" | "batchId" | "retentionClass" | "bucketHour" | "treeSize" | "root" | "retainUntil">): string;
201
+ /**
202
+ * Verify a receipt-ledger proof offline. Pure public-key crypto: no account, no network, and no S3 access.
203
+ *
204
+ * 1. (with `receipt`) the envelope the ledger locked is RECOMPUTED from your saved receipt (its canonical form,
205
+ * hashed, with its signature and key id) and must be the leaf the proof starts from. Without a receipt the leaf is
206
+ * the envelope the proof carries, or just the leaf hash; `receiptBound` says which.
207
+ * 2. the sibling path has exactly the shape its leaf index and tree size require, and folds, leaf to root;
208
+ * 3. the root carries a valid signature by the region's receipt key (the key set that verifies your receipts), over a
209
+ * message that also names the batch, the tree size and the date the lock lapses.
210
+ *
211
+ * It proves the copy was FILED, under that root, by that key. That the object is still locked at S3 is S3's guarantee,
212
+ * checked there (GET object-retention on `s3.key` / `s3.versionId`) by someone holding read access to the bucket.
213
+ *
214
+ * Once the root verifies, the proof's `witness` (the root's entry in the public Rekor log) is checked too and reported in
215
+ * `witness` (see witness.ts): offline, that its hashes are the root message's, that it is by the root's key and its
216
+ * Ed25519ph signature verifies, and that the copy of the log's answer is that entry. With `witnessLog` (opt-in, the only
217
+ * network call here) the entry is also fetched from the log and checked there, with the log's own signatures.
218
+ */
219
+ export declare function verifyLedgerProof(input: unknown, publicKeys: ReadonlyArray<PublishedKey>, options?: {
220
+ receipt?: unknown;
221
+ witnessLog?: boolean | WitnessLogOptions;
222
+ }): Promise<LedgerVerification>;