leviathan-crypto 2.1.0 → 3.0.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.
Files changed (296) hide show
  1. package/CLAUDE.md +86 -443
  2. package/README.md +198 -65
  3. package/dist/aes/aes-cbc.d.ts +40 -0
  4. package/dist/aes/aes-cbc.js +158 -0
  5. package/dist/aes/aes-ctr.d.ts +50 -0
  6. package/dist/aes/aes-ctr.js +141 -0
  7. package/dist/aes/aes-gcm-siv.d.ts +67 -0
  8. package/dist/aes/aes-gcm-siv.js +217 -0
  9. package/dist/aes/aes-gcm.d.ts +61 -0
  10. package/dist/aes/aes-gcm.js +226 -0
  11. package/dist/aes/cipher-suite.d.ts +21 -0
  12. package/dist/aes/cipher-suite.js +179 -0
  13. package/dist/aes/embedded.d.ts +1 -0
  14. package/dist/aes/embedded.js +26 -0
  15. package/dist/aes/generator.d.ts +14 -0
  16. package/dist/aes/generator.js +103 -0
  17. package/dist/aes/index.d.ts +58 -0
  18. package/dist/aes/index.js +125 -0
  19. package/dist/aes/ops.d.ts +60 -0
  20. package/dist/aes/ops.js +164 -0
  21. package/dist/aes/pool-worker.d.ts +1 -0
  22. package/dist/aes/pool-worker.js +92 -0
  23. package/dist/aes/types.d.ts +1 -0
  24. package/dist/aes/types.js +23 -0
  25. package/dist/aes.wasm +0 -0
  26. package/dist/blake3/embedded.d.ts +1 -0
  27. package/dist/blake3/embedded.js +26 -0
  28. package/dist/blake3/index.d.ts +143 -0
  29. package/dist/blake3/index.js +620 -0
  30. package/dist/blake3/types.d.ts +102 -0
  31. package/dist/blake3/types.js +31 -0
  32. package/dist/blake3/validate.d.ts +29 -0
  33. package/dist/blake3/validate.js +80 -0
  34. package/dist/blake3.wasm +0 -0
  35. package/dist/chacha20/cipher-suite.js +47 -25
  36. package/dist/chacha20/generator.d.ts +2 -2
  37. package/dist/chacha20/generator.js +4 -4
  38. package/dist/chacha20/index.d.ts +16 -15
  39. package/dist/chacha20/index.js +52 -46
  40. package/dist/chacha20/ops.d.ts +7 -7
  41. package/dist/chacha20/ops.js +34 -34
  42. package/dist/chacha20/pool-worker.js +5 -3
  43. package/dist/cte-wasm.d.ts +1 -0
  44. package/dist/cte-wasm.js +3 -0
  45. package/dist/curve25519.wasm +0 -0
  46. package/dist/ecdsa/der.d.ts +23 -0
  47. package/dist/ecdsa/der.js +192 -0
  48. package/dist/ecdsa/ecprivatekey-der.d.ts +32 -0
  49. package/dist/ecdsa/ecprivatekey-der.js +230 -0
  50. package/dist/ecdsa/embedded.d.ts +1 -0
  51. package/dist/ecdsa/embedded.js +25 -0
  52. package/dist/ecdsa/index.d.ts +124 -0
  53. package/dist/ecdsa/index.js +366 -0
  54. package/dist/ecdsa/types.d.ts +31 -0
  55. package/dist/ecdsa/types.js +28 -0
  56. package/dist/ecdsa/validate.d.ts +18 -0
  57. package/dist/ecdsa/validate.js +92 -0
  58. package/dist/ed25519/embedded.d.ts +1 -0
  59. package/dist/ed25519/embedded.js +31 -0
  60. package/dist/ed25519/index.d.ts +70 -0
  61. package/dist/ed25519/index.js +308 -0
  62. package/dist/ed25519/types.d.ts +27 -0
  63. package/dist/ed25519/types.js +27 -0
  64. package/dist/ed25519/validate.d.ts +7 -0
  65. package/dist/ed25519/validate.js +77 -0
  66. package/dist/embedded/aes-pool-worker.d.ts +1 -0
  67. package/dist/embedded/aes-pool-worker.js +5 -0
  68. package/dist/embedded/aes.d.ts +1 -0
  69. package/dist/embedded/aes.js +3 -0
  70. package/dist/embedded/blake3.d.ts +1 -0
  71. package/dist/embedded/blake3.js +3 -0
  72. package/dist/embedded/chacha20-pool-worker.d.ts +1 -1
  73. package/dist/embedded/chacha20-pool-worker.js +2 -2
  74. package/dist/embedded/chacha20.d.ts +1 -1
  75. package/dist/embedded/chacha20.js +2 -2
  76. package/dist/embedded/curve25519.d.ts +1 -0
  77. package/dist/embedded/curve25519.js +3 -0
  78. package/dist/embedded/mldsa.d.ts +1 -0
  79. package/dist/embedded/mldsa.js +3 -0
  80. package/dist/embedded/mlkem.d.ts +1 -0
  81. package/dist/embedded/mlkem.js +3 -0
  82. package/dist/embedded/p256.d.ts +1 -0
  83. package/dist/embedded/p256.js +3 -0
  84. package/dist/embedded/serpent-pool-worker.d.ts +1 -1
  85. package/dist/embedded/serpent-pool-worker.js +2 -2
  86. package/dist/embedded/serpent.d.ts +1 -1
  87. package/dist/embedded/serpent.js +2 -2
  88. package/dist/embedded/sha2.d.ts +1 -1
  89. package/dist/embedded/sha2.js +2 -2
  90. package/dist/embedded/sha3.d.ts +1 -1
  91. package/dist/embedded/sha3.js +2 -2
  92. package/dist/embedded/slhdsa.d.ts +1 -0
  93. package/dist/embedded/slhdsa.js +3 -0
  94. package/dist/errors.d.ts +92 -1
  95. package/dist/errors.js +111 -1
  96. package/dist/fortuna.d.ts +5 -5
  97. package/dist/fortuna.js +37 -64
  98. package/dist/index.d.ts +38 -9
  99. package/dist/index.js +63 -19
  100. package/dist/init.d.ts +1 -1
  101. package/dist/init.js +11 -25
  102. package/dist/keccak/embedded.js +1 -1
  103. package/dist/keccak/index.d.ts +2 -0
  104. package/dist/keccak/index.js +4 -2
  105. package/dist/loader.d.ts +1 -24
  106. package/dist/loader.js +13 -16
  107. package/dist/merkle/blake3-tree.d.ts +35 -0
  108. package/dist/merkle/blake3-tree.js +187 -0
  109. package/dist/merkle/checkpoint.d.ts +58 -0
  110. package/dist/merkle/checkpoint.js +217 -0
  111. package/dist/merkle/index.d.ts +19 -0
  112. package/dist/merkle/index.js +37 -0
  113. package/dist/merkle/merkle-log.d.ts +130 -0
  114. package/dist/merkle/merkle-log.js +207 -0
  115. package/dist/merkle/merkle-verifier.d.ts +126 -0
  116. package/dist/merkle/merkle-verifier.js +296 -0
  117. package/dist/merkle/proof.d.ts +70 -0
  118. package/dist/merkle/proof.js +300 -0
  119. package/dist/merkle/sha256-tree.d.ts +33 -0
  120. package/dist/merkle/sha256-tree.js +145 -0
  121. package/dist/merkle/signed-log.d.ts +156 -0
  122. package/dist/merkle/signed-log.js +356 -0
  123. package/dist/merkle/signed-note.d.ts +309 -0
  124. package/dist/merkle/signed-note.js +648 -0
  125. package/dist/merkle/sth.d.ts +31 -0
  126. package/dist/merkle/sth.js +31 -0
  127. package/dist/merkle/storage.d.ts +40 -0
  128. package/dist/merkle/storage.js +71 -0
  129. package/dist/merkle/tree.d.ts +68 -0
  130. package/dist/merkle/tree.js +94 -0
  131. package/dist/mldsa/embedded.d.ts +1 -0
  132. package/dist/{kyber → mldsa}/embedded.js +5 -5
  133. package/dist/mldsa/expand.d.ts +53 -0
  134. package/dist/mldsa/expand.js +188 -0
  135. package/dist/mldsa/format.d.ts +16 -0
  136. package/dist/mldsa/format.js +68 -0
  137. package/dist/mldsa/hashvariant.d.ts +32 -0
  138. package/dist/mldsa/hashvariant.js +248 -0
  139. package/dist/mldsa/index.d.ts +142 -0
  140. package/dist/mldsa/index.js +463 -0
  141. package/dist/mldsa/keygen.d.ts +16 -0
  142. package/dist/mldsa/keygen.js +232 -0
  143. package/dist/mldsa/params.d.ts +21 -0
  144. package/dist/mldsa/params.js +55 -0
  145. package/dist/mldsa/sha3-helpers.d.ts +30 -0
  146. package/dist/mldsa/sha3-helpers.js +124 -0
  147. package/dist/mldsa/sign.d.ts +36 -0
  148. package/dist/mldsa/sign.js +380 -0
  149. package/dist/mldsa/types.d.ts +91 -0
  150. package/dist/mldsa/types.js +25 -0
  151. package/dist/mldsa/validate.d.ts +55 -0
  152. package/dist/mldsa/validate.js +125 -0
  153. package/dist/mldsa/verify.d.ts +29 -0
  154. package/dist/mldsa/verify.js +269 -0
  155. package/dist/mldsa.wasm +0 -0
  156. package/dist/mlkem/embedded.d.ts +1 -0
  157. package/dist/mlkem/embedded.js +27 -0
  158. package/dist/mlkem/indcpa.d.ts +49 -0
  159. package/dist/{kyber → mlkem}/indcpa.js +44 -44
  160. package/dist/mlkem/index.d.ts +37 -0
  161. package/dist/{kyber → mlkem}/index.js +24 -34
  162. package/dist/mlkem/kem.d.ts +21 -0
  163. package/dist/{kyber → mlkem}/kem.js +44 -64
  164. package/dist/{kyber → mlkem}/params.d.ts +4 -4
  165. package/dist/{kyber → mlkem}/params.js +2 -2
  166. package/dist/mlkem/suite.d.ts +12 -0
  167. package/dist/{kyber → mlkem}/suite.js +17 -12
  168. package/dist/{kyber → mlkem}/types.d.ts +3 -3
  169. package/dist/{kyber → mlkem}/types.js +1 -1
  170. package/dist/{kyber → mlkem}/validate.d.ts +7 -7
  171. package/dist/{kyber → mlkem}/validate.js +7 -7
  172. package/dist/{kyber.wasm → mlkem.wasm} +0 -0
  173. package/dist/p256.wasm +0 -0
  174. package/dist/ratchet/index.d.ts +2 -0
  175. package/dist/ratchet/index.js +1 -0
  176. package/dist/ratchet/kdf-chain.js +3 -3
  177. package/dist/ratchet/ratchet-keypair.js +2 -2
  178. package/dist/ratchet/root-kdf.js +7 -7
  179. package/dist/ratchet/skipped-key-store.js +4 -4
  180. package/dist/ratchet/types.d.ts +1 -1
  181. package/dist/serpent/cipher-suite.js +20 -17
  182. package/dist/serpent/generator.d.ts +1 -1
  183. package/dist/serpent/generator.js +2 -2
  184. package/dist/serpent/index.d.ts +8 -7
  185. package/dist/serpent/index.js +18 -27
  186. package/dist/serpent/pool-worker.js +7 -5
  187. package/dist/serpent/serpent-cbc.d.ts +4 -4
  188. package/dist/serpent/serpent-cbc.js +11 -8
  189. package/dist/serpent/shared-ops.d.ts +3 -23
  190. package/dist/serpent/shared-ops.js +50 -85
  191. package/dist/serpent.wasm +0 -0
  192. package/dist/sha2/hkdf.js +5 -5
  193. package/dist/sha2/index.d.ts +21 -1
  194. package/dist/sha2/index.js +65 -10
  195. package/dist/sha2/types.d.ts +41 -2
  196. package/dist/sha2.wasm +0 -0
  197. package/dist/sha3/index.d.ts +72 -3
  198. package/dist/sha3/index.js +240 -14
  199. package/dist/sha3/kmac.d.ts +121 -0
  200. package/dist/sha3/kmac.js +800 -0
  201. package/dist/sha3.wasm +0 -0
  202. package/dist/shared/pkcs7.d.ts +22 -0
  203. package/dist/shared/pkcs7.js +84 -0
  204. package/dist/sign/ctx.d.ts +41 -0
  205. package/dist/sign/ctx.js +102 -0
  206. package/dist/sign/envelope.d.ts +45 -0
  207. package/dist/sign/envelope.js +152 -0
  208. package/dist/sign/hasher.d.ts +9 -0
  209. package/dist/sign/hasher.js +132 -0
  210. package/dist/sign/index.d.ts +11 -0
  211. package/dist/sign/index.js +34 -0
  212. package/dist/sign/sign-stream.d.ts +25 -0
  213. package/dist/sign/sign-stream.js +112 -0
  214. package/dist/sign/suites/ecdsa-p256.d.ts +2 -0
  215. package/dist/sign/suites/ecdsa-p256.js +120 -0
  216. package/dist/sign/suites/ed25519.d.ts +3 -0
  217. package/dist/sign/suites/ed25519.js +165 -0
  218. package/dist/sign/suites/hybrid-classical.d.ts +23 -0
  219. package/dist/sign/suites/hybrid-classical.js +526 -0
  220. package/dist/sign/suites/hybrid-pq.d.ts +4 -0
  221. package/dist/sign/suites/hybrid-pq.js +234 -0
  222. package/dist/sign/suites/mldsa.d.ts +7 -0
  223. package/dist/sign/suites/mldsa.js +161 -0
  224. package/dist/sign/suites/slhdsa.d.ts +7 -0
  225. package/dist/sign/suites/slhdsa.js +176 -0
  226. package/dist/sign/types.d.ts +106 -0
  227. package/dist/sign/types.js +28 -0
  228. package/dist/sign/verify-stream.d.ts +30 -0
  229. package/dist/sign/verify-stream.js +227 -0
  230. package/dist/slhdsa/embedded.d.ts +1 -0
  231. package/dist/slhdsa/embedded.js +26 -0
  232. package/dist/slhdsa/index.d.ts +149 -0
  233. package/dist/slhdsa/index.js +493 -0
  234. package/dist/slhdsa/params.d.ts +26 -0
  235. package/dist/slhdsa/params.js +70 -0
  236. package/dist/slhdsa/prehash.d.ts +68 -0
  237. package/dist/slhdsa/prehash.js +307 -0
  238. package/dist/slhdsa/sign.d.ts +39 -0
  239. package/dist/slhdsa/sign.js +116 -0
  240. package/dist/slhdsa/types.d.ts +129 -0
  241. package/dist/slhdsa/types.js +27 -0
  242. package/dist/slhdsa/validate.d.ts +60 -0
  243. package/dist/slhdsa/validate.js +127 -0
  244. package/dist/slhdsa/verify.d.ts +32 -0
  245. package/dist/slhdsa/verify.js +107 -0
  246. package/dist/slhdsa.wasm +0 -0
  247. package/dist/stream/header.js +3 -3
  248. package/dist/stream/index.d.ts +1 -0
  249. package/dist/stream/index.js +1 -0
  250. package/dist/stream/open-stream.js +31 -10
  251. package/dist/stream/seal-stream-pool.d.ts +1 -0
  252. package/dist/stream/seal-stream-pool.js +63 -26
  253. package/dist/stream/seal-stream.d.ts +1 -1
  254. package/dist/stream/seal-stream.js +20 -9
  255. package/dist/stream/seal.js +6 -6
  256. package/dist/stream/types.d.ts +3 -1
  257. package/dist/stream/types.js +1 -1
  258. package/dist/types.d.ts +1 -1
  259. package/dist/types.js +1 -1
  260. package/dist/utils.d.ts +3 -3
  261. package/dist/utils.js +46 -54
  262. package/dist/wasm-source.d.ts +7 -7
  263. package/dist/wasm-source.js +1 -1
  264. package/dist/x25519/embedded.d.ts +1 -0
  265. package/dist/x25519/embedded.js +31 -0
  266. package/dist/x25519/index.d.ts +43 -0
  267. package/dist/x25519/index.js +159 -0
  268. package/dist/x25519/types.d.ts +25 -0
  269. package/dist/x25519/types.js +27 -0
  270. package/dist/x25519/validate.d.ts +2 -0
  271. package/dist/x25519/validate.js +39 -0
  272. package/package.json +70 -26
  273. package/SECURITY.md +0 -163
  274. package/dist/ct-wasm.d.ts +0 -1
  275. package/dist/ct-wasm.js +0 -3
  276. package/dist/docs/aead.md +0 -363
  277. package/dist/docs/architecture.md +0 -1011
  278. package/dist/docs/argon2id.md +0 -305
  279. package/dist/docs/chacha20.md +0 -781
  280. package/dist/docs/exports.md +0 -277
  281. package/dist/docs/fortuna.md +0 -530
  282. package/dist/docs/init.md +0 -301
  283. package/dist/docs/loader.md +0 -256
  284. package/dist/docs/serpent.md +0 -617
  285. package/dist/docs/sha2.md +0 -671
  286. package/dist/docs/sha3.md +0 -612
  287. package/dist/docs/types.md +0 -416
  288. package/dist/docs/utils.md +0 -457
  289. package/dist/embedded/kyber.d.ts +0 -1
  290. package/dist/embedded/kyber.js +0 -3
  291. package/dist/kyber/embedded.d.ts +0 -1
  292. package/dist/kyber/indcpa.d.ts +0 -49
  293. package/dist/kyber/index.d.ts +0 -38
  294. package/dist/kyber/kem.d.ts +0 -21
  295. package/dist/kyber/suite.d.ts +0 -12
  296. /package/dist/{ct.wasm → cte.wasm} +0 -0
@@ -0,0 +1,296 @@
1
+ // ▄▄▄▄▄▄▄▄▄▄
2
+ // ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
3
+ // ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
4
+ // ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
5
+ // ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
6
+ // ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
7
+ // ███████▌ ▀██▀ ███
8
+ // ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
9
+ // ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
10
+ // ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
11
+ // ▀████▄ ▄██▄
12
+ // ▐████ ▐███ Author: xero (https://x-e.ro)
13
+ // ▄▄██████████ ▐███ ▄▄ License: MIT
14
+ // ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
15
+ // ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
16
+ // ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀ free, "as is", and without
17
+ // ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
18
+ // █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
19
+ // ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
20
+ // ▀█████▀▀
21
+ //
22
+ // src/ts/merkle/merkle-verifier.ts
23
+ //
24
+ // `MerkleVerifier`, verify-only normie surface. Wire format per
25
+ // c2sp.org/signed-note §Format, c2sp.org/tlog-checkpoint §Note text,
26
+ // and c2sp.org/tlog-cosignature §Format.
27
+ import { isInitialized } from '../init.js';
28
+ import { MerkleLogError, MerkleCodecError } from '../errors.js';
29
+ import { constantTimeEqual } from '../utils.js';
30
+ import { parseSignedNote, lookupAlgoEntryByFormatEnum, deriveKeyId, buildCosigSignedMessage, buildCosignedMessage, parseCosigSignaturePayload, } from './signed-note.js';
31
+ import { parseCheckpointBody } from './checkpoint.js';
32
+ import { verifyInclusionProof, verifyConsistencyProof } from './proof.js';
33
+ import { Sha256Hasher } from './sha256-tree.js';
34
+ import { Blake3Hasher } from './blake3-tree.js';
35
+ // Empty ctx for suite.verify; domain separation lives in the
36
+ // cosignature signed-message construction (cosignature/v1 prefix for
37
+ // Ed25519, cosigned_message label for ML-DSA-44) per
38
+ // c2sp.org/tlog-cosignature §Format.
39
+ const EMPTY_CTX = new Uint8Array(0);
40
+ const SHA2_MODULE = 'sha2';
41
+ /**
42
+ * Trust-anchored verifier for c2sp.org/tlog-checkpoint envelopes.
43
+ * Takes a fixed log identity at construction and exposes three verify
44
+ * methods (`verifyCheckpoint`, `verifyInclusion`, `verifyConsistency`)
45
+ * that return `boolean`.
46
+ *
47
+ * Construction is the only place this class throws; every verify path
48
+ * returns `false` on any failure mode including malformed bytes,
49
+ * tampered envelopes, wrong origin, wrong leaf, and signature failure.
50
+ * The convention matches `SignatureSuite.verify` and lets normie
51
+ * callers write a single `if (!verifier.verifyX(...)) reject()` line
52
+ * per check without a try / catch.
53
+ */
54
+ export class MerkleVerifier {
55
+ origin;
56
+ pubkey;
57
+ hasher;
58
+ suite;
59
+ _algoEntry;
60
+ _keyId;
61
+ constructor(opts) {
62
+ const { origin, pubkey, hashing, suite } = opts;
63
+ if (typeof origin !== 'string' || origin.length === 0)
64
+ throw new MerkleLogError('origin-invalid', 'MerkleVerifier: origin must be a non-empty string');
65
+ // c2sp.org/tlog-checkpoint §Note text MUSTs, mirrored from
66
+ // `SignedLog`'s constructor: the origin is the first body line
67
+ // and may not contain whitespace or plus characters.
68
+ if (/\s/.test(origin) || origin.includes('+'))
69
+ throw new MerkleLogError('origin-invalid', 'MerkleVerifier: origin must not contain whitespace or plus characters');
70
+ if (!(pubkey instanceof Uint8Array))
71
+ throw new MerkleLogError('pubkey-size', 'MerkleVerifier: pubkey must be a Uint8Array');
72
+ const hasher = resolveHasher(hashing);
73
+ const algoEntry = lookupAlgoEntryByFormatEnum(suite.formatEnum);
74
+ if (algoEntry === undefined)
75
+ throw new MerkleLogError('unsupported-suite', `MerkleVerifier: suite '${suite.formatName}' (formatEnum 0x${suite.formatEnum
76
+ .toString(16)
77
+ .padStart(2, '0')}) has no c2sp.org/tlog-cosignature §Format algorithm byte; `
78
+ + 'use Ed25519Suite or MlDsa44Suite, or open an issue for a newly C2SP-registered suite');
79
+ if (pubkey.length !== suite.pkSize)
80
+ throw new MerkleLogError('pubkey-size', `MerkleVerifier: pubkey length ${pubkey.length} != suite.pkSize ${suite.pkSize}`);
81
+ // Same modules `SignedLog` requires: the suite's modules, the
82
+ // hasher's module, and sha2 for `deriveKeyId`. Constructor-time
83
+ // check so a verifier built before `init()` fails at construction
84
+ // rather than on first `verifyCheckpoint` call.
85
+ assertModulesInitialized([
86
+ ...suite.wasmModules,
87
+ ...hasher.wasmModules,
88
+ SHA2_MODULE,
89
+ ]);
90
+ this.origin = origin;
91
+ this.pubkey = pubkey.slice();
92
+ this.hasher = hasher;
93
+ this.suite = suite;
94
+ this._algoEntry = algoEntry;
95
+ this._keyId = deriveKeyId(origin, algoEntry.algoByte, this.pubkey);
96
+ }
97
+ /**
98
+ * Verify a signed-note envelope against this verifier's identity.
99
+ * Returns `true` iff the envelope parses, the body's origin equals
100
+ * the constructor origin, the body's root-hash length equals the
101
+ * hasher's `outputSize`, a signature line's keyId equals the
102
+ * pubkey-derived keyId, the `timestamped_signature` payload on
103
+ * that line decodes cleanly, and `suite.verify` accepts the
104
+ * reconstructed cosignature signed message.
105
+ *
106
+ * Returns `false` on every other path. Never throws on envelope
107
+ * content.
108
+ */
109
+ verifyCheckpoint(envelopeBytes) {
110
+ const parsed = this._parseAndVerify(envelopeBytes);
111
+ return parsed !== null;
112
+ }
113
+ /**
114
+ * Verify a leaf's inclusion in the tree committed by an envelope.
115
+ * Runs `verifyCheckpoint` first; on failure returns `false`
116
+ * without examining the proof. On success, hashes `leafBytes`
117
+ * with the verifier's `Hasher` and calls `verifyInclusionProof`
118
+ * against the body's `treeSize` and `rootHash` per RFC 9162 §2.1.3.
119
+ *
120
+ * The "verify checkpoint first" ordering is the security-critical
121
+ * step: the proof is bound to the root hash inside the signed body,
122
+ * so trusting the proof before checking the signature would let any
123
+ * forger pair a malicious proof with their own root.
124
+ */
125
+ verifyInclusion(opts) {
126
+ const parsed = this._parseAndVerify(opts.envelopeBytes);
127
+ if (parsed === null)
128
+ return false;
129
+ if (!(opts.leafBytes instanceof Uint8Array))
130
+ return false;
131
+ if (!Number.isInteger(opts.leafIndex) || opts.leafIndex < 0)
132
+ return false;
133
+ if (opts.leafIndex >= parsed.treeSize)
134
+ return false;
135
+ if (!Array.isArray(opts.proof))
136
+ return false;
137
+ for (const h of opts.proof)
138
+ if (!(h instanceof Uint8Array))
139
+ return false;
140
+ // RFC 9162 §2.1.1: leaf-hash domain separation happens here;
141
+ // the proof verifier expects the MTH({d}) of the leaf, not the
142
+ // raw leaf bytes. Computing it locally rather than accepting a
143
+ // caller-supplied leaf hash closes the "we trust the proof
144
+ // because we trust the leaf hash the caller gave us" gap.
145
+ const leafHash = this.hasher.hashLeaf(opts.leafBytes);
146
+ try {
147
+ return verifyInclusionProof({
148
+ hasher: this.hasher,
149
+ leafHash,
150
+ leafIndex: opts.leafIndex,
151
+ treeSize: parsed.treeSize,
152
+ proof: opts.proof,
153
+ rootHash: parsed.rootHash,
154
+ });
155
+ }
156
+ catch {
157
+ // `verifyInclusionProof` throws on a wrong-sized rootHash or
158
+ // out-of-range leafIndex. Convert to a verify-false: the
159
+ // normie surface keeps a single failure mode.
160
+ return false;
161
+ }
162
+ }
163
+ /**
164
+ * Verify that the tree committed by `oldEnvelopeBytes` is a prefix
165
+ * of the tree committed by `newEnvelopeBytes`. Both envelopes must
166
+ * verify under this verifier's identity; if either fails, returns
167
+ * `false`. On success, calls `verifyConsistencyProof` per
168
+ * RFC 9162 §2.1.4 against the two sizes and roots.
169
+ */
170
+ verifyConsistency(opts) {
171
+ const oldParsed = this._parseAndVerify(opts.oldEnvelopeBytes);
172
+ if (oldParsed === null)
173
+ return false;
174
+ const newParsed = this._parseAndVerify(opts.newEnvelopeBytes);
175
+ if (newParsed === null)
176
+ return false;
177
+ if (!Array.isArray(opts.proof))
178
+ return false;
179
+ for (const h of opts.proof)
180
+ if (!(h instanceof Uint8Array))
181
+ return false;
182
+ try {
183
+ return verifyConsistencyProof({
184
+ hasher: this.hasher,
185
+ oldSize: oldParsed.treeSize,
186
+ newSize: newParsed.treeSize,
187
+ oldRoot: oldParsed.rootHash,
188
+ newRoot: newParsed.rootHash,
189
+ proof: opts.proof,
190
+ });
191
+ }
192
+ catch {
193
+ return false;
194
+ }
195
+ }
196
+ // ── internal ────────────────────────────────────────────────────────
197
+ /**
198
+ * Parse a signed-note envelope, verify the cosignature, and return
199
+ * the decoded `Checkpoint`. Returns `null` on any failure mode:
200
+ * malformed envelope, malformed body, wrong origin, wrong root-hash
201
+ * length, no matching keyId line, malformed payload, signature
202
+ * failure. Keyed-ID comparison uses `constantTimeEqual` for hygiene
203
+ * around key-material-adjacent state.
204
+ */
205
+ _parseAndVerify(bytes) {
206
+ if (!(bytes instanceof Uint8Array))
207
+ return null;
208
+ let env;
209
+ try {
210
+ env = parseSignedNote(bytes);
211
+ }
212
+ catch {
213
+ return null;
214
+ }
215
+ let checkpoint;
216
+ try {
217
+ checkpoint = parseCheckpointBody(env.body, this.hasher.outputSize);
218
+ }
219
+ catch {
220
+ return null;
221
+ }
222
+ if (checkpoint.origin !== this.origin)
223
+ return null;
224
+ if (checkpoint.rootHash.length !== this.hasher.outputSize)
225
+ return null;
226
+ const matching = env.signatures.find(s => s.keyId.length === this._keyId.length
227
+ && constantTimeEqual(s.keyId, this._keyId));
228
+ if (!matching)
229
+ return null;
230
+ let payload;
231
+ try {
232
+ payload = parseCosigSignaturePayload(matching.signature, this._algoEntry.sigSize);
233
+ }
234
+ catch (err) {
235
+ if (err instanceof MerkleCodecError)
236
+ return null;
237
+ throw err;
238
+ }
239
+ let signedMessage;
240
+ try {
241
+ signedMessage = this._buildSignedMessage(env.body, payload.timestamp, checkpoint);
242
+ }
243
+ catch {
244
+ return null;
245
+ }
246
+ const ok = this.suite.verify(this.pubkey, signedMessage, payload.signature, EMPTY_CTX);
247
+ return ok ? checkpoint : null;
248
+ }
249
+ /**
250
+ * Dispatch cosignature signed-message construction on the algorithm
251
+ * registry entry's `messageConstruction`. Mirrors `SignedLog`'s
252
+ * dispatch so producer and verifier always agree on the bytes the
253
+ * suite verifies against.
254
+ *
255
+ * 'cosig' c2sp.org/tlog-cosignature §"Ed25519 signed
256
+ * message". The full envelope body is embedded
257
+ * verbatim after the cosignature/v1 + time
258
+ * prefix.
259
+ *
260
+ * 'cosigned-message' c2sp.org/tlog-cosignature §"ML-DSA-44
261
+ * signed message". cosigner_name and
262
+ * log_origin both equal the checkpoint origin
263
+ * for a log's self-cosignature; start == 0;
264
+ * end == treeSize; hash == rootHash.
265
+ */
266
+ _buildSignedMessage(body, timestamp, cp) {
267
+ if (this._algoEntry.messageConstruction === 'cosig')
268
+ return buildCosigSignedMessage(body, timestamp);
269
+ return buildCosignedMessage({
270
+ cosignerName: this.origin,
271
+ timestamp,
272
+ logOrigin: this.origin,
273
+ start: 0,
274
+ end: cp.treeSize,
275
+ hash: cp.rootHash,
276
+ });
277
+ }
278
+ }
279
+ function resolveHasher(hashing) {
280
+ if (hashing === 'sha256')
281
+ return Sha256Hasher;
282
+ if (hashing === 'blake3')
283
+ return Blake3Hasher;
284
+ throw new MerkleLogError('unsupported-hashing', `MerkleVerifier: hashing must be 'sha256' or 'blake3', got '${hashing}'`);
285
+ }
286
+ function assertModulesInitialized(modules) {
287
+ const seen = new Set();
288
+ for (const mod of modules) {
289
+ if (seen.has(mod))
290
+ continue;
291
+ seen.add(mod);
292
+ if (!isInitialized(mod))
293
+ throw new MerkleLogError('module-not-initialized', `MerkleVerifier: WASM module '${mod}' is not initialized; `
294
+ + 'call init() with the appropriate sources before constructing MerkleVerifier');
295
+ }
296
+ }
@@ -0,0 +1,70 @@
1
+ import type { Hasher } from './tree.js';
2
+ export interface VerifyInclusionInput {
3
+ hasher: Hasher;
4
+ leafHash: Uint8Array;
5
+ leafIndex: number;
6
+ treeSize: number;
7
+ proof: readonly Uint8Array[];
8
+ rootHash: Uint8Array;
9
+ }
10
+ /**
11
+ * RFC 9162 §2.1.3, Inclusion Proof Verification. Returns true if the
12
+ * proof reconstructs `rootHash` from `leafHash` at position
13
+ * (leafIndex, treeSize). Wrong proof length, wrong leaf-hash size, or
14
+ * a reconstructed root that differs from `rootHash` all return false.
15
+ * Contract violations (negative or out-of-range index, treeSize <= 0,
16
+ * wrong-sized rootHash) throw RangeError.
17
+ *
18
+ * `leafHash` is the leaf's MTH ({d_m} hashed under the leaf prefix), not
19
+ * the raw leaf bytes. Thin verifiers receiving a leaf over the wire
20
+ * should compute `hasher.hashLeaf(bytes)` before calling.
21
+ */
22
+ export declare function verifyInclusionProof(input: VerifyInclusionInput): boolean;
23
+ export interface VerifyConsistencyInput {
24
+ hasher: Hasher;
25
+ oldSize: number;
26
+ newSize: number;
27
+ oldRoot: Uint8Array;
28
+ newRoot: Uint8Array;
29
+ proof: readonly Uint8Array[];
30
+ }
31
+ /**
32
+ * RFC 9162 §2.1.4, Consistency Proof Verification. Returns true if
33
+ * `proof` proves that the size-`oldSize` tree with root `oldRoot` is a
34
+ * prefix of the size-`newSize` tree with root `newRoot`.
35
+ *
36
+ * Malformed-proof conditions (wrong proof length, non-empty proof when
37
+ * one is forbidden, mismatched old/new root reconstruction) return
38
+ * false. Contract violations (`oldSize > newSize`, wrong-sized root)
39
+ * throw RangeError; the special "consistency from empty tree" form is
40
+ * not part of the wire format and returns false.
41
+ */
42
+ export declare function verifyConsistencyProof(input: VerifyConsistencyInput): boolean;
43
+ /** Callback the builders use to read the tree without knowing how it is stored. */
44
+ export type GetNode = (level: number, index: number) => Uint8Array;
45
+ export interface BuildInclusionInput {
46
+ hasher: Hasher;
47
+ leafIndex: number;
48
+ treeSize: number;
49
+ getNode: GetNode;
50
+ }
51
+ /**
52
+ * RFC 9162 §2.1.3: build the inclusion proof for leaf `leafIndex` in
53
+ * a tree of size `treeSize`. The returned bytes are ordered from the
54
+ * lowest level upward (leaf sibling first, root-adjacent last), the
55
+ * order `verifyInclusionProof` consumes.
56
+ */
57
+ export declare function buildInclusionProof(input: BuildInclusionInput): Uint8Array[];
58
+ export interface BuildConsistencyInput {
59
+ hasher: Hasher;
60
+ oldSize: number;
61
+ newSize: number;
62
+ getNode: GetNode;
63
+ }
64
+ /**
65
+ * RFC 9162 §2.1.4: build the consistency proof between two tree
66
+ * sizes. Returns an empty array when oldSize equals newSize or
67
+ * oldSize is zero (the verifier rejects the latter, but the builder
68
+ * is symmetric for inspection-time use).
69
+ */
70
+ export declare function buildConsistencyProof(input: BuildConsistencyInput): Uint8Array[];
@@ -0,0 +1,300 @@
1
+ // ▄▄▄▄▄▄▄▄▄▄
2
+ // ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
3
+ // ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
4
+ // ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
5
+ // ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
6
+ // ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
7
+ // ███████▌ ▀██▀ ███
8
+ // ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
9
+ // ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
10
+ // ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
11
+ // ▀████▄ ▄██▄
12
+ // ▐████ ▐███ Author: xero (https://x-e.ro)
13
+ // ▄▄██████████ ▐███ ▄▄ License: MIT
14
+ // ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
15
+ // ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
16
+ // ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀ free, "as is", and without
17
+ // ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
18
+ // █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
19
+ // ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
20
+ // ▀█████▀▀
21
+ //
22
+ // src/ts/merkle/proof.ts
23
+ //
24
+ // Hash-agnostic, free-function proof verifiers and builders for the
25
+ // RFC 9162 (Certificate Transparency Version 2.0) §2.1.3 / §2.1.4
26
+ // proof formats. Every entry point takes a `Hasher`; SHA-256 and
27
+ // BLAKE3 trees share the same wire format and the same algorithmic
28
+ // core.
29
+ //
30
+ // Verifiers return boolean. Malformed-proof conditions (wrong inner /
31
+ // border length, mismatched root) return false. Contract violations
32
+ // (wrong-sized root for the hasher, leafIndex out of range, oldSize >
33
+ // newSize) throw RangeError; the caller is responsible for staying
34
+ // within the public contract.
35
+ //
36
+ // Builders accept a `getNode(level, index)` callback that abstracts
37
+ // the storage layer. Memory, file, and database backends drive the
38
+ // same builder without bringing storage details into the proof
39
+ // algorithms.
40
+ import { bitLen, popcount, splitPoint, trailingZeros } from './tree.js';
41
+ import { constantTimeEqual } from '../utils.js';
42
+ // ── Internal chaining primitives (RFC 9162 §2.1.3 / §2.1.4) ─────────────────
43
+ /**
44
+ * Decompose an inclusion proof into its inner (path-up-the-tree) and
45
+ * border (left siblings completing the right edge) segments. The sum
46
+ * inner + border is the required proof length.
47
+ *
48
+ * RFC 9162 §2.1.3, Inclusion Proof Verification: the path from a leaf
49
+ * at index to the root of a size-n tree has bitLen(index XOR (size-1))
50
+ * inner levels and popcount(index >> inner) border levels.
51
+ */
52
+ function decompInclProof(index, size) {
53
+ const inner = bitLen(index ^ (size - 1));
54
+ const border = popcount(Math.floor(index / 2 ** inner));
55
+ return { inner, border };
56
+ }
57
+ /**
58
+ * Chain `inner` proof entries up from `seed`. At level i, the bit
59
+ * (index >> i) & 1 selects whether the sibling is on the left or the
60
+ * right of `seed`. RFC 9162 §2.1.3.
61
+ */
62
+ function chainInner(hasher, seed, proof, index) {
63
+ let acc = seed;
64
+ for (let i = 0; i < proof.length; i++) {
65
+ const bit = Math.floor(index / 2 ** i) & 1;
66
+ acc = bit === 0
67
+ ? hasher.hashInternal(acc, proof[i])
68
+ : hasher.hashInternal(proof[i], acc);
69
+ }
70
+ return acc;
71
+ }
72
+ /**
73
+ * Chain `inner` entries but only fold in left siblings (skip the right
74
+ * ones). Used by the consistency verifier to reconstruct the OLD root
75
+ * from the suffix shared with the inclusion proof. RFC 9162 §2.1.4.
76
+ */
77
+ function chainInnerRight(hasher, seed, proof, index) {
78
+ let acc = seed;
79
+ for (let i = 0; i < proof.length; i++) {
80
+ const bit = Math.floor(index / 2 ** i) & 1;
81
+ if (bit === 1)
82
+ acc = hasher.hashInternal(proof[i], acc);
83
+ }
84
+ return acc;
85
+ }
86
+ /**
87
+ * Chain border entries: every remaining sibling is a left sibling
88
+ * along the size-1 path back to the root. RFC 9162 §2.1.3.
89
+ */
90
+ function chainBorderRight(hasher, seed, proof) {
91
+ let acc = seed;
92
+ for (const h of proof)
93
+ acc = hasher.hashInternal(h, acc);
94
+ return acc;
95
+ }
96
+ function assertHashLen(hasher, label, h) {
97
+ if (h.length !== hasher.outputSize)
98
+ throw new RangeError(`${label}: wrong length ${h.length}, expected ${hasher.outputSize} for ${hasher.name}`);
99
+ }
100
+ /**
101
+ * RFC 9162 §2.1.3, Inclusion Proof Verification. Returns true if the
102
+ * proof reconstructs `rootHash` from `leafHash` at position
103
+ * (leafIndex, treeSize). Wrong proof length, wrong leaf-hash size, or
104
+ * a reconstructed root that differs from `rootHash` all return false.
105
+ * Contract violations (negative or out-of-range index, treeSize <= 0,
106
+ * wrong-sized rootHash) throw RangeError.
107
+ *
108
+ * `leafHash` is the leaf's MTH ({d_m} hashed under the leaf prefix), not
109
+ * the raw leaf bytes. Thin verifiers receiving a leaf over the wire
110
+ * should compute `hasher.hashLeaf(bytes)` before calling.
111
+ */
112
+ export function verifyInclusionProof(input) {
113
+ const { hasher, leafHash, leafIndex, treeSize, proof, rootHash } = input;
114
+ if (!Number.isInteger(leafIndex) || leafIndex < 0)
115
+ throw new RangeError(`verifyInclusionProof: leafIndex must be a non-negative integer, got ${leafIndex}`);
116
+ if (!Number.isInteger(treeSize) || treeSize < 1)
117
+ throw new RangeError(`verifyInclusionProof: treeSize must be a positive integer, got ${treeSize}`);
118
+ if (leafIndex >= treeSize)
119
+ throw new RangeError(`verifyInclusionProof: leafIndex ${leafIndex} >= treeSize ${treeSize}`);
120
+ assertHashLen(hasher, 'verifyInclusionProof: rootHash', rootHash);
121
+ if (leafHash.length !== hasher.outputSize)
122
+ return false;
123
+ const { inner, border } = decompInclProof(leafIndex, treeSize);
124
+ if (proof.length !== inner + border)
125
+ return false;
126
+ for (const h of proof) {
127
+ if (h.length !== hasher.outputSize)
128
+ return false;
129
+ }
130
+ const innerProof = proof.slice(0, inner);
131
+ const borderProof = proof.slice(inner);
132
+ let res = chainInner(hasher, leafHash, innerProof, leafIndex);
133
+ res = chainBorderRight(hasher, res, borderProof);
134
+ return constantTimeEqual(res, rootHash);
135
+ }
136
+ /**
137
+ * RFC 9162 §2.1.4, Consistency Proof Verification. Returns true if
138
+ * `proof` proves that the size-`oldSize` tree with root `oldRoot` is a
139
+ * prefix of the size-`newSize` tree with root `newRoot`.
140
+ *
141
+ * Malformed-proof conditions (wrong proof length, non-empty proof when
142
+ * one is forbidden, mismatched old/new root reconstruction) return
143
+ * false. Contract violations (`oldSize > newSize`, wrong-sized root)
144
+ * throw RangeError; the special "consistency from empty tree" form is
145
+ * not part of the wire format and returns false.
146
+ */
147
+ export function verifyConsistencyProof(input) {
148
+ const { hasher, oldSize, newSize, oldRoot, newRoot, proof } = input;
149
+ if (!Number.isInteger(oldSize) || oldSize < 0)
150
+ throw new RangeError(`verifyConsistencyProof: oldSize must be a non-negative integer, got ${oldSize}`);
151
+ if (!Number.isInteger(newSize) || newSize < 0)
152
+ throw new RangeError(`verifyConsistencyProof: newSize must be a non-negative integer, got ${newSize}`);
153
+ if (oldSize > newSize)
154
+ throw new RangeError(`verifyConsistencyProof: oldSize ${oldSize} > newSize ${newSize}`);
155
+ // Equal-size shortcut: RFC says the proof is empty and roots match.
156
+ // Byte-for-byte comparison; root hashes flow through unchanged because
157
+ // no reconstruction runs, so hash-length validation does not apply.
158
+ if (oldSize === newSize) {
159
+ if (proof.length > 0)
160
+ return false;
161
+ return oldRoot.length === newRoot.length && constantTimeEqual(oldRoot, newRoot);
162
+ }
163
+ // "Consistency from empty tree" is undefined: the verifier cannot
164
+ // recover oldRoot from no proof, so reject as malformed.
165
+ if (oldSize === 0)
166
+ return false;
167
+ if (proof.length === 0)
168
+ return false;
169
+ assertHashLen(hasher, 'verifyConsistencyProof: oldRoot', oldRoot);
170
+ assertHashLen(hasher, 'verifyConsistencyProof: newRoot', newRoot);
171
+ for (const h of proof) {
172
+ if (h.length !== hasher.outputSize)
173
+ return false;
174
+ }
175
+ const { inner: innerFull, border } = decompInclProof(oldSize - 1, newSize);
176
+ const shift = trailingZeros(oldSize);
177
+ const inner = innerFull - shift;
178
+ // If oldSize is a power of two, the verifier already knows the
179
+ // subtree's root (== oldRoot) and the proof omits it. Otherwise the
180
+ // proof's first element is the seed for both chains.
181
+ const oldIsPow2 = oldSize === 2 ** shift;
182
+ let seed;
183
+ let start;
184
+ if (oldIsPow2) {
185
+ seed = oldRoot;
186
+ start = 0;
187
+ }
188
+ else {
189
+ seed = proof[0];
190
+ start = 1;
191
+ }
192
+ const expectedLen = start + inner + border;
193
+ if (proof.length !== expectedLen)
194
+ return false;
195
+ const tail = proof.slice(start);
196
+ const innerProof = tail.slice(0, inner);
197
+ const borderProof = tail.slice(inner);
198
+ // Bit pattern for chainInnerRight: we re-derive the oldRoot from
199
+ // the proof. `mask` is (oldSize - 1) >> shift, the path bits above
200
+ // the size-`oldSize` subtree's root level.
201
+ const mask = Math.floor((oldSize - 1) / 2 ** shift);
202
+ let hash1 = chainInnerRight(hasher, seed, innerProof, mask);
203
+ hash1 = chainBorderRight(hasher, hash1, borderProof);
204
+ if (!constantTimeEqual(hash1, oldRoot))
205
+ return false;
206
+ let hash2 = chainInner(hasher, seed, innerProof, mask);
207
+ hash2 = chainBorderRight(hasher, hash2, borderProof);
208
+ return constantTimeEqual(hash2, newRoot);
209
+ }
210
+ /**
211
+ * RFC 9162 §2.1.3: build the inclusion proof for leaf `leafIndex` in
212
+ * a tree of size `treeSize`. The returned bytes are ordered from the
213
+ * lowest level upward (leaf sibling first, root-adjacent last), the
214
+ * order `verifyInclusionProof` consumes.
215
+ */
216
+ export function buildInclusionProof(input) {
217
+ const { hasher, leafIndex, treeSize, getNode } = input;
218
+ if (!Number.isInteger(leafIndex) || leafIndex < 0)
219
+ throw new RangeError(`buildInclusionProof: leafIndex must be a non-negative integer, got ${leafIndex}`);
220
+ if (!Number.isInteger(treeSize) || treeSize < 1)
221
+ throw new RangeError(`buildInclusionProof: treeSize must be a positive integer, got ${treeSize}`);
222
+ if (leafIndex >= treeSize)
223
+ throw new RangeError(`buildInclusionProof: leafIndex ${leafIndex} >= treeSize ${treeSize}`);
224
+ return pathBuild(hasher, leafIndex, 0, treeSize, getNode);
225
+ }
226
+ /**
227
+ * RFC 9162 §2.1.4: build the consistency proof between two tree
228
+ * sizes. Returns an empty array when oldSize equals newSize or
229
+ * oldSize is zero (the verifier rejects the latter, but the builder
230
+ * is symmetric for inspection-time use).
231
+ */
232
+ export function buildConsistencyProof(input) {
233
+ const { hasher, oldSize, newSize, getNode } = input;
234
+ if (!Number.isInteger(oldSize) || oldSize < 0)
235
+ throw new RangeError(`buildConsistencyProof: oldSize must be a non-negative integer, got ${oldSize}`);
236
+ if (!Number.isInteger(newSize) || newSize < 0)
237
+ throw new RangeError(`buildConsistencyProof: newSize must be a non-negative integer, got ${newSize}`);
238
+ if (oldSize > newSize)
239
+ throw new RangeError(`buildConsistencyProof: oldSize ${oldSize} > newSize ${newSize}`);
240
+ if (oldSize === newSize || oldSize === 0)
241
+ return [];
242
+ return subProof(hasher, oldSize, 0, newSize, true, getNode);
243
+ }
244
+ // RFC 9162 §2.1.4 SUBPROOF(m, D[n], b). `lo` and `hi` parameterise
245
+ // the [lo, hi) range covered by the current subtree; `m` is the size
246
+ // of the older subtree being witnessed.
247
+ function subProof(hasher, m, lo, hi, b, getNode) {
248
+ const n = hi - lo;
249
+ if (m === n) {
250
+ // Whole subtree: emit its root only if b == false.
251
+ return b ? [] : [subtreeHash(hasher, lo, hi, getNode)];
252
+ }
253
+ const k = splitPoint(n);
254
+ if (m <= k) {
255
+ const sub = subProof(hasher, m, lo, lo + k, b, getNode);
256
+ sub.push(subtreeHash(hasher, lo + k, hi, getNode));
257
+ return sub;
258
+ }
259
+ const sub = subProof(hasher, m - k, lo + k, hi, false, getNode);
260
+ sub.push(subtreeHash(hasher, lo, lo + k, getNode));
261
+ return sub;
262
+ }
263
+ // Inclusion-proof path build: yields siblings ordered from the lowest
264
+ // level (leaf sibling) up. Sibling = root of the other half of the
265
+ // current subtree.
266
+ function pathBuild(hasher, leafIndex, lo, hi, getNode) {
267
+ if (hi - lo <= 1)
268
+ return [];
269
+ const k = splitPoint(hi - lo);
270
+ if (leafIndex - lo < k) {
271
+ const sub = pathBuild(hasher, leafIndex, lo, lo + k, getNode);
272
+ sub.push(subtreeHash(hasher, lo + k, hi, getNode));
273
+ return sub;
274
+ }
275
+ const sub = pathBuild(hasher, leafIndex, lo + k, hi, getNode);
276
+ sub.push(subtreeHash(hasher, lo, lo + k, getNode));
277
+ return sub;
278
+ }
279
+ /**
280
+ * RFC 9162 §2.1.1 MTH(D[lo:hi]). For a perfect aligned subtree the
281
+ * value is stored at (level, index); otherwise the value is the
282
+ * internal hash of the perfect left half and the recursive right
283
+ * half. Visible to the tree class so `rootHash()` can share the
284
+ * recursion with the builders.
285
+ *
286
+ * @internal
287
+ */
288
+ export function subtreeHash(hasher, lo, hi, getNode) {
289
+ const n = hi - lo;
290
+ if (n === 1)
291
+ return getNode(0, lo);
292
+ const k = splitPoint(n);
293
+ if (k === n / 2 && (lo % n) === 0) {
294
+ // Perfect aligned subtree: a stored internal node.
295
+ return getNode(bitLen(n) - 1, Math.floor(lo / n));
296
+ }
297
+ const left = subtreeHash(hasher, lo, lo + k, getNode);
298
+ const right = subtreeHash(hasher, lo + k, hi, getNode);
299
+ return hasher.hashInternal(left, right);
300
+ }