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,526 @@
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/sign/suites/hybrid-classical.ts
23
+ //
24
+ // Composite classical+PQ hybrid suites,
25
+ // draft-ietf-lamps-pq-composite-sigs-19 (composite-sigs). Two internal
26
+ // factories (Ed25519, ECDSA-P256) build the four exported
27
+ // StreamableSignatureSuite consts 0x20..0x23:
28
+ //
29
+ // 0x20 MlDsa44Ed25519Suite OID 1.3.6.1.5.5.7.6.39
30
+ // 0x21 MlDsa65Ed25519Suite OID 1.3.6.1.5.5.7.6.48
31
+ // 0x22 MlDsa44EcdsaP256Suite OID 1.3.6.1.5.5.7.6.40
32
+ // 0x23 MlDsa65EcdsaP256Suite OID 1.3.6.1.5.5.7.6.45
33
+ //
34
+ // composite-sigs §2.2 / §3.2 construction:
35
+ // M' = Prefix || Label || len(ctx) || ctx || PH(M)
36
+ // sig_pq = ML-DSA.Sign(sk_pq_expanded, M', ctx=Label)
37
+ // pure (FIPS 204 §5.2 Algorithm 2), NOT HashML-DSA
38
+ // (composite-sigs §2.1)
39
+ // sig_trad = Trad.Sign(sk_trad, M')
40
+ // Ed25519: RFC 8032 §5.1.6.
41
+ // ECDSA-P256: FIPS 186-5 §6.4 over SHA-256(M'),
42
+ // composite-sigs §6 `ecdsa-with-SHA256`.
43
+ // sig = sig_pq || sig_trad (composite-sigs §4.3, PQ-first).
44
+ //
45
+ // sk_pq is the 32-byte ML-DSA seed only; expanded sk is re-derived
46
+ // per sign via FIPS 204 §6.1 KeyGen_internal (composite-sigs §4.2).
47
+ // user_ctx cap = 255 (composite-sigs §3.2 step 1, FIPS 204 §3.6.1
48
+ // match); overflow throws SigningError('sig-ctx-too-long').
49
+ //
50
+ // verifyPrehashed runs BOTH sub-verifies before AND-reducing.
51
+ // composite-sigs §3.3 permits early-fail on the ML-DSA half;
52
+ // leviathan declines for parity with hybrid-pq.ts.
53
+ //
54
+ // Wire layout, OID table, M' breakdown, hedged posture, and
55
+ // constant-time discipline:
56
+ // docs/signaturesuite.md#classicalpq-hybrid-composite-encoding.
57
+ import { concat, randomBytes, utf8ToBytes, wipe } from '../../utils.js';
58
+ import { SigningError } from '../../errors.js';
59
+ import { MlDsa44, MlDsa65, MLDSA44, MLDSA65, } from '../../mldsa/index.js';
60
+ import { Ed25519 } from '../../ed25519/index.js';
61
+ import { EcdsaP256, encodeEcPrivateKey, decodeEcPrivateKey, } from '../../ecdsa/index.js';
62
+ import { ecdsaSignatureToDer, ecdsaSignatureFromDer } from '../../ecdsa/der.js';
63
+ import { CTX_DOMAIN_MAX } from '../ctx.js';
64
+ import { sha256OneShot, sha512OneShot } from '../hasher.js';
65
+ // ── Module-level constants ─────────────────────────────────────────────────
66
+ // composite-sigs §2.2, Prefix wedged at the head of every M'.
67
+ const COMPOSITE_PREFIX = utf8ToBytes('CompositeAlgorithmSignatures2025');
68
+ const COMPOSITE_USER_CTX_MAX = 255;
69
+ const LABEL_MLDSA44_ED25519 = utf8ToBytes('COMPSIG-MLDSA44-Ed25519-SHA512');
70
+ const LABEL_MLDSA65_ED25519 = utf8ToBytes('COMPSIG-MLDSA65-Ed25519-SHA512');
71
+ const LABEL_MLDSA44_ECDSA_P256 = utf8ToBytes('COMPSIG-MLDSA44-ECDSA-P256-SHA256');
72
+ const LABEL_MLDSA65_ECDSA_P256 = utf8ToBytes('COMPSIG-MLDSA65-ECDSA-P256-SHA512');
73
+ // secp256r1 group order n (SP 800-186 §3.2.1.3). Used by the composite
74
+ // ECDSA verify path to normalise high-S to low-S before delegating to
75
+ // EcdsaP256.verify (composite-sigs §3.3 permits both s and n - s under
76
+ // FIPS 186-5 §6.5; leviathan's standalone verify is strict low-S).
77
+ // Rationale: docs/signaturesuite.md#composite-ecdsa-low-s.
78
+ const SECP256R1_N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
79
+ const SECP256R1_HALF_N = SECP256R1_N >> 1n;
80
+ function be32ToBig(b) {
81
+ let x = 0n;
82
+ for (const v of b)
83
+ x = (x << 8n) | BigInt(v);
84
+ return x;
85
+ }
86
+ function bigToBe32(x) {
87
+ const out = new Uint8Array(32);
88
+ for (let i = 31; i >= 0; i--) {
89
+ out[i] = Number(x & 0xffn);
90
+ x >>= 8n;
91
+ }
92
+ return out;
93
+ }
94
+ /**
95
+ * Normalise raw 64-byte (r, s) to low-S (FIPS 186-5 §6.5).
96
+ * Rationale: docs/signaturesuite.md#composite-ecdsa-low-s.
97
+ */
98
+ function normaliseEcdsaSigLowS(raw64) {
99
+ const s = be32ToBig(raw64.subarray(32, 64));
100
+ if (s <= SECP256R1_HALF_N)
101
+ return raw64;
102
+ const sLow = SECP256R1_N - s;
103
+ const out = new Uint8Array(64);
104
+ out.set(raw64.subarray(0, 32), 0);
105
+ out.set(bigToBe32(sLow), 32);
106
+ return out;
107
+ }
108
+ // ── M' construction ────────────────────────────────────────────────────────
109
+ /**
110
+ * composite-sigs §3.2 step 2:
111
+ * M' := Prefix || Label || len(ctx) || ctx || PH(M)
112
+ *
113
+ * `len(ctx)` is encoded as a single unsigned byte (composite-sigs §3.2
114
+ * step 2; Appendix D worked example confirms `len(ctx) == 0x00` for empty
115
+ * context and a single byte for non-empty). The caller is responsible for
116
+ * the `ctx.length <= COMPOSITE_USER_CTX_MAX` check.
117
+ */
118
+ function buildMPrime(label, ctx, digest) {
119
+ const out = new Uint8Array(COMPOSITE_PREFIX.length + label.length + 1 + ctx.length + digest.length);
120
+ let p = 0;
121
+ out.set(COMPOSITE_PREFIX, p);
122
+ p += COMPOSITE_PREFIX.length;
123
+ out.set(label, p);
124
+ p += label.length;
125
+ out[p++] = ctx.length;
126
+ out.set(ctx, p);
127
+ p += ctx.length;
128
+ out.set(digest, p);
129
+ return out;
130
+ }
131
+ // ── ML-DSA + Ed25519 factory ───────────────────────────────────────────────
132
+ function MldsaEd25519HybridSuite(MlDsaClass, mldsaParams, formatEnum, formatName, ctxDomain, label) {
133
+ if (utf8ToBytes(ctxDomain).length > CTX_DOMAIN_MAX)
134
+ throw new Error(`leviathan-crypto: ctxDomain '${ctxDomain}' too long for ${formatName}`);
135
+ const wasmModules = Object.freeze(['mldsa', 'sha3', 'curve25519', 'sha2']);
136
+ const prehashAlgorithm = 'sha-512';
137
+ const prehashSize = 64;
138
+ const pkSize = mldsaParams.pkBytes + 32;
139
+ const skSize = 32 + 32;
140
+ // composite-sigs Appendix A Table 4 + RFC 8032 §5.1.6: fixed 64-byte Ed25519 sig.
141
+ const sigMaxSize = mldsaParams.sigBytes + 64;
142
+ return {
143
+ formatEnum,
144
+ formatName,
145
+ ctxDomain,
146
+ pkSize,
147
+ skSize,
148
+ sigMaxSize,
149
+ wasmModules,
150
+ prehashAlgorithm,
151
+ prehashSize,
152
+ sign(sk, msg, ctx) {
153
+ // One-shot SHA-512 over the message; the composite PH for these
154
+ // two suites is SHA-512 (composite-sigs §6).
155
+ const digest = sha512OneShot(msg);
156
+ try {
157
+ return this.signPrehashed(sk, digest, ctx);
158
+ }
159
+ finally {
160
+ wipe(digest);
161
+ }
162
+ },
163
+ verify(pk, msg, sig, ctx) {
164
+ const digest = sha512OneShot(msg);
165
+ try {
166
+ return this.verifyPrehashed(pk, digest, sig, ctx);
167
+ }
168
+ finally {
169
+ wipe(digest);
170
+ }
171
+ },
172
+ keygen() {
173
+ const seedMldsa = randomBytes(32);
174
+ let mldsaPk;
175
+ const mldsaInst = new MlDsaClass();
176
+ try {
177
+ const kp = mldsaInst.keygenDerand(seedMldsa);
178
+ mldsaPk = kp.verificationKey;
179
+ wipe(kp.signingKey);
180
+ }
181
+ finally {
182
+ mldsaInst.dispose();
183
+ }
184
+ const edInst = new Ed25519();
185
+ let edPk;
186
+ let seedEd;
187
+ try {
188
+ const kp = edInst.keygen();
189
+ edPk = kp.publicKey;
190
+ seedEd = kp.secretKey;
191
+ }
192
+ finally {
193
+ edInst.dispose();
194
+ }
195
+ try {
196
+ return {
197
+ pk: concat(mldsaPk, edPk),
198
+ sk: concat(seedMldsa, seedEd),
199
+ };
200
+ }
201
+ finally {
202
+ wipe(seedMldsa);
203
+ wipe(seedEd);
204
+ }
205
+ },
206
+ signPrehashed(sk, digest, ctx) {
207
+ if (digest.length !== prehashSize)
208
+ throw new SigningError('sig-malformed-input', `digest length ${digest.length} != ${prehashSize} for ${formatName}`);
209
+ if (sk.length !== skSize)
210
+ throw new SigningError('sig-key-size', `sk length ${sk.length} != ${skSize} for ${formatName}`);
211
+ if (ctx.length > COMPOSITE_USER_CTX_MAX)
212
+ throw new SigningError('sig-ctx-too-long', `user_ctx length ${ctx.length} > ${COMPOSITE_USER_CTX_MAX} `
213
+ + '(composite-sigs §3.2 step 1)');
214
+ const seedMldsa = sk.subarray(0, 32);
215
+ const seedEd = sk.subarray(32, 64);
216
+ const mPrime = buildMPrime(label, ctx, digest);
217
+ try {
218
+ // composite-sigs §4.2: re-derive expanded sk from 32-byte
219
+ // seed (FIPS 204 §6.1 KeyGen_internal); sign with
220
+ // mldsa_ctx = Label (composite-sigs §3.2 step 4).
221
+ let sigMldsa;
222
+ const mldsaInst = new MlDsaClass();
223
+ let expandedSk = null;
224
+ try {
225
+ const kp = mldsaInst.keygenDerand(seedMldsa);
226
+ expandedSk = kp.signingKey;
227
+ wipe(kp.verificationKey);
228
+ sigMldsa = mldsaInst.sign(expandedSk, mPrime, label);
229
+ }
230
+ finally {
231
+ if (expandedSk)
232
+ wipe(expandedSk);
233
+ mldsaInst.dispose();
234
+ }
235
+ // Ed25519 half: pure sign over M', composite-sigs §3.2 step 4 + RFC 8032 §5.1.6.
236
+ let sigEd;
237
+ const edInst = new Ed25519();
238
+ try {
239
+ sigEd = edInst._signInternalPk(seedEd, mPrime);
240
+ }
241
+ finally {
242
+ edInst.dispose();
243
+ }
244
+ // composite-sigs §4.3 SerializeSignatureValue: PQ-first,
245
+ // no length prefix.
246
+ return concat(sigMldsa, sigEd);
247
+ }
248
+ finally {
249
+ wipe(mPrime);
250
+ }
251
+ },
252
+ verifyPrehashed(pk, digest, sig, ctx) {
253
+ // Wire-shape rejects → false (attacker-observable bytes;
254
+ // composite-sigs §3.3 returns "Invalid signature").
255
+ if (pk.length !== pkSize)
256
+ return false;
257
+ if (sig.length !== sigMaxSize)
258
+ return false;
259
+ // Caller-side contract violations throw, symmetric with the
260
+ // sign side: digest length and ctx length are properties of
261
+ // the caller's input shape, not signature validity.
262
+ if (digest.length !== prehashSize)
263
+ throw new SigningError('sig-malformed-input', `digest length ${digest.length} != ${prehashSize} for ${formatName}`);
264
+ if (ctx.length > COMPOSITE_USER_CTX_MAX)
265
+ throw new SigningError('sig-ctx-too-long', `user_ctx length ${ctx.length} > ${COMPOSITE_USER_CTX_MAX} `
266
+ + '(composite-sigs §3.2 step 1)');
267
+ const pkMldsa = pk.subarray(0, mldsaParams.pkBytes);
268
+ const pkEd = pk.subarray(mldsaParams.pkBytes);
269
+ const sigMldsa = sig.subarray(0, mldsaParams.sigBytes);
270
+ const sigEd = sig.subarray(mldsaParams.sigBytes);
271
+ const mPrime = buildMPrime(label, ctx, digest);
272
+ // Both sub-verifies must run before AND-reduce
273
+ // (constant-time gate).
274
+ let mldsaOk;
275
+ let edOk;
276
+ try {
277
+ const mldsaInst = new MlDsaClass();
278
+ try {
279
+ mldsaOk = mldsaInst.verify(pkMldsa, mPrime, sigMldsa, label);
280
+ }
281
+ finally {
282
+ mldsaInst.dispose();
283
+ }
284
+ const edInst = new Ed25519();
285
+ try {
286
+ edOk = edInst.verify(pkEd, mPrime, sigEd);
287
+ }
288
+ finally {
289
+ edInst.dispose();
290
+ }
291
+ }
292
+ finally {
293
+ wipe(mPrime);
294
+ }
295
+ // Do NOT wipe sub-sig / sub-pk subarrays; they alias caller-owned buffers.
296
+ return mldsaOk && edOk;
297
+ },
298
+ };
299
+ }
300
+ // ── ML-DSA + ECDSA-P256 factory ────────────────────────────────────────────
301
+ function MldsaEcdsaP256HybridSuite(MlDsaClass, mldsaParams, formatEnum, formatName, ctxDomain, label, prehashAlgorithm, prehashSize) {
302
+ if (utf8ToBytes(ctxDomain).length > CTX_DOMAIN_MAX)
303
+ throw new Error(`leviathan-crypto: ctxDomain '${ctxDomain}' too long for ${formatName}`);
304
+ // 'p256' drives ECDSA; 'sha2' covers both the composite PH (SHA-256
305
+ // for 0x22, SHA-512 for 0x23) and ECDSA's internal SHA-256(M').
306
+ const wasmModules = Object.freeze(['mldsa', 'sha3', 'p256', 'sha2']);
307
+ const pkSize = mldsaParams.pkBytes + 65;
308
+ const skSize = 32 + 51;
309
+ // composite-sigs Appendix A Table 4 + RFC 3279 §2.2.3, 72-byte DER ceiling.
310
+ const sigMaxSize = mldsaParams.sigBytes + 72;
311
+ return {
312
+ formatEnum,
313
+ formatName,
314
+ ctxDomain,
315
+ pkSize,
316
+ skSize,
317
+ sigMaxSize,
318
+ wasmModules,
319
+ prehashAlgorithm,
320
+ prehashSize,
321
+ sign(sk, msg, ctx) {
322
+ const digest = prehashAlgorithm === 'sha-256'
323
+ ? sha256OneShot(msg)
324
+ : sha512OneShot(msg);
325
+ try {
326
+ return this.signPrehashed(sk, digest, ctx);
327
+ }
328
+ finally {
329
+ wipe(digest);
330
+ }
331
+ },
332
+ verify(pk, msg, sig, ctx) {
333
+ const digest = prehashAlgorithm === 'sha-256'
334
+ ? sha256OneShot(msg)
335
+ : sha512OneShot(msg);
336
+ try {
337
+ return this.verifyPrehashed(pk, digest, sig, ctx);
338
+ }
339
+ finally {
340
+ wipe(digest);
341
+ }
342
+ },
343
+ keygen() {
344
+ const seedMldsa = randomBytes(32);
345
+ let mldsaPk;
346
+ const mldsaInst = new MlDsaClass();
347
+ try {
348
+ const kp = mldsaInst.keygenDerand(seedMldsa);
349
+ mldsaPk = kp.verificationKey;
350
+ wipe(kp.signingKey);
351
+ }
352
+ finally {
353
+ mldsaInst.dispose();
354
+ }
355
+ // ECDSA-P256 half: 65-byte uncompressed pk (SEC 1 §2.3.4,
356
+ // composite-sigs §4) and 51-byte DER ECPrivateKey
357
+ // (RFC 5915 §3, composite-sigs §4.2).
358
+ let ecPk;
359
+ let ecScalar;
360
+ const ecInst = new EcdsaP256();
361
+ try {
362
+ const kp = ecInst.keygenUncompressed();
363
+ ecPk = kp.publicKey;
364
+ ecScalar = kp.secretKey;
365
+ }
366
+ finally {
367
+ ecInst.dispose();
368
+ }
369
+ let ecDer;
370
+ try {
371
+ ecDer = encodeEcPrivateKey(ecScalar);
372
+ }
373
+ finally {
374
+ wipe(ecScalar);
375
+ }
376
+ try {
377
+ return {
378
+ pk: concat(mldsaPk, ecPk),
379
+ sk: concat(seedMldsa, ecDer),
380
+ };
381
+ }
382
+ finally {
383
+ wipe(seedMldsa);
384
+ wipe(ecDer);
385
+ }
386
+ },
387
+ signPrehashed(sk, digest, ctx) {
388
+ if (digest.length !== prehashSize)
389
+ throw new SigningError('sig-malformed-input', `digest length ${digest.length} != ${prehashSize} for ${formatName}`);
390
+ if (sk.length !== skSize)
391
+ throw new SigningError('sig-key-size', `sk length ${sk.length} != ${skSize} for ${formatName}`);
392
+ if (ctx.length > COMPOSITE_USER_CTX_MAX)
393
+ throw new SigningError('sig-ctx-too-long', `user_ctx length ${ctx.length} > ${COMPOSITE_USER_CTX_MAX} `
394
+ + '(composite-sigs §3.2 step 1)');
395
+ const seedMldsa = sk.subarray(0, 32);
396
+ const ecDer = sk.subarray(32, 83);
397
+ // RFC 5915 §3 ECPrivateKey decode; strict DER per the codec's
398
+ // X.690 §10 hygiene rules. Throws on syntax violation.
399
+ const ecScalar = decodeEcPrivateKey(ecDer);
400
+ const mPrime = buildMPrime(label, ctx, digest);
401
+ // composite-sigs §6, ECDSA half is SHA-256(M') for both 0x22 and 0x23.
402
+ const ecDigest = sha256OneShot(mPrime);
403
+ const rnd = randomBytes(32);
404
+ try {
405
+ let sigMldsa;
406
+ const mldsaInst = new MlDsaClass();
407
+ let expandedSk = null;
408
+ try {
409
+ const kp = mldsaInst.keygenDerand(seedMldsa);
410
+ expandedSk = kp.signingKey;
411
+ wipe(kp.verificationKey);
412
+ sigMldsa = mldsaInst.sign(expandedSk, mPrime, label);
413
+ }
414
+ finally {
415
+ if (expandedSk)
416
+ wipe(expandedSk);
417
+ mldsaInst.dispose();
418
+ }
419
+ let sigEcRaw;
420
+ const ecInst = new EcdsaP256();
421
+ try {
422
+ sigEcRaw = ecInst._signInternalPk(ecScalar, ecDigest, rnd);
423
+ }
424
+ finally {
425
+ ecInst.dispose();
426
+ }
427
+ let sigEcDer;
428
+ try {
429
+ // composite-sigs §4.3, DER on the wire.
430
+ sigEcDer = ecdsaSignatureToDer(sigEcRaw);
431
+ }
432
+ finally {
433
+ wipe(sigEcRaw);
434
+ }
435
+ return concat(sigMldsa, sigEcDer);
436
+ }
437
+ finally {
438
+ wipe(rnd);
439
+ wipe(ecScalar);
440
+ wipe(ecDigest);
441
+ wipe(mPrime);
442
+ }
443
+ },
444
+ verifyPrehashed(pk, digest, sig, ctx) {
445
+ // Wire-shape rejects → false. 8-byte DER floor per RFC 3279 §2.2.3.
446
+ if (pk.length !== pkSize)
447
+ return false;
448
+ if (sig.length < mldsaParams.sigBytes + 8)
449
+ return false;
450
+ if (sig.length > sigMaxSize)
451
+ return false;
452
+ if (digest.length !== prehashSize)
453
+ throw new SigningError('sig-malformed-input', `digest length ${digest.length} != ${prehashSize} for ${formatName}`);
454
+ if (ctx.length > COMPOSITE_USER_CTX_MAX)
455
+ throw new SigningError('sig-ctx-too-long', `user_ctx length ${ctx.length} > ${COMPOSITE_USER_CTX_MAX} `
456
+ + '(composite-sigs §3.2 step 1)');
457
+ const pkMldsa = pk.subarray(0, mldsaParams.pkBytes);
458
+ const pkEc = pk.subarray(mldsaParams.pkBytes, mldsaParams.pkBytes + 65);
459
+ const sigMldsa = sig.subarray(0, mldsaParams.sigBytes);
460
+ const sigEcDer = sig.subarray(mldsaParams.sigBytes);
461
+ const mPrime = buildMPrime(label, ctx, digest);
462
+ const ecDigest = sha256OneShot(mPrime);
463
+ let mldsaOk;
464
+ let ecOk;
465
+ try {
466
+ const mldsaInst = new MlDsaClass();
467
+ try {
468
+ mldsaOk = mldsaInst.verify(pkMldsa, mPrime, sigMldsa, label);
469
+ }
470
+ finally {
471
+ mldsaInst.dispose();
472
+ }
473
+ let sigEcRaw = null;
474
+ try {
475
+ sigEcRaw = ecdsaSignatureFromDer(sigEcDer);
476
+ }
477
+ catch {
478
+ sigEcRaw = null;
479
+ }
480
+ if (sigEcRaw === null) {
481
+ ecOk = false;
482
+ }
483
+ else {
484
+ const sigEcLowS = normaliseEcdsaSigLowS(sigEcRaw);
485
+ const ecInst = new EcdsaP256();
486
+ try {
487
+ ecOk = ecInst.verify(pkEc, ecDigest, sigEcLowS);
488
+ }
489
+ finally {
490
+ ecInst.dispose();
491
+ }
492
+ wipe(sigEcLowS);
493
+ wipe(sigEcRaw);
494
+ }
495
+ }
496
+ finally {
497
+ wipe(mPrime);
498
+ wipe(ecDigest);
499
+ }
500
+ return mldsaOk && ecOk;
501
+ },
502
+ };
503
+ }
504
+ // ── Exported suite consts ──────────────────────────────────────────────────
505
+ /**
506
+ * Composite ML-DSA-44 + Ed25519 with SHA-512 prehash.
507
+ * composite-sigs §6, id-MLDSA44-Ed25519-SHA512 (OID 1.3.6.1.5.5.7.6.39).
508
+ */
509
+ export const MlDsa44Ed25519Suite = MldsaEd25519HybridSuite(MlDsa44, MLDSA44, 0x20, 'mldsa44-ed25519', 'mldsa44-ed25519-envelope-v3', LABEL_MLDSA44_ED25519);
510
+ /**
511
+ * Composite ML-DSA-65 + Ed25519 with SHA-512 prehash.
512
+ * composite-sigs §6, id-MLDSA65-Ed25519-SHA512 (OID 1.3.6.1.5.5.7.6.48).
513
+ */
514
+ export const MlDsa65Ed25519Suite = MldsaEd25519HybridSuite(MlDsa65, MLDSA65, 0x21, 'mldsa65-ed25519', 'mldsa65-ed25519-envelope-v3', LABEL_MLDSA65_ED25519);
515
+ /**
516
+ * Composite ML-DSA-44 + ECDSA-P256 with SHA-256 prehash.
517
+ * composite-sigs §6, id-MLDSA44-ECDSA-P256-SHA256 (OID 1.3.6.1.5.5.7.6.40).
518
+ */
519
+ export const MlDsa44EcdsaP256Suite = MldsaEcdsaP256HybridSuite(MlDsa44, MLDSA44, 0x22, 'mldsa44-ecdsa-p256', 'mldsa44-ecdsa-p256-envelope-v3', LABEL_MLDSA44_ECDSA_P256, 'sha-256', 32);
520
+ /**
521
+ * Composite ML-DSA-65 + ECDSA-P256 with SHA-512 prehash on the composite
522
+ * layer; the ECDSA half still hashes M' with SHA-256 per composite-sigs §6
523
+ * `ecdsa-with-SHA256` and §10.1 (deployment-fit rationale).
524
+ * composite-sigs §6, id-MLDSA65-ECDSA-P256-SHA512 (OID 1.3.6.1.5.5.7.6.45).
525
+ */
526
+ export const MlDsa65EcdsaP256Suite = MldsaEcdsaP256HybridSuite(MlDsa65, MLDSA65, 0x23, 'mldsa65-ecdsa-p256', 'mldsa65-ecdsa-p256-envelope-v3', LABEL_MLDSA65_ECDSA_P256, 'sha-512', 64);
@@ -0,0 +1,4 @@
1
+ import type { StreamableSignatureSuite } from '../types.js';
2
+ export declare const MlDsa44SlhDsa128fSuite: StreamableSignatureSuite;
3
+ export declare const MlDsa65SlhDsa192fSuite: StreamableSignatureSuite;
4
+ export declare const MlDsa87SlhDsa256fSuite: StreamableSignatureSuite;