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.
- package/CLAUDE.md +88 -281
- package/LICENSE +4 -0
- package/README.md +275 -87
- package/dist/aes/aes-cbc.d.ts +40 -0
- package/dist/aes/aes-cbc.js +158 -0
- package/dist/aes/aes-ctr.d.ts +50 -0
- package/dist/aes/aes-ctr.js +141 -0
- package/dist/aes/aes-gcm-siv.d.ts +67 -0
- package/dist/aes/aes-gcm-siv.js +217 -0
- package/dist/aes/aes-gcm.d.ts +61 -0
- package/dist/aes/aes-gcm.js +226 -0
- package/dist/aes/cipher-suite.d.ts +21 -0
- package/dist/aes/cipher-suite.js +179 -0
- package/dist/aes/embedded.d.ts +1 -0
- package/dist/aes/embedded.js +26 -0
- package/dist/aes/generator.d.ts +14 -0
- package/dist/aes/generator.js +103 -0
- package/dist/aes/index.d.ts +58 -0
- package/dist/aes/index.js +125 -0
- package/dist/aes/ops.d.ts +60 -0
- package/dist/aes/ops.js +164 -0
- package/dist/aes/pool-worker.d.ts +1 -0
- package/dist/aes/pool-worker.js +92 -0
- package/dist/aes/types.d.ts +1 -0
- package/dist/aes/types.js +23 -0
- package/dist/aes.wasm +0 -0
- package/dist/blake3/embedded.d.ts +1 -0
- package/dist/blake3/embedded.js +26 -0
- package/dist/blake3/index.d.ts +143 -0
- package/dist/blake3/index.js +620 -0
- package/dist/blake3/types.d.ts +102 -0
- package/dist/blake3/types.js +31 -0
- package/dist/blake3/validate.d.ts +29 -0
- package/dist/blake3/validate.js +80 -0
- package/dist/blake3.wasm +0 -0
- package/dist/chacha20/cipher-suite.d.ts +10 -0
- package/dist/chacha20/cipher-suite.js +98 -13
- package/dist/chacha20/generator.d.ts +12 -0
- package/dist/chacha20/generator.js +91 -0
- package/dist/chacha20/index.d.ts +100 -3
- package/dist/chacha20/index.js +169 -35
- package/dist/chacha20/ops.d.ts +57 -6
- package/dist/chacha20/ops.js +107 -27
- package/dist/chacha20/pool-worker.js +14 -0
- package/dist/chacha20/types.d.ts +1 -32
- package/dist/cte-wasm.d.ts +1 -0
- package/dist/cte-wasm.js +3 -0
- package/dist/cte.wasm +0 -0
- package/dist/curve25519.wasm +0 -0
- package/dist/ecdsa/der.d.ts +23 -0
- package/dist/ecdsa/der.js +192 -0
- package/dist/ecdsa/ecprivatekey-der.d.ts +32 -0
- package/dist/ecdsa/ecprivatekey-der.js +230 -0
- package/dist/ecdsa/embedded.d.ts +1 -0
- package/dist/ecdsa/embedded.js +25 -0
- package/dist/ecdsa/index.d.ts +124 -0
- package/dist/ecdsa/index.js +366 -0
- package/dist/ecdsa/types.d.ts +31 -0
- package/dist/ecdsa/types.js +28 -0
- package/dist/ecdsa/validate.d.ts +18 -0
- package/dist/ecdsa/validate.js +92 -0
- package/dist/ed25519/embedded.d.ts +1 -0
- package/dist/ed25519/embedded.js +31 -0
- package/dist/ed25519/index.d.ts +70 -0
- package/dist/ed25519/index.js +308 -0
- package/dist/ed25519/types.d.ts +27 -0
- package/dist/ed25519/types.js +27 -0
- package/dist/ed25519/validate.d.ts +7 -0
- package/dist/ed25519/validate.js +77 -0
- package/dist/embedded/aes-pool-worker.d.ts +1 -0
- package/dist/embedded/aes-pool-worker.js +5 -0
- package/dist/embedded/aes.d.ts +1 -0
- package/dist/embedded/aes.js +3 -0
- package/dist/embedded/blake3.d.ts +1 -0
- package/dist/embedded/blake3.js +3 -0
- package/dist/embedded/chacha20-pool-worker.d.ts +1 -0
- package/dist/embedded/chacha20-pool-worker.js +5 -0
- package/dist/embedded/chacha20.d.ts +1 -1
- package/dist/embedded/chacha20.js +2 -2
- package/dist/embedded/curve25519.d.ts +1 -0
- package/dist/embedded/curve25519.js +3 -0
- package/dist/embedded/mldsa.d.ts +1 -0
- package/dist/embedded/mldsa.js +3 -0
- package/dist/embedded/mlkem.d.ts +1 -0
- package/dist/embedded/mlkem.js +3 -0
- package/dist/embedded/p256.d.ts +1 -0
- package/dist/embedded/p256.js +3 -0
- package/dist/embedded/serpent-pool-worker.d.ts +1 -0
- package/dist/embedded/serpent-pool-worker.js +5 -0
- package/dist/embedded/serpent.d.ts +1 -1
- package/dist/embedded/serpent.js +2 -2
- package/dist/embedded/sha2.d.ts +1 -1
- package/dist/embedded/sha2.js +2 -2
- package/dist/embedded/sha3.d.ts +1 -1
- package/dist/embedded/sha3.js +2 -2
- package/dist/embedded/slhdsa.d.ts +1 -0
- package/dist/embedded/slhdsa.js +3 -0
- package/dist/errors.d.ts +92 -1
- package/dist/errors.js +111 -1
- package/dist/fortuna.d.ts +18 -12
- package/dist/fortuna.js +166 -99
- package/dist/index.d.ts +42 -11
- package/dist/index.js +65 -20
- package/dist/init.d.ts +1 -3
- package/dist/init.js +73 -7
- package/dist/keccak/embedded.js +1 -1
- package/dist/keccak/index.d.ts +2 -0
- package/dist/keccak/index.js +4 -2
- package/dist/loader.d.ts +1 -19
- package/dist/loader.js +26 -32
- package/dist/merkle/blake3-tree.d.ts +35 -0
- package/dist/merkle/blake3-tree.js +187 -0
- package/dist/merkle/checkpoint.d.ts +58 -0
- package/dist/merkle/checkpoint.js +217 -0
- package/dist/merkle/index.d.ts +19 -0
- package/dist/merkle/index.js +37 -0
- package/dist/merkle/merkle-log.d.ts +130 -0
- package/dist/merkle/merkle-log.js +207 -0
- package/dist/merkle/merkle-verifier.d.ts +126 -0
- package/dist/merkle/merkle-verifier.js +296 -0
- package/dist/merkle/proof.d.ts +70 -0
- package/dist/merkle/proof.js +300 -0
- package/dist/merkle/sha256-tree.d.ts +33 -0
- package/dist/merkle/sha256-tree.js +145 -0
- package/dist/merkle/signed-log.d.ts +156 -0
- package/dist/merkle/signed-log.js +356 -0
- package/dist/merkle/signed-note.d.ts +309 -0
- package/dist/merkle/signed-note.js +648 -0
- package/dist/merkle/sth.d.ts +31 -0
- package/dist/merkle/sth.js +31 -0
- package/dist/merkle/storage.d.ts +40 -0
- package/dist/merkle/storage.js +71 -0
- package/dist/merkle/tree.d.ts +68 -0
- package/dist/merkle/tree.js +94 -0
- package/dist/mldsa/embedded.d.ts +1 -0
- package/dist/{kyber → mldsa}/embedded.js +5 -5
- package/dist/mldsa/expand.d.ts +53 -0
- package/dist/mldsa/expand.js +188 -0
- package/dist/mldsa/format.d.ts +16 -0
- package/dist/mldsa/format.js +68 -0
- package/dist/mldsa/hashvariant.d.ts +32 -0
- package/dist/mldsa/hashvariant.js +248 -0
- package/dist/mldsa/index.d.ts +142 -0
- package/dist/mldsa/index.js +463 -0
- package/dist/mldsa/keygen.d.ts +16 -0
- package/dist/mldsa/keygen.js +232 -0
- package/dist/mldsa/params.d.ts +21 -0
- package/dist/mldsa/params.js +55 -0
- package/dist/mldsa/sha3-helpers.d.ts +30 -0
- package/dist/mldsa/sha3-helpers.js +124 -0
- package/dist/mldsa/sign.d.ts +36 -0
- package/dist/mldsa/sign.js +380 -0
- package/dist/mldsa/types.d.ts +91 -0
- package/dist/mldsa/types.js +25 -0
- package/dist/mldsa/validate.d.ts +55 -0
- package/dist/mldsa/validate.js +125 -0
- package/dist/mldsa/verify.d.ts +29 -0
- package/dist/mldsa/verify.js +269 -0
- package/dist/mldsa.wasm +0 -0
- package/dist/mlkem/embedded.d.ts +1 -0
- package/dist/mlkem/embedded.js +27 -0
- package/dist/mlkem/indcpa.d.ts +49 -0
- package/dist/{kyber → mlkem}/indcpa.js +48 -48
- package/dist/mlkem/index.d.ts +37 -0
- package/dist/{kyber → mlkem}/index.js +41 -31
- package/dist/mlkem/kem.d.ts +21 -0
- package/dist/{kyber → mlkem}/kem.js +48 -13
- package/dist/{kyber → mlkem}/params.d.ts +4 -4
- package/dist/{kyber → mlkem}/params.js +2 -2
- package/dist/mlkem/suite.d.ts +12 -0
- package/dist/{kyber → mlkem}/suite.js +17 -12
- package/dist/{kyber → mlkem}/types.d.ts +4 -3
- package/dist/{kyber → mlkem}/types.js +1 -1
- package/dist/mlkem/validate.d.ts +23 -0
- package/dist/{kyber → mlkem}/validate.js +24 -20
- package/dist/{kyber.wasm → mlkem.wasm} +0 -0
- package/dist/p256.wasm +0 -0
- package/dist/ratchet/index.d.ts +8 -0
- package/dist/ratchet/index.js +38 -0
- package/dist/ratchet/kdf-chain.d.ts +13 -0
- package/dist/ratchet/kdf-chain.js +85 -0
- package/dist/ratchet/ratchet-keypair.d.ts +9 -0
- package/dist/ratchet/ratchet-keypair.js +61 -0
- package/dist/ratchet/root-kdf.d.ts +4 -0
- package/dist/ratchet/root-kdf.js +124 -0
- package/dist/ratchet/skipped-key-store.d.ts +14 -0
- package/dist/ratchet/skipped-key-store.js +154 -0
- package/dist/ratchet/types.d.ts +36 -0
- package/dist/ratchet/types.js +26 -0
- package/dist/serpent/cipher-suite.d.ts +10 -0
- package/dist/serpent/cipher-suite.js +144 -56
- package/dist/serpent/generator.d.ts +12 -0
- package/dist/serpent/generator.js +97 -0
- package/dist/serpent/index.d.ts +62 -1
- package/dist/serpent/index.js +97 -21
- package/dist/serpent/pool-worker.js +28 -102
- package/dist/serpent/serpent-cbc.d.ts +16 -6
- package/dist/serpent/serpent-cbc.js +58 -37
- package/dist/serpent/shared-ops.d.ts +63 -0
- package/dist/serpent/shared-ops.js +178 -0
- package/dist/serpent/types.d.ts +1 -5
- package/dist/serpent.wasm +0 -0
- package/dist/sha2/hash.d.ts +2 -0
- package/dist/sha2/hash.js +53 -0
- package/dist/sha2/hkdf.js +5 -5
- package/dist/sha2/index.d.ts +22 -1
- package/dist/sha2/index.js +80 -11
- package/dist/sha2/types.d.ts +41 -2
- package/dist/sha2.wasm +0 -0
- package/dist/sha3/hash.d.ts +2 -0
- package/dist/sha3/hash.js +53 -0
- package/dist/sha3/index.d.ts +87 -3
- package/dist/sha3/index.js +317 -19
- package/dist/sha3/kmac.d.ts +121 -0
- package/dist/sha3/kmac.js +800 -0
- package/dist/sha3.wasm +0 -0
- package/dist/shared/pkcs7.d.ts +22 -0
- package/dist/shared/pkcs7.js +84 -0
- package/dist/sign/ctx.d.ts +41 -0
- package/dist/sign/ctx.js +102 -0
- package/dist/sign/envelope.d.ts +45 -0
- package/dist/sign/envelope.js +152 -0
- package/dist/sign/hasher.d.ts +9 -0
- package/dist/sign/hasher.js +132 -0
- package/dist/sign/index.d.ts +11 -0
- package/dist/sign/index.js +34 -0
- package/dist/sign/sign-stream.d.ts +25 -0
- package/dist/sign/sign-stream.js +112 -0
- package/dist/sign/suites/ecdsa-p256.d.ts +2 -0
- package/dist/sign/suites/ecdsa-p256.js +120 -0
- package/dist/sign/suites/ed25519.d.ts +3 -0
- package/dist/sign/suites/ed25519.js +165 -0
- package/dist/sign/suites/hybrid-classical.d.ts +23 -0
- package/dist/sign/suites/hybrid-classical.js +526 -0
- package/dist/sign/suites/hybrid-pq.d.ts +4 -0
- package/dist/sign/suites/hybrid-pq.js +234 -0
- package/dist/sign/suites/mldsa.d.ts +7 -0
- package/dist/sign/suites/mldsa.js +161 -0
- package/dist/sign/suites/slhdsa.d.ts +7 -0
- package/dist/sign/suites/slhdsa.js +176 -0
- package/dist/sign/types.d.ts +106 -0
- package/dist/sign/types.js +28 -0
- package/dist/sign/verify-stream.d.ts +30 -0
- package/dist/sign/verify-stream.js +227 -0
- package/dist/slhdsa/embedded.d.ts +1 -0
- package/dist/slhdsa/embedded.js +26 -0
- package/dist/slhdsa/index.d.ts +149 -0
- package/dist/slhdsa/index.js +493 -0
- package/dist/slhdsa/params.d.ts +26 -0
- package/dist/slhdsa/params.js +70 -0
- package/dist/slhdsa/prehash.d.ts +68 -0
- package/dist/slhdsa/prehash.js +307 -0
- package/dist/slhdsa/sign.d.ts +39 -0
- package/dist/slhdsa/sign.js +116 -0
- package/dist/slhdsa/types.d.ts +129 -0
- package/dist/slhdsa/types.js +27 -0
- package/dist/slhdsa/validate.d.ts +60 -0
- package/dist/slhdsa/validate.js +127 -0
- package/dist/slhdsa/verify.d.ts +32 -0
- package/dist/slhdsa/verify.js +107 -0
- package/dist/slhdsa.wasm +0 -0
- package/dist/stream/header.js +8 -8
- package/dist/stream/index.d.ts +1 -0
- package/dist/stream/index.js +1 -0
- package/dist/stream/open-stream.js +65 -22
- package/dist/stream/seal-stream-pool.d.ts +2 -0
- package/dist/stream/seal-stream-pool.js +100 -33
- package/dist/stream/seal-stream.d.ts +1 -1
- package/dist/stream/seal-stream.js +48 -19
- package/dist/stream/seal.js +6 -6
- package/dist/stream/types.d.ts +3 -1
- package/dist/stream/types.js +1 -1
- package/dist/types.d.ts +22 -1
- package/dist/types.js +1 -1
- package/dist/utils.d.ts +9 -10
- package/dist/utils.js +84 -59
- package/dist/wasm-source.d.ts +9 -8
- package/dist/wasm-source.js +1 -1
- package/dist/x25519/embedded.d.ts +1 -0
- package/dist/x25519/embedded.js +31 -0
- package/dist/x25519/index.d.ts +43 -0
- package/dist/x25519/index.js +159 -0
- package/dist/x25519/types.d.ts +25 -0
- package/dist/x25519/types.js +27 -0
- package/dist/x25519/validate.d.ts +2 -0
- package/dist/x25519/validate.js +39 -0
- package/package.json +123 -64
- package/SECURITY.md +0 -276
- package/dist/ct-wasm.d.ts +0 -1
- package/dist/ct-wasm.js +0 -3
- package/dist/ct.wasm +0 -0
- package/dist/docs/aead.md +0 -323
- package/dist/docs/architecture.md +0 -932
- package/dist/docs/argon2id.md +0 -302
- package/dist/docs/chacha20.md +0 -674
- package/dist/docs/exports.md +0 -241
- package/dist/docs/fortuna.md +0 -313
- package/dist/docs/init.md +0 -302
- package/dist/docs/loader.md +0 -161
- package/dist/docs/serpent.md +0 -519
- package/dist/docs/sha2.md +0 -613
- package/dist/docs/sha3.md +0 -546
- package/dist/docs/types.md +0 -276
- package/dist/docs/utils.md +0 -367
- package/dist/embedded/kyber.d.ts +0 -1
- package/dist/embedded/kyber.js +0 -3
- package/dist/kyber/embedded.d.ts +0 -1
- package/dist/kyber/indcpa.d.ts +0 -49
- package/dist/kyber/index.d.ts +0 -38
- package/dist/kyber/kem.d.ts +0 -21
- package/dist/kyber/suite.d.ts +0 -13
- 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
|
+
}
|