@entpoind/verify 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Avonao, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,212 @@
1
- # Temporary Holding Version
1
+ # @entpoind/verify
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Offline verification of **Entpoind Tools** receipts and **VerifiableArtifact**
4
+ bundles. Zero dependencies; it uses only the runtime's own WebCrypto.
5
+
6
+ ```bash
7
+ npm install @entpoind/verify
8
+ ```
9
+
10
+ ```ts
11
+ import { verifyReceiptOnline } from "@entpoind/verify";
12
+
13
+ const result = await verifyReceiptOnline(receiptJson);
14
+ if (result.ok) {
15
+ console.log("signed at", result.signedAt, "by key", result.signatureKeyId);
16
+ } else {
17
+ console.log("not verified:", result.reason);
18
+ }
19
+ ```
20
+
21
+ `receiptJson` can be the receipt object, its JSON text, or the `{ "receipt": … }`
22
+ wrapper the API returns.
23
+
24
+ ## Where it runs
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
+
28
+ Any runtime whose WebCrypto verifies Ed25519: Node 20 or later (tested on
29
+ Node 22), Deno, Bun, Cloudflare Workers, and current browsers (Chrome and Edge
30
+ 137+, Firefox 129+, Safari 17+). Browsers expose WebCrypto only on secure
31
+ (`https:` or `localhost`) pages. Where Ed25519 is missing, verification returns
32
+ `unsupported_runtime` instead of judging the receipt; `ed25519Supported()` tells
33
+ you in advance.
34
+
35
+ ## What it verifies
36
+
37
+ - **Tools receipts**: the signed envelope every Tools call returns, of every
38
+ receipt `type` (`agent.call`, `content.attest`, `monitor.http`,
39
+ `heartbeat.attest`, …). Scheduled-watch digest receipts verify the same way.
40
+ - **VerifiableArtifact envelopes and bundles**: the signature, and, when a bundle
41
+ carries an `anchor`, that the artifact is included under the anchored Merkle
42
+ root.
43
+
44
+ The signature is Ed25519 over the **RFC 8785 (JCS)** canonical form of the
45
+ envelope minus `signature`. Public keys come from the published JWK Set at
46
+ `https://entpoind.com/.well-known/tools-keys.json`, which any origin may fetch
47
+ (`access-control-allow-origin: *`). For air-gapped checks, save that file and pass
48
+ its `keys` to `verifyReceipt`.
49
+
50
+ ## API
51
+
52
+ | Function | Purpose |
53
+ |---|---|
54
+ | `verifyReceipt(input, keys)` | Verify a receipt against keys you supply. No network. |
55
+ | `verifyReceiptOnline(input, url?)` | Fetch the published keys, then verify. |
56
+ | `fetchToolsKeys(url)` | Fetch the published JWK Set and keep its Ed25519 keys. |
57
+ | `verifyArtifact(input, keys)` | Verify a standalone artifact envelope. |
58
+ | `verifyBundle(input, keys)` | Signature, plus Merkle inclusion when anchored. |
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. |
62
+ | `canonicalJcs(value)` | The RFC 8785 canonical string. |
63
+ | `ed25519Supported()` | Whether this runtime can verify Ed25519. |
64
+ | `verifySignature(key, message, signature)` | The raw Ed25519 check, as a boolean. |
65
+
66
+ The `verify*` functions never throw on hostile input. `fetchToolsKeys` and
67
+ `verifyReceiptOnline` throw when the keys cannot be fetched, so "could not
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.
71
+
72
+ ## Failure reasons
73
+
74
+ | `reason` | Meaning |
75
+ |---|---|
76
+ | `malformed_envelope` | Not a complete receipt or artifact. |
77
+ | `unknown_key` | `signatureKeyId` matches no Ed25519 key in the set. Refresh the keys; if it is still missing, the document was not signed by Entpoind. |
78
+ | `bad_signature` | The signature does not match the canonical bytes: the document was altered, or is not Entpoind's. |
79
+ | `unsupported_runtime` | This runtime cannot verify Ed25519. Nothing was judged. |
80
+ | `not_in_anchored_root` | Bundles only: the signature is valid but the Merkle proof does not reach the anchored root. |
81
+
82
+ A bundle without an anchor verifies with `anchored: false`; that is a valid
83
+ result, not a failure.
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
+
205
+ ## Documentation
206
+
207
+ Receipt format, verification procedure and key rotation:
208
+ <https://entpoind.com/docs/verification>.
209
+
210
+ ## License
211
+
212
+ MIT © Avonao, Inc.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @entpoind/verify — RFC 8785 (JCS) canonicalization.
3
+ *
4
+ * Byte-identical port of the signer's canonicalizer
5
+ * (runtime/shared/canonical-json.ts). A receipt verifies only when the verifier
6
+ * re-derives the EXACT bytes the signature covers, so this file must never
7
+ * drift from that source: recursive key sort by UTF-16 code unit, ECMAScript
8
+ * Number→String formatting, JSON.stringify escaping, loud rejection of values
9
+ * two implementations couldn't serialize identically.
10
+ */
11
+ export type JsonValue = null | boolean | number | string | JsonValue[] | {
12
+ [key: string]: JsonValue;
13
+ };
14
+ export declare class NonCanonicalizableError extends Error {
15
+ readonly path: string;
16
+ constructor(message: string, path: string);
17
+ }
18
+ /** The RFC 8785 canonical string for `value` — the bytes a signature covers. */
19
+ export declare function canonicalJcs(value: unknown): string;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @entpoind/verify — RFC 8785 (JCS) canonicalization.
3
+ *
4
+ * Byte-identical port of the signer's canonicalizer
5
+ * (runtime/shared/canonical-json.ts). A receipt verifies only when the verifier
6
+ * re-derives the EXACT bytes the signature covers, so this file must never
7
+ * drift from that source: recursive key sort by UTF-16 code unit, ECMAScript
8
+ * Number→String formatting, JSON.stringify escaping, loud rejection of values
9
+ * two implementations couldn't serialize identically.
10
+ */
11
+ export class NonCanonicalizableError extends Error {
12
+ path;
13
+ constructor(message, path) {
14
+ super(`canonical-json: ${message} (at ${path || "<root>"})`);
15
+ this.path = path;
16
+ this.name = "NonCanonicalizableError";
17
+ }
18
+ }
19
+ function serializeValue(value, path) {
20
+ if (value === null)
21
+ return "null";
22
+ const t = typeof value;
23
+ if (t === "boolean" || t === "string")
24
+ return JSON.stringify(value);
25
+ if (t === "number") {
26
+ if (!Number.isFinite(value)) {
27
+ throw new NonCanonicalizableError(`non-finite number (${String(value)}) is not valid JSON`, path);
28
+ }
29
+ return JSON.stringify(value);
30
+ }
31
+ if (t === "bigint") {
32
+ throw new NonCanonicalizableError("bigint cannot be canonicalized (no JSON representation)", path);
33
+ }
34
+ if (t === "undefined" || t === "function" || t === "symbol") {
35
+ throw new NonCanonicalizableError(`${t} cannot be canonicalized`, path);
36
+ }
37
+ if (Array.isArray(value)) {
38
+ return `[${value.map((v, i) => serializeValue(v, `${path}[${i}]`)).join(",")}]`;
39
+ }
40
+ const proto = Object.getPrototypeOf(value);
41
+ if (proto !== Object.prototype && proto !== null) {
42
+ throw new NonCanonicalizableError(`non-plain object (${value.constructor?.name ?? "unknown"}) cannot be canonicalized; pass plain JSON`, path);
43
+ }
44
+ const obj = value;
45
+ // Written as text: an object would reorder integer-like keys and swallow a `__proto__` key.
46
+ const members = [];
47
+ for (const key of Object.keys(obj).sort()) {
48
+ const child = obj[key];
49
+ if (child === undefined)
50
+ continue;
51
+ members.push(`${JSON.stringify(key)}:${serializeValue(child, path ? `${path}.${key}` : key)}`);
52
+ }
53
+ return `{${members.join(",")}}`;
54
+ }
55
+ /** The RFC 8785 canonical string for `value` — the bytes a signature covers. */
56
+ export function canonicalJcs(value) {
57
+ return serializeValue(value, "");
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
+ }