leviathan-crypto 2.0.1 → 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 (312) hide show
  1. package/CLAUDE.md +88 -281
  2. package/LICENSE +4 -0
  3. package/README.md +275 -87
  4. package/dist/aes/aes-cbc.d.ts +40 -0
  5. package/dist/aes/aes-cbc.js +158 -0
  6. package/dist/aes/aes-ctr.d.ts +50 -0
  7. package/dist/aes/aes-ctr.js +141 -0
  8. package/dist/aes/aes-gcm-siv.d.ts +67 -0
  9. package/dist/aes/aes-gcm-siv.js +217 -0
  10. package/dist/aes/aes-gcm.d.ts +61 -0
  11. package/dist/aes/aes-gcm.js +226 -0
  12. package/dist/aes/cipher-suite.d.ts +21 -0
  13. package/dist/aes/cipher-suite.js +179 -0
  14. package/dist/aes/embedded.d.ts +1 -0
  15. package/dist/aes/embedded.js +26 -0
  16. package/dist/aes/generator.d.ts +14 -0
  17. package/dist/aes/generator.js +103 -0
  18. package/dist/aes/index.d.ts +58 -0
  19. package/dist/aes/index.js +125 -0
  20. package/dist/aes/ops.d.ts +60 -0
  21. package/dist/aes/ops.js +164 -0
  22. package/dist/aes/pool-worker.d.ts +1 -0
  23. package/dist/aes/pool-worker.js +92 -0
  24. package/dist/aes/types.d.ts +1 -0
  25. package/dist/aes/types.js +23 -0
  26. package/dist/aes.wasm +0 -0
  27. package/dist/blake3/embedded.d.ts +1 -0
  28. package/dist/blake3/embedded.js +26 -0
  29. package/dist/blake3/index.d.ts +143 -0
  30. package/dist/blake3/index.js +620 -0
  31. package/dist/blake3/types.d.ts +102 -0
  32. package/dist/blake3/types.js +31 -0
  33. package/dist/blake3/validate.d.ts +29 -0
  34. package/dist/blake3/validate.js +80 -0
  35. package/dist/blake3.wasm +0 -0
  36. package/dist/chacha20/cipher-suite.d.ts +10 -0
  37. package/dist/chacha20/cipher-suite.js +98 -13
  38. package/dist/chacha20/generator.d.ts +12 -0
  39. package/dist/chacha20/generator.js +91 -0
  40. package/dist/chacha20/index.d.ts +100 -3
  41. package/dist/chacha20/index.js +169 -35
  42. package/dist/chacha20/ops.d.ts +57 -6
  43. package/dist/chacha20/ops.js +107 -27
  44. package/dist/chacha20/pool-worker.js +14 -0
  45. package/dist/chacha20/types.d.ts +1 -32
  46. package/dist/cte-wasm.d.ts +1 -0
  47. package/dist/cte-wasm.js +3 -0
  48. package/dist/cte.wasm +0 -0
  49. package/dist/curve25519.wasm +0 -0
  50. package/dist/ecdsa/der.d.ts +23 -0
  51. package/dist/ecdsa/der.js +192 -0
  52. package/dist/ecdsa/ecprivatekey-der.d.ts +32 -0
  53. package/dist/ecdsa/ecprivatekey-der.js +230 -0
  54. package/dist/ecdsa/embedded.d.ts +1 -0
  55. package/dist/ecdsa/embedded.js +25 -0
  56. package/dist/ecdsa/index.d.ts +124 -0
  57. package/dist/ecdsa/index.js +366 -0
  58. package/dist/ecdsa/types.d.ts +31 -0
  59. package/dist/ecdsa/types.js +28 -0
  60. package/dist/ecdsa/validate.d.ts +18 -0
  61. package/dist/ecdsa/validate.js +92 -0
  62. package/dist/ed25519/embedded.d.ts +1 -0
  63. package/dist/ed25519/embedded.js +31 -0
  64. package/dist/ed25519/index.d.ts +70 -0
  65. package/dist/ed25519/index.js +308 -0
  66. package/dist/ed25519/types.d.ts +27 -0
  67. package/dist/ed25519/types.js +27 -0
  68. package/dist/ed25519/validate.d.ts +7 -0
  69. package/dist/ed25519/validate.js +77 -0
  70. package/dist/embedded/aes-pool-worker.d.ts +1 -0
  71. package/dist/embedded/aes-pool-worker.js +5 -0
  72. package/dist/embedded/aes.d.ts +1 -0
  73. package/dist/embedded/aes.js +3 -0
  74. package/dist/embedded/blake3.d.ts +1 -0
  75. package/dist/embedded/blake3.js +3 -0
  76. package/dist/embedded/chacha20-pool-worker.d.ts +1 -0
  77. package/dist/embedded/chacha20-pool-worker.js +5 -0
  78. package/dist/embedded/chacha20.d.ts +1 -1
  79. package/dist/embedded/chacha20.js +2 -2
  80. package/dist/embedded/curve25519.d.ts +1 -0
  81. package/dist/embedded/curve25519.js +3 -0
  82. package/dist/embedded/mldsa.d.ts +1 -0
  83. package/dist/embedded/mldsa.js +3 -0
  84. package/dist/embedded/mlkem.d.ts +1 -0
  85. package/dist/embedded/mlkem.js +3 -0
  86. package/dist/embedded/p256.d.ts +1 -0
  87. package/dist/embedded/p256.js +3 -0
  88. package/dist/embedded/serpent-pool-worker.d.ts +1 -0
  89. package/dist/embedded/serpent-pool-worker.js +5 -0
  90. package/dist/embedded/serpent.d.ts +1 -1
  91. package/dist/embedded/serpent.js +2 -2
  92. package/dist/embedded/sha2.d.ts +1 -1
  93. package/dist/embedded/sha2.js +2 -2
  94. package/dist/embedded/sha3.d.ts +1 -1
  95. package/dist/embedded/sha3.js +2 -2
  96. package/dist/embedded/slhdsa.d.ts +1 -0
  97. package/dist/embedded/slhdsa.js +3 -0
  98. package/dist/errors.d.ts +92 -1
  99. package/dist/errors.js +111 -1
  100. package/dist/fortuna.d.ts +18 -12
  101. package/dist/fortuna.js +166 -99
  102. package/dist/index.d.ts +42 -11
  103. package/dist/index.js +65 -20
  104. package/dist/init.d.ts +1 -3
  105. package/dist/init.js +73 -7
  106. package/dist/keccak/embedded.js +1 -1
  107. package/dist/keccak/index.d.ts +2 -0
  108. package/dist/keccak/index.js +4 -2
  109. package/dist/loader.d.ts +1 -19
  110. package/dist/loader.js +26 -32
  111. package/dist/merkle/blake3-tree.d.ts +35 -0
  112. package/dist/merkle/blake3-tree.js +187 -0
  113. package/dist/merkle/checkpoint.d.ts +58 -0
  114. package/dist/merkle/checkpoint.js +217 -0
  115. package/dist/merkle/index.d.ts +19 -0
  116. package/dist/merkle/index.js +37 -0
  117. package/dist/merkle/merkle-log.d.ts +130 -0
  118. package/dist/merkle/merkle-log.js +207 -0
  119. package/dist/merkle/merkle-verifier.d.ts +126 -0
  120. package/dist/merkle/merkle-verifier.js +296 -0
  121. package/dist/merkle/proof.d.ts +70 -0
  122. package/dist/merkle/proof.js +300 -0
  123. package/dist/merkle/sha256-tree.d.ts +33 -0
  124. package/dist/merkle/sha256-tree.js +145 -0
  125. package/dist/merkle/signed-log.d.ts +156 -0
  126. package/dist/merkle/signed-log.js +356 -0
  127. package/dist/merkle/signed-note.d.ts +309 -0
  128. package/dist/merkle/signed-note.js +648 -0
  129. package/dist/merkle/sth.d.ts +31 -0
  130. package/dist/merkle/sth.js +31 -0
  131. package/dist/merkle/storage.d.ts +40 -0
  132. package/dist/merkle/storage.js +71 -0
  133. package/dist/merkle/tree.d.ts +68 -0
  134. package/dist/merkle/tree.js +94 -0
  135. package/dist/mldsa/embedded.d.ts +1 -0
  136. package/dist/{kyber → mldsa}/embedded.js +5 -5
  137. package/dist/mldsa/expand.d.ts +53 -0
  138. package/dist/mldsa/expand.js +188 -0
  139. package/dist/mldsa/format.d.ts +16 -0
  140. package/dist/mldsa/format.js +68 -0
  141. package/dist/mldsa/hashvariant.d.ts +32 -0
  142. package/dist/mldsa/hashvariant.js +248 -0
  143. package/dist/mldsa/index.d.ts +142 -0
  144. package/dist/mldsa/index.js +463 -0
  145. package/dist/mldsa/keygen.d.ts +16 -0
  146. package/dist/mldsa/keygen.js +232 -0
  147. package/dist/mldsa/params.d.ts +21 -0
  148. package/dist/mldsa/params.js +55 -0
  149. package/dist/mldsa/sha3-helpers.d.ts +30 -0
  150. package/dist/mldsa/sha3-helpers.js +124 -0
  151. package/dist/mldsa/sign.d.ts +36 -0
  152. package/dist/mldsa/sign.js +380 -0
  153. package/dist/mldsa/types.d.ts +91 -0
  154. package/dist/mldsa/types.js +25 -0
  155. package/dist/mldsa/validate.d.ts +55 -0
  156. package/dist/mldsa/validate.js +125 -0
  157. package/dist/mldsa/verify.d.ts +29 -0
  158. package/dist/mldsa/verify.js +269 -0
  159. package/dist/mldsa.wasm +0 -0
  160. package/dist/mlkem/embedded.d.ts +1 -0
  161. package/dist/mlkem/embedded.js +27 -0
  162. package/dist/mlkem/indcpa.d.ts +49 -0
  163. package/dist/{kyber → mlkem}/indcpa.js +48 -48
  164. package/dist/mlkem/index.d.ts +37 -0
  165. package/dist/{kyber → mlkem}/index.js +41 -31
  166. package/dist/mlkem/kem.d.ts +21 -0
  167. package/dist/{kyber → mlkem}/kem.js +48 -13
  168. package/dist/{kyber → mlkem}/params.d.ts +4 -4
  169. package/dist/{kyber → mlkem}/params.js +2 -2
  170. package/dist/mlkem/suite.d.ts +12 -0
  171. package/dist/{kyber → mlkem}/suite.js +17 -12
  172. package/dist/{kyber → mlkem}/types.d.ts +4 -3
  173. package/dist/{kyber → mlkem}/types.js +1 -1
  174. package/dist/mlkem/validate.d.ts +23 -0
  175. package/dist/{kyber → mlkem}/validate.js +24 -20
  176. package/dist/{kyber.wasm → mlkem.wasm} +0 -0
  177. package/dist/p256.wasm +0 -0
  178. package/dist/ratchet/index.d.ts +8 -0
  179. package/dist/ratchet/index.js +38 -0
  180. package/dist/ratchet/kdf-chain.d.ts +13 -0
  181. package/dist/ratchet/kdf-chain.js +85 -0
  182. package/dist/ratchet/ratchet-keypair.d.ts +9 -0
  183. package/dist/ratchet/ratchet-keypair.js +61 -0
  184. package/dist/ratchet/root-kdf.d.ts +4 -0
  185. package/dist/ratchet/root-kdf.js +124 -0
  186. package/dist/ratchet/skipped-key-store.d.ts +14 -0
  187. package/dist/ratchet/skipped-key-store.js +154 -0
  188. package/dist/ratchet/types.d.ts +36 -0
  189. package/dist/ratchet/types.js +26 -0
  190. package/dist/serpent/cipher-suite.d.ts +10 -0
  191. package/dist/serpent/cipher-suite.js +144 -56
  192. package/dist/serpent/generator.d.ts +12 -0
  193. package/dist/serpent/generator.js +97 -0
  194. package/dist/serpent/index.d.ts +62 -1
  195. package/dist/serpent/index.js +97 -21
  196. package/dist/serpent/pool-worker.js +28 -102
  197. package/dist/serpent/serpent-cbc.d.ts +16 -6
  198. package/dist/serpent/serpent-cbc.js +58 -37
  199. package/dist/serpent/shared-ops.d.ts +63 -0
  200. package/dist/serpent/shared-ops.js +178 -0
  201. package/dist/serpent/types.d.ts +1 -5
  202. package/dist/serpent.wasm +0 -0
  203. package/dist/sha2/hash.d.ts +2 -0
  204. package/dist/sha2/hash.js +53 -0
  205. package/dist/sha2/hkdf.js +5 -5
  206. package/dist/sha2/index.d.ts +22 -1
  207. package/dist/sha2/index.js +80 -11
  208. package/dist/sha2/types.d.ts +41 -2
  209. package/dist/sha2.wasm +0 -0
  210. package/dist/sha3/hash.d.ts +2 -0
  211. package/dist/sha3/hash.js +53 -0
  212. package/dist/sha3/index.d.ts +87 -3
  213. package/dist/sha3/index.js +317 -19
  214. package/dist/sha3/kmac.d.ts +121 -0
  215. package/dist/sha3/kmac.js +800 -0
  216. package/dist/sha3.wasm +0 -0
  217. package/dist/shared/pkcs7.d.ts +22 -0
  218. package/dist/shared/pkcs7.js +84 -0
  219. package/dist/sign/ctx.d.ts +41 -0
  220. package/dist/sign/ctx.js +102 -0
  221. package/dist/sign/envelope.d.ts +45 -0
  222. package/dist/sign/envelope.js +152 -0
  223. package/dist/sign/hasher.d.ts +9 -0
  224. package/dist/sign/hasher.js +132 -0
  225. package/dist/sign/index.d.ts +11 -0
  226. package/dist/sign/index.js +34 -0
  227. package/dist/sign/sign-stream.d.ts +25 -0
  228. package/dist/sign/sign-stream.js +112 -0
  229. package/dist/sign/suites/ecdsa-p256.d.ts +2 -0
  230. package/dist/sign/suites/ecdsa-p256.js +120 -0
  231. package/dist/sign/suites/ed25519.d.ts +3 -0
  232. package/dist/sign/suites/ed25519.js +165 -0
  233. package/dist/sign/suites/hybrid-classical.d.ts +23 -0
  234. package/dist/sign/suites/hybrid-classical.js +526 -0
  235. package/dist/sign/suites/hybrid-pq.d.ts +4 -0
  236. package/dist/sign/suites/hybrid-pq.js +234 -0
  237. package/dist/sign/suites/mldsa.d.ts +7 -0
  238. package/dist/sign/suites/mldsa.js +161 -0
  239. package/dist/sign/suites/slhdsa.d.ts +7 -0
  240. package/dist/sign/suites/slhdsa.js +176 -0
  241. package/dist/sign/types.d.ts +106 -0
  242. package/dist/sign/types.js +28 -0
  243. package/dist/sign/verify-stream.d.ts +30 -0
  244. package/dist/sign/verify-stream.js +227 -0
  245. package/dist/slhdsa/embedded.d.ts +1 -0
  246. package/dist/slhdsa/embedded.js +26 -0
  247. package/dist/slhdsa/index.d.ts +149 -0
  248. package/dist/slhdsa/index.js +493 -0
  249. package/dist/slhdsa/params.d.ts +26 -0
  250. package/dist/slhdsa/params.js +70 -0
  251. package/dist/slhdsa/prehash.d.ts +68 -0
  252. package/dist/slhdsa/prehash.js +307 -0
  253. package/dist/slhdsa/sign.d.ts +39 -0
  254. package/dist/slhdsa/sign.js +116 -0
  255. package/dist/slhdsa/types.d.ts +129 -0
  256. package/dist/slhdsa/types.js +27 -0
  257. package/dist/slhdsa/validate.d.ts +60 -0
  258. package/dist/slhdsa/validate.js +127 -0
  259. package/dist/slhdsa/verify.d.ts +32 -0
  260. package/dist/slhdsa/verify.js +107 -0
  261. package/dist/slhdsa.wasm +0 -0
  262. package/dist/stream/header.js +8 -8
  263. package/dist/stream/index.d.ts +1 -0
  264. package/dist/stream/index.js +1 -0
  265. package/dist/stream/open-stream.js +65 -22
  266. package/dist/stream/seal-stream-pool.d.ts +2 -0
  267. package/dist/stream/seal-stream-pool.js +100 -33
  268. package/dist/stream/seal-stream.d.ts +1 -1
  269. package/dist/stream/seal-stream.js +48 -19
  270. package/dist/stream/seal.js +6 -6
  271. package/dist/stream/types.d.ts +3 -1
  272. package/dist/stream/types.js +1 -1
  273. package/dist/types.d.ts +22 -1
  274. package/dist/types.js +1 -1
  275. package/dist/utils.d.ts +9 -10
  276. package/dist/utils.js +84 -59
  277. package/dist/wasm-source.d.ts +9 -8
  278. package/dist/wasm-source.js +1 -1
  279. package/dist/x25519/embedded.d.ts +1 -0
  280. package/dist/x25519/embedded.js +31 -0
  281. package/dist/x25519/index.d.ts +43 -0
  282. package/dist/x25519/index.js +159 -0
  283. package/dist/x25519/types.d.ts +25 -0
  284. package/dist/x25519/types.js +27 -0
  285. package/dist/x25519/validate.d.ts +2 -0
  286. package/dist/x25519/validate.js +39 -0
  287. package/package.json +123 -64
  288. package/SECURITY.md +0 -276
  289. package/dist/ct-wasm.d.ts +0 -1
  290. package/dist/ct-wasm.js +0 -3
  291. package/dist/ct.wasm +0 -0
  292. package/dist/docs/aead.md +0 -323
  293. package/dist/docs/architecture.md +0 -932
  294. package/dist/docs/argon2id.md +0 -302
  295. package/dist/docs/chacha20.md +0 -674
  296. package/dist/docs/exports.md +0 -241
  297. package/dist/docs/fortuna.md +0 -313
  298. package/dist/docs/init.md +0 -302
  299. package/dist/docs/loader.md +0 -161
  300. package/dist/docs/serpent.md +0 -519
  301. package/dist/docs/sha2.md +0 -613
  302. package/dist/docs/sha3.md +0 -546
  303. package/dist/docs/types.md +0 -276
  304. package/dist/docs/utils.md +0 -367
  305. package/dist/embedded/kyber.d.ts +0 -1
  306. package/dist/embedded/kyber.js +0 -3
  307. package/dist/kyber/embedded.d.ts +0 -1
  308. package/dist/kyber/indcpa.d.ts +0 -49
  309. package/dist/kyber/index.d.ts +0 -38
  310. package/dist/kyber/kem.d.ts +0 -21
  311. package/dist/kyber/suite.d.ts +0 -13
  312. package/dist/kyber/validate.d.ts +0 -19
@@ -0,0 +1,217 @@
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/checkpoint.ts
23
+ //
24
+ // Canonical checkpoint body codec per c2sp.org/tlog-checkpoint (Transparency
25
+ // Log Checkpoints) §Note text. Three newline-terminated lines: origin, tree
26
+ // size in ASCII decimal with no leading zeroes, base64-encoded root hash.
27
+ // The body bytes are exactly what the STH signature is computed over, so
28
+ // producers and verifiers MUST serialize byte-for-byte identically.
29
+ //
30
+ // Extension lines are spec-listed as OPTIONAL and NOT RECOMMENDED. The
31
+ // ML-DSA-44 cosignature format defined in c2sp.org/tlog-cosignature does
32
+ // not commit to extension lines, so leviathan emits empty extension sections
33
+ // and the parser rejects any input that contains extension lines.
34
+ import { utf8ToBytes, bytesToUtf8, base64ToBytes, bytesToBase64 } from '../utils.js';
35
+ // ── serialization ───────────────────────────────────────────────────────────
36
+ const LF = 0x0a; // U+000A, the only legal line terminator in the body
37
+ const SPACE = 0x20; // U+0020, illegal anywhere inside origin
38
+ const PLUS = 0x2b; // U+002B, illegal anywhere inside origin
39
+ /**
40
+ * Decimal-encode a non-negative integer per c2sp.org/tlog-checkpoint §Note
41
+ * text: ASCII digits, no leading zeroes, the literal `0` for an empty tree.
42
+ * `Number.toString(10)` is already in this form for non-negative safe
43
+ * integers, the explicit guard exists so a Number that slipped past the
44
+ * upstream call site does not silently produce `"1e+21"` or similar.
45
+ */
46
+ function decimalTreeSize(n) {
47
+ if (!Number.isInteger(n) || n < 0 || n > Number.MAX_SAFE_INTEGER)
48
+ throw new RangeError(`serializeCheckpointBody: treeSize must be a non-negative safe integer, got ${n}`);
49
+ return n.toString(10);
50
+ }
51
+ /**
52
+ * Throw if `origin` violates the c2sp.org/tlog-checkpoint §Note text MUSTs:
53
+ * non-empty, no embedded newlines, no Unicode spaces, no plus characters.
54
+ * The "schemeless URL" advice from the spec is SHOULD-level and not
55
+ * enforced here, broader policy belongs to the application layer.
56
+ */
57
+ function validateOrigin(origin) {
58
+ if (origin.length === 0)
59
+ throw new RangeError('checkpoint: origin must be non-empty');
60
+ // Unicode space classes are wider than ASCII 0x20; the c2sp spec text
61
+ // says "Unicode spaces", so we use \s which covers the same family.
62
+ if (/\s/.test(origin) || origin.includes('+'))
63
+ throw new RangeError('checkpoint: origin must not contain whitespace or plus characters');
64
+ }
65
+ /**
66
+ * Serialize a Checkpoint into its canonical body bytes per
67
+ * c2sp.org/tlog-checkpoint §Note text. Layout:
68
+ *
69
+ * utf8(origin) || 0x0A || utf8(decimal(treeSize)) || 0x0A
70
+ * || base64(rootHash) || 0x0A
71
+ *
72
+ * Base64 uses the RFC 4648 §4 standard alphabet with `=` padding (NOT the
73
+ * URL-safe variant from §5 and NOT padding-stripped). The body has no
74
+ * leading or trailing whitespace beyond the final 0x0A; byte stability
75
+ * is the entire purpose of the codec, since the body bytes are what the
76
+ * STH signature is computed over.
77
+ */
78
+ export function serializeCheckpointBody(c) {
79
+ validateOrigin(c.origin);
80
+ const originBytes = utf8ToBytes(c.origin);
81
+ const sizeBytes = utf8ToBytes(decimalTreeSize(c.treeSize));
82
+ const rootB64 = bytesToBase64(c.rootHash);
83
+ const rootBytes = utf8ToBytes(rootB64);
84
+ const out = new Uint8Array(originBytes.length + 1 + sizeBytes.length + 1 + rootBytes.length + 1);
85
+ let off = 0;
86
+ out.set(originBytes, off);
87
+ off += originBytes.length;
88
+ out[off++] = LF;
89
+ out.set(sizeBytes, off);
90
+ off += sizeBytes.length;
91
+ out[off++] = LF;
92
+ out.set(rootBytes, off);
93
+ off += rootBytes.length;
94
+ out[off] = LF;
95
+ return out;
96
+ }
97
+ // ── parsing ─────────────────────────────────────────────────────────────────
98
+ /**
99
+ * Reject ASCII control characters below U+0020 other than 0x0A. The
100
+ * signed-note spec at c2sp.org/signed-note §Format prohibits these in the
101
+ * envelope; the checkpoint body inherits the same rule because the body
102
+ * is the prefix of a signed-note text region.
103
+ */
104
+ function hasIllegalControls(bytes) {
105
+ for (const b of bytes) {
106
+ if (b < 0x20 && b !== LF)
107
+ return true;
108
+ if (b === 0x7f)
109
+ return true;
110
+ }
111
+ return false;
112
+ }
113
+ /**
114
+ * Validate that a decimal tree-size string carries no leading zeroes per
115
+ * c2sp.org/tlog-checkpoint §Note text. The literal `"0"` is the sole legal
116
+ * string starting with `0`.
117
+ */
118
+ function parseTreeSize(s) {
119
+ if (s.length === 0)
120
+ throw new RangeError('checkpoint: empty tree-size line');
121
+ if (!/^[0-9]+$/.test(s))
122
+ throw new RangeError(`checkpoint: tree size '${s}' is not ASCII decimal`);
123
+ if (s.length > 1 && s.charCodeAt(0) === 0x30 /* '0' */)
124
+ throw new RangeError(`checkpoint: tree size '${s}' has a leading zero`);
125
+ const n = Number(s);
126
+ if (!Number.isInteger(n) || n < 0 || n > Number.MAX_SAFE_INTEGER)
127
+ throw new RangeError(`checkpoint: tree size '${s}' exceeds Number.MAX_SAFE_INTEGER`);
128
+ return n;
129
+ }
130
+ /**
131
+ * Parse a canonical checkpoint body. Inverse of `serializeCheckpointBody`;
132
+ * round-trips byte-for-byte. Rejects extension lines, leading or trailing
133
+ * whitespace beyond the mandatory final 0x0A, non-newline ASCII control
134
+ * characters, malformed base64, and root hashes whose decoded length does
135
+ * not match `expectedHashLen` (default 32, the size for both Sha256Tree
136
+ * and Blake3Tree).
137
+ *
138
+ * The caller pins `expectedHashLen` to its hasher's `outputSize`; a future
139
+ * SignedLog (TASK-4) will bind this to the tree's hasher automatically.
140
+ *
141
+ * Per c2sp.org/tlog-checkpoint §Note text and c2sp.org/signed-note
142
+ * §Format.
143
+ */
144
+ export function parseCheckpointBody(bytes, expectedHashLen = 32) {
145
+ if (!(bytes instanceof Uint8Array))
146
+ throw new TypeError('parseCheckpointBody: input must be a Uint8Array');
147
+ if (bytes.length === 0)
148
+ throw new RangeError('parseCheckpointBody: empty body');
149
+ if (bytes[bytes.length - 1] !== LF)
150
+ throw new RangeError('parseCheckpointBody: body must end with U+000A');
151
+ if (hasIllegalControls(bytes))
152
+ throw new RangeError('parseCheckpointBody: body contains non-newline ASCII control characters');
153
+ // Collect line offsets without TextDecoder gymnastics, so an embedded
154
+ // newline inside the origin can be caught structurally rather than via
155
+ // post-hoc string checks.
156
+ const lineStarts = [0];
157
+ for (let i = 0; i < bytes.length; i++) {
158
+ if (bytes[i] === LF && i + 1 < bytes.length)
159
+ lineStarts.push(i + 1);
160
+ }
161
+ // Three mandatory lines, each newline-terminated, plus the trailing LF
162
+ // at end of body. Extension lines (4th line and beyond) are NOT
163
+ // RECOMMENDED per c2sp.org/tlog-checkpoint §Note text; the ML-DSA-44
164
+ // cosignature format does not sign them, so leviathan rejects them
165
+ // outright to keep the wire format witness-ready end to end.
166
+ if (lineStarts.length !== 3)
167
+ throw new RangeError(`parseCheckpointBody: expected exactly 3 lines, got ${lineStarts.length}`);
168
+ // Slice the three lines without their terminating LF.
169
+ const sliceLine = (idx) => {
170
+ const start = lineStarts[idx];
171
+ const end = idx + 1 < lineStarts.length ? lineStarts[idx + 1] - 1 : bytes.length - 1;
172
+ return bytes.subarray(start, end);
173
+ };
174
+ const originBytes = sliceLine(0);
175
+ const sizeBytes = sliceLine(1);
176
+ const rootB64Bytes = sliceLine(2);
177
+ if (originBytes.length === 0)
178
+ throw new RangeError('parseCheckpointBody: empty origin line');
179
+ let origin;
180
+ try {
181
+ origin = bytesToUtf8(originBytes);
182
+ }
183
+ catch {
184
+ throw new RangeError('parseCheckpointBody: origin is not valid UTF-8');
185
+ }
186
+ validateOrigin(origin);
187
+ // The byte-level scan caught most disallowed characters above; reject
188
+ // stray SP/PLUS bytes that slipped past the UTF-8 validation in case
189
+ // of a future encoding edge case.
190
+ for (const b of originBytes)
191
+ if (b === SPACE || b === PLUS)
192
+ throw new RangeError('parseCheckpointBody: origin contains space or plus');
193
+ const sizeStr = bytesToUtf8(sizeBytes);
194
+ const treeSize = parseTreeSize(sizeStr);
195
+ const rootB64 = bytesToUtf8(rootB64Bytes);
196
+ // RFC 4648 §4 standard alphabet, with padding. `base64ToBytes` accepts
197
+ // the URL-safe variant and the padding-stripped form; reject both
198
+ // explicitly so the codec stays strictly compliant with
199
+ // c2sp.org/tlog-checkpoint §Conventions. A standard padded base64
200
+ // string always has length divisible by 4.
201
+ if (/[-_]/.test(rootB64))
202
+ throw new RangeError('parseCheckpointBody: root hash uses URL-safe base64');
203
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(rootB64))
204
+ throw new RangeError('parseCheckpointBody: root hash is not standard base64');
205
+ if (rootB64.length % 4 !== 0)
206
+ throw new RangeError('parseCheckpointBody: root hash base64 length is not a multiple of 4 (padding missing)');
207
+ let rootHash;
208
+ try {
209
+ rootHash = base64ToBytes(rootB64);
210
+ }
211
+ catch {
212
+ throw new RangeError('parseCheckpointBody: root hash failed base64 decoding');
213
+ }
214
+ if (rootHash.length !== expectedHashLen)
215
+ throw new RangeError(`parseCheckpointBody: root hash length ${rootHash.length} != expected ${expectedHashLen}`);
216
+ return { origin, treeSize, rootHash };
217
+ }
@@ -0,0 +1,19 @@
1
+ export { splitPoint } from './tree.js';
2
+ export type { Hasher, MerkleTree } from './tree.js';
3
+ export { MemoryStorage } from './storage.js';
4
+ export type { MerkleStorage } from './storage.js';
5
+ export { verifyInclusionProof, verifyConsistencyProof, buildInclusionProof, buildConsistencyProof, } from './proof.js';
6
+ export type { VerifyInclusionInput, VerifyConsistencyInput, BuildInclusionInput, BuildConsistencyInput, GetNode, } from './proof.js';
7
+ export { Sha256Hasher, Sha256Tree } from './sha256-tree.js';
8
+ export { Blake3Hasher, Blake3Tree } from './blake3-tree.js';
9
+ export { serializeCheckpointBody, parseCheckpointBody } from './checkpoint.js';
10
+ export type { Checkpoint } from './checkpoint.js';
11
+ export { emitSignedNote, parseSignedNote, deriveKeyId, suiteFormatEnumToAlgoByte, lookupAlgoEntryByFormatEnum, lookupAlgoEntryByByte, buildCosigSignedMessage, buildCosignedMessage, emitCosigSignaturePayload, parseCosigSignaturePayload, ALGO_BYTE_ED25519_NOTE, ALGO_BYTE_ED25519_COSIG, ALGO_BYTE_MLDSA44_COSIG, } from './signed-note.js';
12
+ export type { SignatureLine, SignedNote, AlgoEntry, MessageConstruction, SignaturePayload, CosignedMessageInput, } from './signed-note.js';
13
+ export type { SignedTreeHead } from './sth.js';
14
+ export { SignedLog } from './signed-log.js';
15
+ export type { SignedLogOpts } from './signed-log.js';
16
+ export { MerkleVerifier } from './merkle-verifier.js';
17
+ export type { MerkleVerifierOpts } from './merkle-verifier.js';
18
+ export { MerkleLog } from './merkle-log.js';
19
+ export type { MerkleLogCreateOpts, MerkleLogGenerateOpts } from './merkle-log.js';
@@ -0,0 +1,37 @@
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/index.ts
23
+ //
24
+ // Public surface for the merkle log primitives. Interfaces, free
25
+ // functions, and the SHA-256 specialisation. Hash-agnostic by design;
26
+ // the BLAKE3 specialisation lives alongside this module and re-exports
27
+ // the same interfaces.
28
+ export { splitPoint } from './tree.js';
29
+ export { MemoryStorage } from './storage.js';
30
+ export { verifyInclusionProof, verifyConsistencyProof, buildInclusionProof, buildConsistencyProof, } from './proof.js';
31
+ export { Sha256Hasher, Sha256Tree } from './sha256-tree.js';
32
+ export { Blake3Hasher, Blake3Tree } from './blake3-tree.js';
33
+ export { serializeCheckpointBody, parseCheckpointBody } from './checkpoint.js';
34
+ export { emitSignedNote, parseSignedNote, deriveKeyId, suiteFormatEnumToAlgoByte, lookupAlgoEntryByFormatEnum, lookupAlgoEntryByByte, buildCosigSignedMessage, buildCosignedMessage, emitCosigSignaturePayload, parseCosigSignaturePayload, ALGO_BYTE_ED25519_NOTE, ALGO_BYTE_ED25519_COSIG, ALGO_BYTE_MLDSA44_COSIG, } from './signed-note.js';
35
+ export { SignedLog } from './signed-log.js';
36
+ export { MerkleVerifier } from './merkle-verifier.js';
37
+ export { MerkleLog } from './merkle-log.js';
@@ -0,0 +1,130 @@
1
+ import type { Hasher } from './tree.js';
2
+ import type { SignatureSuite } from '../sign/types.js';
3
+ /**
4
+ * Options for `MerkleLog.create`. The signing key and pubkey are
5
+ * caller-supplied: `MerkleLog` does not persist keys. For ephemeral
6
+ * use cases the companion factory `MerkleLog.generate` materialises a
7
+ * fresh keypair via `suite.keygen()` and returns the keypair to the
8
+ * caller so it can be persisted externally.
9
+ */
10
+ export interface MerkleLogCreateOpts {
11
+ /**
12
+ * Log identity, the first line of every checkpoint body. Validated
13
+ * by the inner `SignedLog` (non-empty, no whitespace, no plus
14
+ * characters) per c2sp.org/tlog-checkpoint §Note text.
15
+ */
16
+ readonly origin: string;
17
+ /** Signing key. Length must equal `suite.skSize`. */
18
+ readonly signingKey: Uint8Array;
19
+ /** Public key. Length must equal `suite.pkSize`. */
20
+ readonly pubkey: Uint8Array;
21
+ /**
22
+ * Hash function the tree uses. `'sha256'` (default) resolves to
23
+ * `Sha256Tree`, `'blake3'` resolves to `Blake3Tree`. SHA-256 is the
24
+ * C2SP-interop choice; the BLAKE3 specialisation is for callers who
25
+ * already invest in BLAKE3 elsewhere in their stack.
26
+ */
27
+ readonly hashing?: 'sha256' | 'blake3';
28
+ /**
29
+ * Cosignature signature suite. Defaults to `MlDsa44Suite` per the
30
+ * project's PQ-first principle and c2sp.org/tlog-checkpoint
31
+ * §Format's MUST/SHOULD wording on ML-DSA-44. Must be registered
32
+ * in the c2sp.org/tlog-cosignature §Format algorithm-byte registry;
33
+ * other suites raise `MerkleLogError('unsupported-suite')`.
34
+ */
35
+ readonly suite?: SignatureSuite;
36
+ }
37
+ /**
38
+ * Options for `MerkleLog.generate`. Identical to `MerkleLogCreateOpts`
39
+ * minus the key fields; `generate` materialises a fresh keypair via
40
+ * `suite.keygen()`.
41
+ */
42
+ export interface MerkleLogGenerateOpts {
43
+ readonly origin: string;
44
+ readonly hashing?: 'sha256' | 'blake3';
45
+ readonly suite?: SignatureSuite;
46
+ }
47
+ /**
48
+ * Memory-backed signed transparency log. The normie producer surface.
49
+ * Construct via `MerkleLog.create` (caller supplies keys) or
50
+ * `MerkleLog.generate` (the class materialises a fresh keypair and
51
+ * returns it). Methods after construction are synchronous; module-init
52
+ * readiness and keygen are the only async steps.
53
+ *
54
+ * Methods delegate to an inner `SignedLog<S>` with a fresh
55
+ * `MemoryStorage` backend. For file or database storage, construct
56
+ * `SignedLog` directly with a custom `MerkleStorage` implementation,
57
+ * see `docs/merkle.md` for the extension pattern.
58
+ */
59
+ export declare class MerkleLog {
60
+ readonly origin: string;
61
+ readonly hasher: Hasher;
62
+ readonly suite: SignatureSuite;
63
+ private readonly _inner;
64
+ private constructor();
65
+ /**
66
+ * Construct a `MerkleLog` with caller-supplied keys. Validates the
67
+ * suite against the c2sp.org/tlog-cosignature §Format algorithm-byte
68
+ * registry before instantiating the inner `SignedLog`; an
69
+ * unregistered suite raises `MerkleLogError('unsupported-suite')`
70
+ * with a message naming the suite and pointing at the spec.
71
+ *
72
+ * Async to keep the construction surface uniform with `generate`,
73
+ * which is async because `suite.keygen()` may route through async
74
+ * WASM acquisition under load. The hot-path methods (`append`,
75
+ * `head`, `size`, etc.) stay sync per the merkle layer's locked
76
+ * sync invariant.
77
+ */
78
+ static create(opts: MerkleLogCreateOpts): Promise<MerkleLog>;
79
+ /**
80
+ * Construct a `MerkleLog` with a freshly generated keypair. Returns
81
+ * the log plus the keypair; the caller is responsible for
82
+ * persisting the keys externally if the log outlives the process.
83
+ *
84
+ * The returned `signingKey` is a copy, the log retains its own
85
+ * internal copy that `dispose()` wipes; modifying the returned
86
+ * buffer after construction does not affect the log.
87
+ */
88
+ static generate(opts: MerkleLogGenerateOpts): Promise<{
89
+ log: MerkleLog;
90
+ signingKey: Uint8Array;
91
+ pubkey: Uint8Array;
92
+ }>;
93
+ /**
94
+ * Append a leaf and return its index, hash, and inclusion proof
95
+ * against the post-append tree size. Delegates to the inner
96
+ * `SignedLog.append`.
97
+ */
98
+ append(leafBytes: Uint8Array): {
99
+ leafIndex: number;
100
+ leafHash: Uint8Array;
101
+ inclusionProof: Uint8Array[];
102
+ };
103
+ /**
104
+ * Emit the current checkpoint as a signed-note envelope. Re-signed
105
+ * on every call; the body reflects the live tree size and root
106
+ * hash. Timestamp defaults to `Math.floor(Date.now() / 1000)`.
107
+ */
108
+ head(opts?: {
109
+ timestamp?: number;
110
+ }): Uint8Array;
111
+ /** Current number of leaves in the tree. */
112
+ size(): number;
113
+ /** Current Merkle root hash. */
114
+ rootHash(): Uint8Array;
115
+ /**
116
+ * Inclusion proof for `leafIndex` in a tree of the given size, or
117
+ * the current tree size if omitted. Per RFC 9162 §2.1.3.
118
+ */
119
+ inclusionProof(leafIndex: number, treeSize?: number): Uint8Array[];
120
+ /**
121
+ * Consistency proof between two tree sizes per RFC 9162 §2.1.4.
122
+ * `oldSize` must be `<= newSize <= size()`.
123
+ */
124
+ consistencyProof(oldSize: number, newSize: number): Uint8Array[];
125
+ /**
126
+ * Zero the stored signing-key copy. Idempotent. Subsequent calls
127
+ * to any public method throw.
128
+ */
129
+ dispose(): void;
130
+ }
@@ -0,0 +1,207 @@
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-log.ts
23
+ //
24
+ // `MerkleLog`, the producer-side normie surface. Memory-backed via
25
+ // `MemoryStorage`. Real deployments drop down to `SignedLog<S>` with a
26
+ // custom `MerkleStorage`.
27
+ //
28
+ // Defaults: `hashing: 'sha256'`, `suite: MlDsa44Suite`. ML-DSA-44 is
29
+ // the PQ default per c2sp.org/tlog-checkpoint, the only PQ suite
30
+ // currently in the c2sp.org/tlog-cosignature §Format algorithm-byte
31
+ // registry. Sigsum interop: pass `suite: Ed25519Suite`.
32
+ import { isInitialized } from '../init.js';
33
+ import { MerkleLogError } from '../errors.js';
34
+ import { SignedLog } from './signed-log.js';
35
+ import { MemoryStorage } from './storage.js';
36
+ import { Sha256Tree, Sha256Hasher } from './sha256-tree.js';
37
+ import { Blake3Tree, Blake3Hasher } from './blake3-tree.js';
38
+ import { lookupAlgoEntryByFormatEnum } from './signed-note.js';
39
+ import { MlDsa44Suite } from '../sign/suites/mldsa.js';
40
+ const SHA2_MODULE = 'sha2';
41
+ /**
42
+ * Memory-backed signed transparency log. The normie producer surface.
43
+ * Construct via `MerkleLog.create` (caller supplies keys) or
44
+ * `MerkleLog.generate` (the class materialises a fresh keypair and
45
+ * returns it). Methods after construction are synchronous; module-init
46
+ * readiness and keygen are the only async steps.
47
+ *
48
+ * Methods delegate to an inner `SignedLog<S>` with a fresh
49
+ * `MemoryStorage` backend. For file or database storage, construct
50
+ * `SignedLog` directly with a custom `MerkleStorage` implementation,
51
+ * see `docs/merkle.md` for the extension pattern.
52
+ */
53
+ export class MerkleLog {
54
+ origin;
55
+ hasher;
56
+ suite;
57
+ _inner;
58
+ constructor(inner) {
59
+ this._inner = inner;
60
+ this.origin = inner.origin;
61
+ this.hasher = inner.tree.hasher;
62
+ this.suite = inner.suite;
63
+ }
64
+ /**
65
+ * Construct a `MerkleLog` with caller-supplied keys. Validates the
66
+ * suite against the c2sp.org/tlog-cosignature §Format algorithm-byte
67
+ * registry before instantiating the inner `SignedLog`; an
68
+ * unregistered suite raises `MerkleLogError('unsupported-suite')`
69
+ * with a message naming the suite and pointing at the spec.
70
+ *
71
+ * Async to keep the construction surface uniform with `generate`,
72
+ * which is async because `suite.keygen()` may route through async
73
+ * WASM acquisition under load. The hot-path methods (`append`,
74
+ * `head`, `size`, etc.) stay sync per the merkle layer's locked
75
+ * sync invariant.
76
+ */
77
+ static async create(opts) {
78
+ const hashing = opts.hashing ?? 'sha256';
79
+ const suite = opts.suite ?? MlDsa44Suite;
80
+ if (lookupAlgoEntryByFormatEnum(suite.formatEnum) === undefined)
81
+ throw new MerkleLogError('unsupported-suite', `MerkleLog: suite '${suite.formatName}' (formatEnum 0x${suite.formatEnum
82
+ .toString(16)
83
+ .padStart(2, '0')}) has no c2sp.org/tlog-cosignature §Format algorithm byte; `
84
+ + 'use Ed25519Suite or MlDsa44Suite, or open an issue for a newly C2SP-registered suite');
85
+ const tree = buildTree(hashing);
86
+ assertModulesInitialized([
87
+ ...suite.wasmModules,
88
+ ...tree.hasher.wasmModules,
89
+ SHA2_MODULE,
90
+ ]);
91
+ const inner = new SignedLog({
92
+ tree,
93
+ suite,
94
+ origin: opts.origin,
95
+ signingKey: opts.signingKey,
96
+ pubkey: opts.pubkey,
97
+ });
98
+ return new MerkleLog(inner);
99
+ }
100
+ /**
101
+ * Construct a `MerkleLog` with a freshly generated keypair. Returns
102
+ * the log plus the keypair; the caller is responsible for
103
+ * persisting the keys externally if the log outlives the process.
104
+ *
105
+ * The returned `signingKey` is a copy, the log retains its own
106
+ * internal copy that `dispose()` wipes; modifying the returned
107
+ * buffer after construction does not affect the log.
108
+ */
109
+ static async generate(opts) {
110
+ const hashing = opts.hashing ?? 'sha256';
111
+ const suite = opts.suite ?? MlDsa44Suite;
112
+ if (lookupAlgoEntryByFormatEnum(suite.formatEnum) === undefined)
113
+ throw new MerkleLogError('unsupported-suite', `MerkleLog.generate: suite '${suite.formatName}' (formatEnum 0x${suite.formatEnum
114
+ .toString(16)
115
+ .padStart(2, '0')}) has no c2sp.org/tlog-cosignature §Format algorithm byte; `
116
+ + 'use Ed25519Suite or MlDsa44Suite, or open an issue for a newly C2SP-registered suite');
117
+ // suite.keygen() requires the suite's modules already initialised;
118
+ // the same modules are checked again inside create() before
119
+ // instantiating SignedLog. Checking here keeps the throw surface
120
+ // consistent regardless of which factory the caller used.
121
+ const tmpHasher = resolveHasher(hashing);
122
+ assertModulesInitialized([
123
+ ...suite.wasmModules,
124
+ ...tmpHasher.wasmModules,
125
+ SHA2_MODULE,
126
+ ]);
127
+ const { pk, sk } = suite.keygen();
128
+ const log = await MerkleLog.create({
129
+ origin: opts.origin,
130
+ signingKey: sk,
131
+ pubkey: pk,
132
+ hashing,
133
+ suite,
134
+ });
135
+ return { log, signingKey: sk, pubkey: pk };
136
+ }
137
+ /**
138
+ * Append a leaf and return its index, hash, and inclusion proof
139
+ * against the post-append tree size. Delegates to the inner
140
+ * `SignedLog.append`.
141
+ */
142
+ append(leafBytes) {
143
+ return this._inner.append(leafBytes);
144
+ }
145
+ /**
146
+ * Emit the current checkpoint as a signed-note envelope. Re-signed
147
+ * on every call; the body reflects the live tree size and root
148
+ * hash. Timestamp defaults to `Math.floor(Date.now() / 1000)`.
149
+ */
150
+ head(opts) {
151
+ return this._inner.signCheckpoint(opts);
152
+ }
153
+ /** Current number of leaves in the tree. */
154
+ size() {
155
+ return this._inner.size();
156
+ }
157
+ /** Current Merkle root hash. */
158
+ rootHash() {
159
+ return this._inner.rootHash();
160
+ }
161
+ /**
162
+ * Inclusion proof for `leafIndex` in a tree of the given size, or
163
+ * the current tree size if omitted. Per RFC 9162 §2.1.3.
164
+ */
165
+ inclusionProof(leafIndex, treeSize) {
166
+ return this._inner.getInclusionProof(leafIndex, treeSize);
167
+ }
168
+ /**
169
+ * Consistency proof between two tree sizes per RFC 9162 §2.1.4.
170
+ * `oldSize` must be `<= newSize <= size()`.
171
+ */
172
+ consistencyProof(oldSize, newSize) {
173
+ return this._inner.getConsistencyProof(oldSize, newSize);
174
+ }
175
+ /**
176
+ * Zero the stored signing-key copy. Idempotent. Subsequent calls
177
+ * to any public method throw.
178
+ */
179
+ dispose() {
180
+ this._inner.dispose();
181
+ }
182
+ }
183
+ function buildTree(hashing) {
184
+ if (hashing === 'sha256')
185
+ return new Sha256Tree(new MemoryStorage());
186
+ if (hashing === 'blake3')
187
+ return new Blake3Tree(new MemoryStorage());
188
+ throw new MerkleLogError('unsupported-hashing', `MerkleLog: hashing must be 'sha256' or 'blake3', got '${hashing}'`);
189
+ }
190
+ function resolveHasher(hashing) {
191
+ if (hashing === 'sha256')
192
+ return Sha256Hasher;
193
+ if (hashing === 'blake3')
194
+ return Blake3Hasher;
195
+ throw new MerkleLogError('unsupported-hashing', `MerkleLog: hashing must be 'sha256' or 'blake3', got '${hashing}'`);
196
+ }
197
+ function assertModulesInitialized(modules) {
198
+ const seen = new Set();
199
+ for (const mod of modules) {
200
+ if (seen.has(mod))
201
+ continue;
202
+ seen.add(mod);
203
+ if (!isInitialized(mod))
204
+ throw new MerkleLogError('module-not-initialized', `MerkleLog: WASM module '${mod}' is not initialized; `
205
+ + 'call init() with the appropriate sources before constructing MerkleLog');
206
+ }
207
+ }