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.
- package/CLAUDE.md +86 -443
- package/README.md +198 -65
- 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.js +47 -25
- package/dist/chacha20/generator.d.ts +2 -2
- package/dist/chacha20/generator.js +4 -4
- package/dist/chacha20/index.d.ts +16 -15
- package/dist/chacha20/index.js +52 -46
- package/dist/chacha20/ops.d.ts +7 -7
- package/dist/chacha20/ops.js +34 -34
- package/dist/chacha20/pool-worker.js +5 -3
- package/dist/cte-wasm.d.ts +1 -0
- package/dist/cte-wasm.js +3 -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 -1
- package/dist/embedded/chacha20-pool-worker.js +2 -2
- 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 -1
- package/dist/embedded/serpent-pool-worker.js +2 -2
- 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 +5 -5
- package/dist/fortuna.js +37 -64
- package/dist/index.d.ts +38 -9
- package/dist/index.js +63 -19
- package/dist/init.d.ts +1 -1
- package/dist/init.js +11 -25
- 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 -24
- package/dist/loader.js +13 -16
- 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 +44 -44
- package/dist/mlkem/index.d.ts +37 -0
- package/dist/{kyber → mlkem}/index.js +24 -34
- package/dist/mlkem/kem.d.ts +21 -0
- package/dist/{kyber → mlkem}/kem.js +44 -64
- 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 +3 -3
- package/dist/{kyber → mlkem}/types.js +1 -1
- package/dist/{kyber → mlkem}/validate.d.ts +7 -7
- package/dist/{kyber → mlkem}/validate.js +7 -7
- package/dist/{kyber.wasm → mlkem.wasm} +0 -0
- package/dist/p256.wasm +0 -0
- package/dist/ratchet/index.d.ts +2 -0
- package/dist/ratchet/index.js +1 -0
- package/dist/ratchet/kdf-chain.js +3 -3
- package/dist/ratchet/ratchet-keypair.js +2 -2
- package/dist/ratchet/root-kdf.js +7 -7
- package/dist/ratchet/skipped-key-store.js +4 -4
- package/dist/ratchet/types.d.ts +1 -1
- package/dist/serpent/cipher-suite.js +20 -17
- package/dist/serpent/generator.d.ts +1 -1
- package/dist/serpent/generator.js +2 -2
- package/dist/serpent/index.d.ts +8 -7
- package/dist/serpent/index.js +18 -27
- package/dist/serpent/pool-worker.js +7 -5
- package/dist/serpent/serpent-cbc.d.ts +4 -4
- package/dist/serpent/serpent-cbc.js +11 -8
- package/dist/serpent/shared-ops.d.ts +3 -23
- package/dist/serpent/shared-ops.js +50 -85
- package/dist/serpent.wasm +0 -0
- package/dist/sha2/hkdf.js +5 -5
- package/dist/sha2/index.d.ts +21 -1
- package/dist/sha2/index.js +65 -10
- package/dist/sha2/types.d.ts +41 -2
- package/dist/sha2.wasm +0 -0
- package/dist/sha3/index.d.ts +72 -3
- package/dist/sha3/index.js +240 -14
- 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 +3 -3
- package/dist/stream/index.d.ts +1 -0
- package/dist/stream/index.js +1 -0
- package/dist/stream/open-stream.js +31 -10
- package/dist/stream/seal-stream-pool.d.ts +1 -0
- package/dist/stream/seal-stream-pool.js +63 -26
- package/dist/stream/seal-stream.d.ts +1 -1
- package/dist/stream/seal-stream.js +20 -9
- 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 +1 -1
- package/dist/types.js +1 -1
- package/dist/utils.d.ts +3 -3
- package/dist/utils.js +46 -54
- package/dist/wasm-source.d.ts +7 -7
- 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 +70 -26
- package/SECURITY.md +0 -163
- package/dist/ct-wasm.d.ts +0 -1
- package/dist/ct-wasm.js +0 -3
- package/dist/docs/aead.md +0 -363
- package/dist/docs/architecture.md +0 -1011
- package/dist/docs/argon2id.md +0 -305
- package/dist/docs/chacha20.md +0 -781
- package/dist/docs/exports.md +0 -277
- package/dist/docs/fortuna.md +0 -530
- package/dist/docs/init.md +0 -301
- package/dist/docs/loader.md +0 -256
- package/dist/docs/serpent.md +0 -617
- package/dist/docs/sha2.md +0 -671
- package/dist/docs/sha3.md +0 -612
- package/dist/docs/types.md +0 -416
- package/dist/docs/utils.md +0 -457
- 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 -12
- /package/dist/{ct.wasm → cte.wasm} +0 -0
package/dist/loader.js
CHANGED
|
@@ -13,12 +13,13 @@ function toArrayBuffer(bytes) {
|
|
|
13
13
|
* Decode a gzip+base64 embedded WASM string to raw bytes.
|
|
14
14
|
* Guards against missing DecompressionStream (Node <18, non-browser runtimes).
|
|
15
15
|
* Exported for pool worker launchers that decode blobs before spawning threads.
|
|
16
|
+
* @internal
|
|
16
17
|
*/
|
|
17
18
|
export async function decodeWasm(b64) {
|
|
18
19
|
if (typeof DecompressionStream === 'undefined')
|
|
19
|
-
throw new Error('leviathan-crypto: DecompressionStream not available
|
|
20
|
+
throw new Error('leviathan-crypto: DecompressionStream not available, '
|
|
20
21
|
+ 'use a URL, ArrayBuffer, or WebAssembly.Module source in this runtime');
|
|
21
|
-
// _b64 throws RangeError on invalid base64
|
|
22
|
+
// _b64 throws RangeError on invalid base64, no nullish check required.
|
|
22
23
|
const compressed = _b64(b64);
|
|
23
24
|
const ds = new DecompressionStream('gzip');
|
|
24
25
|
const writer = ds.writable.getWriter();
|
|
@@ -39,10 +40,7 @@ export async function decodeWasm(b64) {
|
|
|
39
40
|
}
|
|
40
41
|
return out;
|
|
41
42
|
}
|
|
42
|
-
//
|
|
43
|
-
// `Promise<Promise<Response>>` (e.g. deferred fetch wrapped in another async
|
|
44
|
-
// layer), but arbitrary `Promise<Promise<Promise<...>>>` chains would indicate
|
|
45
|
-
// a caller bug — cap at 3 levels and throw a clear error beyond that.
|
|
43
|
+
// Cap thenable-source nesting at 3 to prevent runaway recursion.
|
|
46
44
|
const MAX_THENABLE_DEPTH = 3;
|
|
47
45
|
/**
|
|
48
46
|
* Compile a WASM source to a Module without instantiating.
|
|
@@ -51,13 +49,14 @@ const MAX_THENABLE_DEPTH = 3;
|
|
|
51
49
|
* Thenable sources (Promise<Response>, Promise<ArrayBuffer>, etc.) are
|
|
52
50
|
* resolved and then re-dispatched by the runtime type of the resolved value.
|
|
53
51
|
* Depth is capped at `MAX_THENABLE_DEPTH` to prevent runaway recursion.
|
|
52
|
+
* @internal
|
|
54
53
|
*/
|
|
55
|
-
export async function compileWasm(source,
|
|
56
|
-
if (
|
|
54
|
+
export async function compileWasm(source, depth = 0) {
|
|
55
|
+
if (depth > MAX_THENABLE_DEPTH)
|
|
57
56
|
throw new TypeError(`leviathan-crypto: thenable nesting too deep (max ${MAX_THENABLE_DEPTH})`);
|
|
58
57
|
if (typeof source === 'string') {
|
|
59
58
|
if (source.length === 0)
|
|
60
|
-
throw new TypeError('leviathan-crypto: invalid WasmSource
|
|
59
|
+
throw new TypeError('leviathan-crypto: invalid WasmSource, empty string');
|
|
61
60
|
return WebAssembly.compile(toArrayBuffer(await decodeWasm(source)));
|
|
62
61
|
}
|
|
63
62
|
if (source instanceof URL)
|
|
@@ -72,22 +71,20 @@ export async function compileWasm(source, _depth = 0) {
|
|
|
72
71
|
return WebAssembly.compileStreaming(source);
|
|
73
72
|
if (source != null && typeof source.then === 'function') {
|
|
74
73
|
const resolved = await source;
|
|
75
|
-
return compileWasm(resolved,
|
|
74
|
+
return compileWasm(resolved, depth + 1);
|
|
76
75
|
}
|
|
77
|
-
throw new TypeError(`leviathan-crypto: invalid WasmSource
|
|
76
|
+
throw new TypeError(`leviathan-crypto: invalid WasmSource, got ${source === null ? 'null' : typeof source}`);
|
|
78
77
|
}
|
|
79
78
|
/**
|
|
80
79
|
* Load a WASM module from any accepted source type.
|
|
81
|
-
* The loading strategy is inferred from the argument type
|
|
80
|
+
* The loading strategy is inferred from the argument type, no mode string.
|
|
82
81
|
*
|
|
83
82
|
* Throws `TypeError` for null, numeric, or unrecognised inputs, or if a
|
|
84
83
|
* thenable source nests deeper than `MAX_THENABLE_DEPTH`.
|
|
84
|
+
* @internal
|
|
85
85
|
*/
|
|
86
86
|
export async function loadWasm(source) {
|
|
87
|
-
// All
|
|
88
|
-
// nothing from the host. If a future module needs imports, they would be
|
|
89
|
-
// computed and passed here.
|
|
90
|
-
// compileWasm already handles thenable resolution + depth capping.
|
|
87
|
+
// All modules export their own memory; no host imports today.
|
|
91
88
|
const mod = await compileWasm(source);
|
|
92
89
|
return WebAssembly.instantiate(mod);
|
|
93
90
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Hasher, MerkleTree } from './tree.js';
|
|
2
|
+
import type { MerkleStorage } from './storage.js';
|
|
3
|
+
/**
|
|
4
|
+
* BLAKE3-native `Hasher` (BLAKE3 §2.3, §2.4, §2.5).
|
|
5
|
+
*
|
|
6
|
+
* Empty-tree value is `BLAKE3()`; leaves are `BLAKE3(leaf)`; internal
|
|
7
|
+
* nodes are the §2.5 parent compress over the two child CVs with the
|
|
8
|
+
* BLAKE3 IV as the starting CV and `modeFlags = 0`, `isRoot = 0`.
|
|
9
|
+
*
|
|
10
|
+
* Stateless and reentrant: each method acquires the blake3 module
|
|
11
|
+
* fresh, runs the operation, and releases. No `dispose()` is needed.
|
|
12
|
+
*/
|
|
13
|
+
export declare const Blake3Hasher: Hasher;
|
|
14
|
+
/**
|
|
15
|
+
* Stateful BLAKE3 Merkle log. Same surface and storage discipline as
|
|
16
|
+
* `Sha256Tree`: leaf hashes and every perfect aligned internal subtree
|
|
17
|
+
* hash live in the injected `MerkleStorage`; partial right-edge subtrees
|
|
18
|
+
* are recomputed on demand.
|
|
19
|
+
*
|
|
20
|
+
* `append` is the only mutator and is the leaf-hash factory; consumers
|
|
21
|
+
* feed leaf bytes, not pre-computed leaf hashes.
|
|
22
|
+
*/
|
|
23
|
+
export declare class Blake3Tree implements MerkleTree {
|
|
24
|
+
readonly hasher: Hasher;
|
|
25
|
+
private readonly storage;
|
|
26
|
+
constructor(storage: MerkleStorage);
|
|
27
|
+
size(): number;
|
|
28
|
+
rootHash(): Uint8Array;
|
|
29
|
+
append(leafBytes: Uint8Array): {
|
|
30
|
+
leafIndex: number;
|
|
31
|
+
leafHash: Uint8Array;
|
|
32
|
+
};
|
|
33
|
+
getInclusionProof(leafIndex: number, treeSize?: number): Uint8Array[];
|
|
34
|
+
getConsistencyProof(oldSize: number, newSize: number): Uint8Array[];
|
|
35
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
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/blake3-tree.ts
|
|
23
|
+
//
|
|
24
|
+
// BLAKE3 Hasher / MerkleTree. Domain separation comes from BLAKE3's own
|
|
25
|
+
// §2.4 CHUNK_START / CHUNK_END / ROOT and §2.5 PARENT flags, not from
|
|
26
|
+
// RFC 6962-style 0x00 / 0x01 prefix bytes (those would be redundant and
|
|
27
|
+
// discard `compress4` parallelism at the internal-node layer).
|
|
28
|
+
//
|
|
29
|
+
// Composition:
|
|
30
|
+
// hashEmpty() = BLAKE3() §2.5
|
|
31
|
+
// hashLeaf(leaf) = BLAKE3(leaf) §2.4
|
|
32
|
+
// hashInternal(left, right) = _testParentCV(left, right, IV, 0, 0) §2.5
|
|
33
|
+
//
|
|
34
|
+
// Parent compress runs with modeFlags = 0 and isRoot = 0 at every level.
|
|
35
|
+
// The root flag is the SignedLog layer's concern; the tree's top hash
|
|
36
|
+
// exits as a plain CV, keeping `hashInternal` symmetric.
|
|
37
|
+
import { BLAKE3 } from '../blake3/index.js';
|
|
38
|
+
import { getInstance } from '../init.js';
|
|
39
|
+
import { buildConsistencyProof, buildInclusionProof, subtreeHash, } from './proof.js';
|
|
40
|
+
function getBlake3Exports() {
|
|
41
|
+
return getInstance('blake3').exports;
|
|
42
|
+
}
|
|
43
|
+
// ── Scratch layout for `_testParentCV` ──────────────────────────────────────
|
|
44
|
+
//
|
|
45
|
+
// Second-page offsets (past BUFFER_END = 26328 from
|
|
46
|
+
// `src/asm/blake3/buffers.ts`) are untouched by the §2.4 chunk pipeline
|
|
47
|
+
// and §2.5 tree-assembly queues; safe for caller-supplied scratch.
|
|
48
|
+
const PARENT_LEFT_OFF = 65536;
|
|
49
|
+
const PARENT_RIGHT_OFF = PARENT_LEFT_OFF + 32;
|
|
50
|
+
const PARENT_START_OFF = PARENT_RIGHT_OFF + 32;
|
|
51
|
+
const PARENT_OUT_OFF = PARENT_START_OFF + 32;
|
|
52
|
+
const PARENT_SCRATCH_LEN = 128;
|
|
53
|
+
// BLAKE3 §2.2 Table 1: the BLAKE3 IV equals the FIPS 180-4 SHA-256 IV,
|
|
54
|
+
// packed as eight u32 little-endian words. The IV is the starting CV
|
|
55
|
+
// for default-mode parent compresses (BLAKE3 §2.5, Tree Mode).
|
|
56
|
+
const BLAKE3_IV_BYTES = (() => {
|
|
57
|
+
const iv32 = new Uint32Array([
|
|
58
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
|
|
59
|
+
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
|
60
|
+
]);
|
|
61
|
+
return new Uint8Array(iv32.buffer);
|
|
62
|
+
})();
|
|
63
|
+
const BLAKE3_OUTPUT = 32;
|
|
64
|
+
const BLAKE3_WASM_MODULES = Object.freeze(['blake3']);
|
|
65
|
+
// ── Blake3Hasher const ──────────────────────────────────────────────────────
|
|
66
|
+
/**
|
|
67
|
+
* BLAKE3-native `Hasher` (BLAKE3 §2.3, §2.4, §2.5).
|
|
68
|
+
*
|
|
69
|
+
* Empty-tree value is `BLAKE3()`; leaves are `BLAKE3(leaf)`; internal
|
|
70
|
+
* nodes are the §2.5 parent compress over the two child CVs with the
|
|
71
|
+
* BLAKE3 IV as the starting CV and `modeFlags = 0`, `isRoot = 0`.
|
|
72
|
+
*
|
|
73
|
+
* Stateless and reentrant: each method acquires the blake3 module
|
|
74
|
+
* fresh, runs the operation, and releases. No `dispose()` is needed.
|
|
75
|
+
*/
|
|
76
|
+
export const Blake3Hasher = Object.freeze({
|
|
77
|
+
name: 'blake3',
|
|
78
|
+
outputSize: BLAKE3_OUTPUT,
|
|
79
|
+
wasmModules: BLAKE3_WASM_MODULES,
|
|
80
|
+
hashEmpty() {
|
|
81
|
+
// BLAKE3 §2.5, Tree Mode: the natural tree-mode root for an
|
|
82
|
+
// empty input is `BLAKE3()` itself. The chunk machine handles
|
|
83
|
+
// the single empty chunk (CHUNK_START | CHUNK_END | ROOT)
|
|
84
|
+
// internally and returns the 32-byte XOF prefix.
|
|
85
|
+
const h = new BLAKE3();
|
|
86
|
+
try {
|
|
87
|
+
return h.hash(new Uint8Array(0));
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
h.dispose();
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
hashLeaf(leaf) {
|
|
94
|
+
// BLAKE3 §2.4, Chunks: the chunk pipeline applies CHUNK_START,
|
|
95
|
+
// CHUNK_END, and ROOT flags internally. The caller sees a plain
|
|
96
|
+
// 32-byte hash; leaf-vs-internal domain separation is BLAKE3's
|
|
97
|
+
// job through these flag bytes, not the caller's job through
|
|
98
|
+
// prefix bytes.
|
|
99
|
+
const h = new BLAKE3();
|
|
100
|
+
try {
|
|
101
|
+
return h.hash(leaf);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
h.dispose();
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
hashInternal(left, right) {
|
|
108
|
+
if (left.length !== BLAKE3_OUTPUT)
|
|
109
|
+
throw new RangeError(`Blake3Hasher.hashInternal: left must be ${BLAKE3_OUTPUT} bytes, got ${left.length}`);
|
|
110
|
+
if (right.length !== BLAKE3_OUTPUT)
|
|
111
|
+
throw new RangeError(`Blake3Hasher.hashInternal: right must be ${BLAKE3_OUTPUT} bytes, got ${right.length}`);
|
|
112
|
+
// BLAKE3 §2.5, Tree Mode: parent compress over (left || right)
|
|
113
|
+
// with IV as the starting CV and PARENT as the only flag bit
|
|
114
|
+
// (modeFlags = 0 selects default mode; isRoot = 0 keeps the
|
|
115
|
+
// node generic so callers can stack identical compresses up
|
|
116
|
+
// the tree).
|
|
117
|
+
const x = getBlake3Exports();
|
|
118
|
+
const mem = new Uint8Array(x.memory.buffer);
|
|
119
|
+
mem.set(left, PARENT_LEFT_OFF);
|
|
120
|
+
mem.set(right, PARENT_RIGHT_OFF);
|
|
121
|
+
mem.set(BLAKE3_IV_BYTES, PARENT_START_OFF);
|
|
122
|
+
try {
|
|
123
|
+
x._testParentCV(PARENT_LEFT_OFF, PARENT_RIGHT_OFF, PARENT_START_OFF, 0, 0, PARENT_OUT_OFF);
|
|
124
|
+
return mem.slice(PARENT_OUT_OFF, PARENT_OUT_OFF + BLAKE3_OUTPUT);
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
mem.fill(0, PARENT_LEFT_OFF, PARENT_LEFT_OFF + PARENT_SCRATCH_LEN);
|
|
128
|
+
x.wipeBuffers();
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
// ── Blake3Tree class ────────────────────────────────────────────────────────
|
|
133
|
+
/**
|
|
134
|
+
* Stateful BLAKE3 Merkle log. Same surface and storage discipline as
|
|
135
|
+
* `Sha256Tree`: leaf hashes and every perfect aligned internal subtree
|
|
136
|
+
* hash live in the injected `MerkleStorage`; partial right-edge subtrees
|
|
137
|
+
* are recomputed on demand.
|
|
138
|
+
*
|
|
139
|
+
* `append` is the only mutator and is the leaf-hash factory; consumers
|
|
140
|
+
* feed leaf bytes, not pre-computed leaf hashes.
|
|
141
|
+
*/
|
|
142
|
+
export class Blake3Tree {
|
|
143
|
+
hasher = Blake3Hasher;
|
|
144
|
+
storage;
|
|
145
|
+
constructor(storage) {
|
|
146
|
+
this.storage = storage;
|
|
147
|
+
}
|
|
148
|
+
size() {
|
|
149
|
+
return this.storage.size();
|
|
150
|
+
}
|
|
151
|
+
rootHash() {
|
|
152
|
+
const n = this.storage.size();
|
|
153
|
+
if (n === 0)
|
|
154
|
+
return this.hasher.hashEmpty();
|
|
155
|
+
const getNode = (level, index) => this.storage.getNode(level, index);
|
|
156
|
+
return subtreeHash(this.hasher, 0, n, getNode);
|
|
157
|
+
}
|
|
158
|
+
append(leafBytes) {
|
|
159
|
+
const leafIndex = this.storage.size();
|
|
160
|
+
const leafHash = this.hasher.hashLeaf(leafBytes);
|
|
161
|
+
this.storage.appendLeaf(leafIndex, leafHash);
|
|
162
|
+
let level = 0;
|
|
163
|
+
let idx = leafIndex;
|
|
164
|
+
while ((idx & 1) === 1) {
|
|
165
|
+
const left = this.storage.getNode(level, idx - 1);
|
|
166
|
+
const right = this.storage.getNode(level, idx);
|
|
167
|
+
const parent = this.hasher.hashInternal(left, right);
|
|
168
|
+
this.storage.putNode(level + 1, idx >>> 1, parent);
|
|
169
|
+
idx = idx >>> 1;
|
|
170
|
+
level++;
|
|
171
|
+
}
|
|
172
|
+
return { leafIndex, leafHash };
|
|
173
|
+
}
|
|
174
|
+
getInclusionProof(leafIndex, treeSize) {
|
|
175
|
+
const ts = treeSize ?? this.storage.size();
|
|
176
|
+
if (!Number.isInteger(ts) || ts < 1 || ts > this.storage.size())
|
|
177
|
+
throw new RangeError(`Blake3Tree.getInclusionProof: treeSize ${ts} out of range [1, ${this.storage.size()}]`);
|
|
178
|
+
const getNode = (level, index) => this.storage.getNode(level, index);
|
|
179
|
+
return buildInclusionProof({ hasher: this.hasher, leafIndex, treeSize: ts, getNode });
|
|
180
|
+
}
|
|
181
|
+
getConsistencyProof(oldSize, newSize) {
|
|
182
|
+
if (!Number.isInteger(newSize) || newSize < 0 || newSize > this.storage.size())
|
|
183
|
+
throw new RangeError(`Blake3Tree.getConsistencyProof: newSize ${newSize} out of range [0, ${this.storage.size()}]`);
|
|
184
|
+
const getNode = (level, index) => this.storage.getNode(level, index);
|
|
185
|
+
return buildConsistencyProof({ hasher: this.hasher, oldSize, newSize, getNode });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decoded form of a c2sp.org/tlog-checkpoint body. The body shape is
|
|
3
|
+
* hash-and-algo-agnostic: `rootHash` is 32 bytes for both the SHA-256 and
|
|
4
|
+
* BLAKE3 trees Phase 7 ships, but the codec only enforces a caller-supplied
|
|
5
|
+
* length in `parseCheckpointBody`. The signed-note envelope that wraps a
|
|
6
|
+
* checkpoint is handled in `signed-note.ts`.
|
|
7
|
+
*/
|
|
8
|
+
export interface Checkpoint {
|
|
9
|
+
/**
|
|
10
|
+
* Log identity, non-empty UTF-8 with no Unicode spaces, plus signs, or
|
|
11
|
+
* embedded newlines. Per c2sp.org/tlog-checkpoint §Note text the origin
|
|
12
|
+
* SHOULD be a schemeless URL such as `example.com/log42`, but the codec
|
|
13
|
+
* only enforces the MUST-level structural constraints; broader URL
|
|
14
|
+
* shape policy is a caller concern.
|
|
15
|
+
*/
|
|
16
|
+
readonly origin: string;
|
|
17
|
+
/**
|
|
18
|
+
* Number of leaves in the tree at signing time. Must be a non-negative
|
|
19
|
+
* safe integer; ASCII decimal serialization carries no leading zeroes
|
|
20
|
+
* (the literal `0` is the only valid string starting with `0`).
|
|
21
|
+
*/
|
|
22
|
+
readonly treeSize: number;
|
|
23
|
+
/**
|
|
24
|
+
* Merkle root hash. 32 bytes for both Sha256Tree and Blake3Tree; the
|
|
25
|
+
* caller-supplied `expectedHashLen` parameter on `parseCheckpointBody`
|
|
26
|
+
* pins the exact length for a given hasher.
|
|
27
|
+
*/
|
|
28
|
+
readonly rootHash: Uint8Array;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Serialize a Checkpoint into its canonical body bytes per
|
|
32
|
+
* c2sp.org/tlog-checkpoint §Note text. Layout:
|
|
33
|
+
*
|
|
34
|
+
* utf8(origin) || 0x0A || utf8(decimal(treeSize)) || 0x0A
|
|
35
|
+
* || base64(rootHash) || 0x0A
|
|
36
|
+
*
|
|
37
|
+
* Base64 uses the RFC 4648 §4 standard alphabet with `=` padding (NOT the
|
|
38
|
+
* URL-safe variant from §5 and NOT padding-stripped). The body has no
|
|
39
|
+
* leading or trailing whitespace beyond the final 0x0A; byte stability
|
|
40
|
+
* is the entire purpose of the codec, since the body bytes are what the
|
|
41
|
+
* STH signature is computed over.
|
|
42
|
+
*/
|
|
43
|
+
export declare function serializeCheckpointBody(c: Checkpoint): Uint8Array;
|
|
44
|
+
/**
|
|
45
|
+
* Parse a canonical checkpoint body. Inverse of `serializeCheckpointBody`;
|
|
46
|
+
* round-trips byte-for-byte. Rejects extension lines, leading or trailing
|
|
47
|
+
* whitespace beyond the mandatory final 0x0A, non-newline ASCII control
|
|
48
|
+
* characters, malformed base64, and root hashes whose decoded length does
|
|
49
|
+
* not match `expectedHashLen` (default 32, the size for both Sha256Tree
|
|
50
|
+
* and Blake3Tree).
|
|
51
|
+
*
|
|
52
|
+
* The caller pins `expectedHashLen` to its hasher's `outputSize`; a future
|
|
53
|
+
* SignedLog (TASK-4) will bind this to the tree's hasher automatically.
|
|
54
|
+
*
|
|
55
|
+
* Per c2sp.org/tlog-checkpoint §Note text and c2sp.org/signed-note
|
|
56
|
+
* §Format.
|
|
57
|
+
*/
|
|
58
|
+
export declare function parseCheckpointBody(bytes: Uint8Array, expectedHashLen?: number): Checkpoint;
|
|
@@ -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';
|