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,300 @@
|
|
|
1
|
+
// ▄▄▄▄▄▄▄▄▄▄
|
|
2
|
+
// ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
|
|
3
|
+
// ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
|
|
4
|
+
// ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
|
|
5
|
+
// ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
|
|
6
|
+
// ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
|
|
7
|
+
// ███████▌ ▀██▀ ███
|
|
8
|
+
// ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
|
|
9
|
+
// ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
|
|
10
|
+
// ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
|
|
11
|
+
// ▀████▄ ▄██▄
|
|
12
|
+
// ▐████ ▐███ Author: xero (https://x-e.ro)
|
|
13
|
+
// ▄▄██████████ ▐███ ▄▄ License: MIT
|
|
14
|
+
// ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
|
|
15
|
+
// ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
|
|
16
|
+
// ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀ free, "as is", and without
|
|
17
|
+
// ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
|
|
18
|
+
// █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
|
|
19
|
+
// ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
|
|
20
|
+
// ▀█████▀▀
|
|
21
|
+
//
|
|
22
|
+
// src/ts/merkle/proof.ts
|
|
23
|
+
//
|
|
24
|
+
// Hash-agnostic, free-function proof verifiers and builders for the
|
|
25
|
+
// RFC 9162 (Certificate Transparency Version 2.0) §2.1.3 / §2.1.4
|
|
26
|
+
// proof formats. Every entry point takes a `Hasher`; SHA-256 and
|
|
27
|
+
// BLAKE3 trees share the same wire format and the same algorithmic
|
|
28
|
+
// core.
|
|
29
|
+
//
|
|
30
|
+
// Verifiers return boolean. Malformed-proof conditions (wrong inner /
|
|
31
|
+
// border length, mismatched root) return false. Contract violations
|
|
32
|
+
// (wrong-sized root for the hasher, leafIndex out of range, oldSize >
|
|
33
|
+
// newSize) throw RangeError; the caller is responsible for staying
|
|
34
|
+
// within the public contract.
|
|
35
|
+
//
|
|
36
|
+
// Builders accept a `getNode(level, index)` callback that abstracts
|
|
37
|
+
// the storage layer. Memory, file, and database backends drive the
|
|
38
|
+
// same builder without bringing storage details into the proof
|
|
39
|
+
// algorithms.
|
|
40
|
+
import { bitLen, popcount, splitPoint, trailingZeros } from './tree.js';
|
|
41
|
+
import { constantTimeEqual } from '../utils.js';
|
|
42
|
+
// ── Internal chaining primitives (RFC 9162 §2.1.3 / §2.1.4) ─────────────────
|
|
43
|
+
/**
|
|
44
|
+
* Decompose an inclusion proof into its inner (path-up-the-tree) and
|
|
45
|
+
* border (left siblings completing the right edge) segments. The sum
|
|
46
|
+
* inner + border is the required proof length.
|
|
47
|
+
*
|
|
48
|
+
* RFC 9162 §2.1.3, Inclusion Proof Verification: the path from a leaf
|
|
49
|
+
* at index to the root of a size-n tree has bitLen(index XOR (size-1))
|
|
50
|
+
* inner levels and popcount(index >> inner) border levels.
|
|
51
|
+
*/
|
|
52
|
+
function decompInclProof(index, size) {
|
|
53
|
+
const inner = bitLen(index ^ (size - 1));
|
|
54
|
+
const border = popcount(Math.floor(index / 2 ** inner));
|
|
55
|
+
return { inner, border };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Chain `inner` proof entries up from `seed`. At level i, the bit
|
|
59
|
+
* (index >> i) & 1 selects whether the sibling is on the left or the
|
|
60
|
+
* right of `seed`. RFC 9162 §2.1.3.
|
|
61
|
+
*/
|
|
62
|
+
function chainInner(hasher, seed, proof, index) {
|
|
63
|
+
let acc = seed;
|
|
64
|
+
for (let i = 0; i < proof.length; i++) {
|
|
65
|
+
const bit = Math.floor(index / 2 ** i) & 1;
|
|
66
|
+
acc = bit === 0
|
|
67
|
+
? hasher.hashInternal(acc, proof[i])
|
|
68
|
+
: hasher.hashInternal(proof[i], acc);
|
|
69
|
+
}
|
|
70
|
+
return acc;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Chain `inner` entries but only fold in left siblings (skip the right
|
|
74
|
+
* ones). Used by the consistency verifier to reconstruct the OLD root
|
|
75
|
+
* from the suffix shared with the inclusion proof. RFC 9162 §2.1.4.
|
|
76
|
+
*/
|
|
77
|
+
function chainInnerRight(hasher, seed, proof, index) {
|
|
78
|
+
let acc = seed;
|
|
79
|
+
for (let i = 0; i < proof.length; i++) {
|
|
80
|
+
const bit = Math.floor(index / 2 ** i) & 1;
|
|
81
|
+
if (bit === 1)
|
|
82
|
+
acc = hasher.hashInternal(proof[i], acc);
|
|
83
|
+
}
|
|
84
|
+
return acc;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Chain border entries: every remaining sibling is a left sibling
|
|
88
|
+
* along the size-1 path back to the root. RFC 9162 §2.1.3.
|
|
89
|
+
*/
|
|
90
|
+
function chainBorderRight(hasher, seed, proof) {
|
|
91
|
+
let acc = seed;
|
|
92
|
+
for (const h of proof)
|
|
93
|
+
acc = hasher.hashInternal(h, acc);
|
|
94
|
+
return acc;
|
|
95
|
+
}
|
|
96
|
+
function assertHashLen(hasher, label, h) {
|
|
97
|
+
if (h.length !== hasher.outputSize)
|
|
98
|
+
throw new RangeError(`${label}: wrong length ${h.length}, expected ${hasher.outputSize} for ${hasher.name}`);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* RFC 9162 §2.1.3, Inclusion Proof Verification. Returns true if the
|
|
102
|
+
* proof reconstructs `rootHash` from `leafHash` at position
|
|
103
|
+
* (leafIndex, treeSize). Wrong proof length, wrong leaf-hash size, or
|
|
104
|
+
* a reconstructed root that differs from `rootHash` all return false.
|
|
105
|
+
* Contract violations (negative or out-of-range index, treeSize <= 0,
|
|
106
|
+
* wrong-sized rootHash) throw RangeError.
|
|
107
|
+
*
|
|
108
|
+
* `leafHash` is the leaf's MTH ({d_m} hashed under the leaf prefix), not
|
|
109
|
+
* the raw leaf bytes. Thin verifiers receiving a leaf over the wire
|
|
110
|
+
* should compute `hasher.hashLeaf(bytes)` before calling.
|
|
111
|
+
*/
|
|
112
|
+
export function verifyInclusionProof(input) {
|
|
113
|
+
const { hasher, leafHash, leafIndex, treeSize, proof, rootHash } = input;
|
|
114
|
+
if (!Number.isInteger(leafIndex) || leafIndex < 0)
|
|
115
|
+
throw new RangeError(`verifyInclusionProof: leafIndex must be a non-negative integer, got ${leafIndex}`);
|
|
116
|
+
if (!Number.isInteger(treeSize) || treeSize < 1)
|
|
117
|
+
throw new RangeError(`verifyInclusionProof: treeSize must be a positive integer, got ${treeSize}`);
|
|
118
|
+
if (leafIndex >= treeSize)
|
|
119
|
+
throw new RangeError(`verifyInclusionProof: leafIndex ${leafIndex} >= treeSize ${treeSize}`);
|
|
120
|
+
assertHashLen(hasher, 'verifyInclusionProof: rootHash', rootHash);
|
|
121
|
+
if (leafHash.length !== hasher.outputSize)
|
|
122
|
+
return false;
|
|
123
|
+
const { inner, border } = decompInclProof(leafIndex, treeSize);
|
|
124
|
+
if (proof.length !== inner + border)
|
|
125
|
+
return false;
|
|
126
|
+
for (const h of proof) {
|
|
127
|
+
if (h.length !== hasher.outputSize)
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
const innerProof = proof.slice(0, inner);
|
|
131
|
+
const borderProof = proof.slice(inner);
|
|
132
|
+
let res = chainInner(hasher, leafHash, innerProof, leafIndex);
|
|
133
|
+
res = chainBorderRight(hasher, res, borderProof);
|
|
134
|
+
return constantTimeEqual(res, rootHash);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* RFC 9162 §2.1.4, Consistency Proof Verification. Returns true if
|
|
138
|
+
* `proof` proves that the size-`oldSize` tree with root `oldRoot` is a
|
|
139
|
+
* prefix of the size-`newSize` tree with root `newRoot`.
|
|
140
|
+
*
|
|
141
|
+
* Malformed-proof conditions (wrong proof length, non-empty proof when
|
|
142
|
+
* one is forbidden, mismatched old/new root reconstruction) return
|
|
143
|
+
* false. Contract violations (`oldSize > newSize`, wrong-sized root)
|
|
144
|
+
* throw RangeError; the special "consistency from empty tree" form is
|
|
145
|
+
* not part of the wire format and returns false.
|
|
146
|
+
*/
|
|
147
|
+
export function verifyConsistencyProof(input) {
|
|
148
|
+
const { hasher, oldSize, newSize, oldRoot, newRoot, proof } = input;
|
|
149
|
+
if (!Number.isInteger(oldSize) || oldSize < 0)
|
|
150
|
+
throw new RangeError(`verifyConsistencyProof: oldSize must be a non-negative integer, got ${oldSize}`);
|
|
151
|
+
if (!Number.isInteger(newSize) || newSize < 0)
|
|
152
|
+
throw new RangeError(`verifyConsistencyProof: newSize must be a non-negative integer, got ${newSize}`);
|
|
153
|
+
if (oldSize > newSize)
|
|
154
|
+
throw new RangeError(`verifyConsistencyProof: oldSize ${oldSize} > newSize ${newSize}`);
|
|
155
|
+
// Equal-size shortcut: RFC says the proof is empty and roots match.
|
|
156
|
+
// Byte-for-byte comparison; root hashes flow through unchanged because
|
|
157
|
+
// no reconstruction runs, so hash-length validation does not apply.
|
|
158
|
+
if (oldSize === newSize) {
|
|
159
|
+
if (proof.length > 0)
|
|
160
|
+
return false;
|
|
161
|
+
return oldRoot.length === newRoot.length && constantTimeEqual(oldRoot, newRoot);
|
|
162
|
+
}
|
|
163
|
+
// "Consistency from empty tree" is undefined: the verifier cannot
|
|
164
|
+
// recover oldRoot from no proof, so reject as malformed.
|
|
165
|
+
if (oldSize === 0)
|
|
166
|
+
return false;
|
|
167
|
+
if (proof.length === 0)
|
|
168
|
+
return false;
|
|
169
|
+
assertHashLen(hasher, 'verifyConsistencyProof: oldRoot', oldRoot);
|
|
170
|
+
assertHashLen(hasher, 'verifyConsistencyProof: newRoot', newRoot);
|
|
171
|
+
for (const h of proof) {
|
|
172
|
+
if (h.length !== hasher.outputSize)
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
const { inner: innerFull, border } = decompInclProof(oldSize - 1, newSize);
|
|
176
|
+
const shift = trailingZeros(oldSize);
|
|
177
|
+
const inner = innerFull - shift;
|
|
178
|
+
// If oldSize is a power of two, the verifier already knows the
|
|
179
|
+
// subtree's root (== oldRoot) and the proof omits it. Otherwise the
|
|
180
|
+
// proof's first element is the seed for both chains.
|
|
181
|
+
const oldIsPow2 = oldSize === 2 ** shift;
|
|
182
|
+
let seed;
|
|
183
|
+
let start;
|
|
184
|
+
if (oldIsPow2) {
|
|
185
|
+
seed = oldRoot;
|
|
186
|
+
start = 0;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
seed = proof[0];
|
|
190
|
+
start = 1;
|
|
191
|
+
}
|
|
192
|
+
const expectedLen = start + inner + border;
|
|
193
|
+
if (proof.length !== expectedLen)
|
|
194
|
+
return false;
|
|
195
|
+
const tail = proof.slice(start);
|
|
196
|
+
const innerProof = tail.slice(0, inner);
|
|
197
|
+
const borderProof = tail.slice(inner);
|
|
198
|
+
// Bit pattern for chainInnerRight: we re-derive the oldRoot from
|
|
199
|
+
// the proof. `mask` is (oldSize - 1) >> shift, the path bits above
|
|
200
|
+
// the size-`oldSize` subtree's root level.
|
|
201
|
+
const mask = Math.floor((oldSize - 1) / 2 ** shift);
|
|
202
|
+
let hash1 = chainInnerRight(hasher, seed, innerProof, mask);
|
|
203
|
+
hash1 = chainBorderRight(hasher, hash1, borderProof);
|
|
204
|
+
if (!constantTimeEqual(hash1, oldRoot))
|
|
205
|
+
return false;
|
|
206
|
+
let hash2 = chainInner(hasher, seed, innerProof, mask);
|
|
207
|
+
hash2 = chainBorderRight(hasher, hash2, borderProof);
|
|
208
|
+
return constantTimeEqual(hash2, newRoot);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* RFC 9162 §2.1.3: build the inclusion proof for leaf `leafIndex` in
|
|
212
|
+
* a tree of size `treeSize`. The returned bytes are ordered from the
|
|
213
|
+
* lowest level upward (leaf sibling first, root-adjacent last), the
|
|
214
|
+
* order `verifyInclusionProof` consumes.
|
|
215
|
+
*/
|
|
216
|
+
export function buildInclusionProof(input) {
|
|
217
|
+
const { hasher, leafIndex, treeSize, getNode } = input;
|
|
218
|
+
if (!Number.isInteger(leafIndex) || leafIndex < 0)
|
|
219
|
+
throw new RangeError(`buildInclusionProof: leafIndex must be a non-negative integer, got ${leafIndex}`);
|
|
220
|
+
if (!Number.isInteger(treeSize) || treeSize < 1)
|
|
221
|
+
throw new RangeError(`buildInclusionProof: treeSize must be a positive integer, got ${treeSize}`);
|
|
222
|
+
if (leafIndex >= treeSize)
|
|
223
|
+
throw new RangeError(`buildInclusionProof: leafIndex ${leafIndex} >= treeSize ${treeSize}`);
|
|
224
|
+
return pathBuild(hasher, leafIndex, 0, treeSize, getNode);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* RFC 9162 §2.1.4: build the consistency proof between two tree
|
|
228
|
+
* sizes. Returns an empty array when oldSize equals newSize or
|
|
229
|
+
* oldSize is zero (the verifier rejects the latter, but the builder
|
|
230
|
+
* is symmetric for inspection-time use).
|
|
231
|
+
*/
|
|
232
|
+
export function buildConsistencyProof(input) {
|
|
233
|
+
const { hasher, oldSize, newSize, getNode } = input;
|
|
234
|
+
if (!Number.isInteger(oldSize) || oldSize < 0)
|
|
235
|
+
throw new RangeError(`buildConsistencyProof: oldSize must be a non-negative integer, got ${oldSize}`);
|
|
236
|
+
if (!Number.isInteger(newSize) || newSize < 0)
|
|
237
|
+
throw new RangeError(`buildConsistencyProof: newSize must be a non-negative integer, got ${newSize}`);
|
|
238
|
+
if (oldSize > newSize)
|
|
239
|
+
throw new RangeError(`buildConsistencyProof: oldSize ${oldSize} > newSize ${newSize}`);
|
|
240
|
+
if (oldSize === newSize || oldSize === 0)
|
|
241
|
+
return [];
|
|
242
|
+
return subProof(hasher, oldSize, 0, newSize, true, getNode);
|
|
243
|
+
}
|
|
244
|
+
// RFC 9162 §2.1.4 SUBPROOF(m, D[n], b). `lo` and `hi` parameterise
|
|
245
|
+
// the [lo, hi) range covered by the current subtree; `m` is the size
|
|
246
|
+
// of the older subtree being witnessed.
|
|
247
|
+
function subProof(hasher, m, lo, hi, b, getNode) {
|
|
248
|
+
const n = hi - lo;
|
|
249
|
+
if (m === n) {
|
|
250
|
+
// Whole subtree: emit its root only if b == false.
|
|
251
|
+
return b ? [] : [subtreeHash(hasher, lo, hi, getNode)];
|
|
252
|
+
}
|
|
253
|
+
const k = splitPoint(n);
|
|
254
|
+
if (m <= k) {
|
|
255
|
+
const sub = subProof(hasher, m, lo, lo + k, b, getNode);
|
|
256
|
+
sub.push(subtreeHash(hasher, lo + k, hi, getNode));
|
|
257
|
+
return sub;
|
|
258
|
+
}
|
|
259
|
+
const sub = subProof(hasher, m - k, lo + k, hi, false, getNode);
|
|
260
|
+
sub.push(subtreeHash(hasher, lo, lo + k, getNode));
|
|
261
|
+
return sub;
|
|
262
|
+
}
|
|
263
|
+
// Inclusion-proof path build: yields siblings ordered from the lowest
|
|
264
|
+
// level (leaf sibling) up. Sibling = root of the other half of the
|
|
265
|
+
// current subtree.
|
|
266
|
+
function pathBuild(hasher, leafIndex, lo, hi, getNode) {
|
|
267
|
+
if (hi - lo <= 1)
|
|
268
|
+
return [];
|
|
269
|
+
const k = splitPoint(hi - lo);
|
|
270
|
+
if (leafIndex - lo < k) {
|
|
271
|
+
const sub = pathBuild(hasher, leafIndex, lo, lo + k, getNode);
|
|
272
|
+
sub.push(subtreeHash(hasher, lo + k, hi, getNode));
|
|
273
|
+
return sub;
|
|
274
|
+
}
|
|
275
|
+
const sub = pathBuild(hasher, leafIndex, lo + k, hi, getNode);
|
|
276
|
+
sub.push(subtreeHash(hasher, lo, lo + k, getNode));
|
|
277
|
+
return sub;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* RFC 9162 §2.1.1 MTH(D[lo:hi]). For a perfect aligned subtree the
|
|
281
|
+
* value is stored at (level, index); otherwise the value is the
|
|
282
|
+
* internal hash of the perfect left half and the recursive right
|
|
283
|
+
* half. Visible to the tree class so `rootHash()` can share the
|
|
284
|
+
* recursion with the builders.
|
|
285
|
+
*
|
|
286
|
+
* @internal
|
|
287
|
+
*/
|
|
288
|
+
export function subtreeHash(hasher, lo, hi, getNode) {
|
|
289
|
+
const n = hi - lo;
|
|
290
|
+
if (n === 1)
|
|
291
|
+
return getNode(0, lo);
|
|
292
|
+
const k = splitPoint(n);
|
|
293
|
+
if (k === n / 2 && (lo % n) === 0) {
|
|
294
|
+
// Perfect aligned subtree: a stored internal node.
|
|
295
|
+
return getNode(bitLen(n) - 1, Math.floor(lo / n));
|
|
296
|
+
}
|
|
297
|
+
const left = subtreeHash(hasher, lo, lo + k, getNode);
|
|
298
|
+
const right = subtreeHash(hasher, lo + k, hi, getNode);
|
|
299
|
+
return hasher.hashInternal(left, right);
|
|
300
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Hasher, MerkleTree } from './tree.js';
|
|
2
|
+
import type { MerkleStorage } from './storage.js';
|
|
3
|
+
/**
|
|
4
|
+
* RFC 9162 §2.1.1, Merkle Hash Trees. The CT-flavoured SHA-256 hash
|
|
5
|
+
* function: empty-tree value `MTH({}) = SHA-256()`, leaf prefix `0x00`,
|
|
6
|
+
* internal-node prefix `0x01`.
|
|
7
|
+
*
|
|
8
|
+
* Stateless and reentrant: each method takes the sha2 module fresh,
|
|
9
|
+
* runs SHA-256 once, and releases. No `dispose()` is needed.
|
|
10
|
+
*/
|
|
11
|
+
export declare const Sha256Hasher: Hasher;
|
|
12
|
+
/**
|
|
13
|
+
* Stateful SHA-256 Merkle log. Stores leaf hashes and every perfect
|
|
14
|
+
* aligned internal subtree's hash via the injected `MerkleStorage`;
|
|
15
|
+
* partial right-edge subtrees are recomputed on demand from the
|
|
16
|
+
* stored perfect subtrees.
|
|
17
|
+
*
|
|
18
|
+
* Constructed empty. `append` is the only mutator and is the leaf-hash
|
|
19
|
+
* factory; consumers feed leaf bytes, not pre-computed leaf hashes.
|
|
20
|
+
*/
|
|
21
|
+
export declare class Sha256Tree implements MerkleTree {
|
|
22
|
+
readonly hasher: Hasher;
|
|
23
|
+
private readonly storage;
|
|
24
|
+
constructor(storage: MerkleStorage);
|
|
25
|
+
size(): number;
|
|
26
|
+
rootHash(): Uint8Array;
|
|
27
|
+
append(leafBytes: Uint8Array): {
|
|
28
|
+
leafIndex: number;
|
|
29
|
+
leafHash: Uint8Array;
|
|
30
|
+
};
|
|
31
|
+
getInclusionProof(leafIndex: number, treeSize?: number): Uint8Array[];
|
|
32
|
+
getConsistencyProof(oldSize: number, newSize: number): Uint8Array[];
|
|
33
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
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/sha256-tree.ts
|
|
23
|
+
//
|
|
24
|
+
// SHA-256 specialisation of the Hasher / MerkleTree interfaces. Wraps
|
|
25
|
+
// the existing `SHA256` class from the sha2 module under the RFC 9162
|
|
26
|
+
// (Certificate Transparency Version 2.0) §2.1.1 leaf and internal-node
|
|
27
|
+
// domain separators.
|
|
28
|
+
//
|
|
29
|
+
// Per-call WASM lifecycle: every Sha256Hasher method instantiates a
|
|
30
|
+
// fresh SHA256 inside a try / finally + dispose pattern. There is no
|
|
31
|
+
// long-lived module ownership; concurrent users are serialised by the
|
|
32
|
+
// per-module exclusivity guard in the sha2 substrate. This mirrors
|
|
33
|
+
// the SignatureSuite factories under src/ts/sign/suites/.
|
|
34
|
+
import { SHA256 } from '../sha2/index.js';
|
|
35
|
+
import { buildConsistencyProof, buildInclusionProof, subtreeHash, } from './proof.js';
|
|
36
|
+
// ── Sha256Hasher const ──────────────────────────────────────────────────────
|
|
37
|
+
const SHA256_OUTPUT = 32;
|
|
38
|
+
const LEAF_PREFIX = new Uint8Array([0x00]);
|
|
39
|
+
const INTERNAL_PREFIX = new Uint8Array([0x01]);
|
|
40
|
+
const SHA256_WASM_MODULES = Object.freeze(['sha2']);
|
|
41
|
+
function sha256Hash(input) {
|
|
42
|
+
const h = new SHA256();
|
|
43
|
+
try {
|
|
44
|
+
return h.hash(input);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
h.dispose();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* RFC 9162 §2.1.1, Merkle Hash Trees. The CT-flavoured SHA-256 hash
|
|
52
|
+
* function: empty-tree value `MTH({}) = SHA-256()`, leaf prefix `0x00`,
|
|
53
|
+
* internal-node prefix `0x01`.
|
|
54
|
+
*
|
|
55
|
+
* Stateless and reentrant: each method takes the sha2 module fresh,
|
|
56
|
+
* runs SHA-256 once, and releases. No `dispose()` is needed.
|
|
57
|
+
*/
|
|
58
|
+
export const Sha256Hasher = Object.freeze({
|
|
59
|
+
name: 'sha256',
|
|
60
|
+
outputSize: SHA256_OUTPUT,
|
|
61
|
+
wasmModules: SHA256_WASM_MODULES,
|
|
62
|
+
hashEmpty() {
|
|
63
|
+
// RFC 9162 §2.1.1: MTH({}) is the hash of an empty bit-string.
|
|
64
|
+
return sha256Hash(new Uint8Array(0));
|
|
65
|
+
},
|
|
66
|
+
hashLeaf(leaf) {
|
|
67
|
+
// RFC 9162 §2.1.1: MTH({d}) = HASH(0x00 || d). The 0x00 prefix
|
|
68
|
+
// is the domain separator that prevents an internal-node hash
|
|
69
|
+
// from being mistaken for a leaf hash.
|
|
70
|
+
const buf = new Uint8Array(1 + leaf.length);
|
|
71
|
+
buf.set(LEAF_PREFIX, 0);
|
|
72
|
+
buf.set(leaf, 1);
|
|
73
|
+
return sha256Hash(buf);
|
|
74
|
+
},
|
|
75
|
+
hashInternal(left, right) {
|
|
76
|
+
// RFC 9162 §2.1.1: MTH(D[n]) = HASH(0x01 || MTH(D[0:k]) ||
|
|
77
|
+
// MTH(D[k:n])). The 0x01 prefix is the other half of the
|
|
78
|
+
// second-preimage-resistance domain separator.
|
|
79
|
+
const buf = new Uint8Array(1 + left.length + right.length);
|
|
80
|
+
buf.set(INTERNAL_PREFIX, 0);
|
|
81
|
+
buf.set(left, 1);
|
|
82
|
+
buf.set(right, 1 + left.length);
|
|
83
|
+
return sha256Hash(buf);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
// ── Sha256Tree class ────────────────────────────────────────────────────────
|
|
87
|
+
/**
|
|
88
|
+
* Stateful SHA-256 Merkle log. Stores leaf hashes and every perfect
|
|
89
|
+
* aligned internal subtree's hash via the injected `MerkleStorage`;
|
|
90
|
+
* partial right-edge subtrees are recomputed on demand from the
|
|
91
|
+
* stored perfect subtrees.
|
|
92
|
+
*
|
|
93
|
+
* Constructed empty. `append` is the only mutator and is the leaf-hash
|
|
94
|
+
* factory; consumers feed leaf bytes, not pre-computed leaf hashes.
|
|
95
|
+
*/
|
|
96
|
+
export class Sha256Tree {
|
|
97
|
+
hasher = Sha256Hasher;
|
|
98
|
+
storage;
|
|
99
|
+
constructor(storage) {
|
|
100
|
+
this.storage = storage;
|
|
101
|
+
}
|
|
102
|
+
size() {
|
|
103
|
+
return this.storage.size();
|
|
104
|
+
}
|
|
105
|
+
rootHash() {
|
|
106
|
+
const n = this.storage.size();
|
|
107
|
+
if (n === 0)
|
|
108
|
+
return this.hasher.hashEmpty();
|
|
109
|
+
const getNode = (level, index) => this.storage.getNode(level, index);
|
|
110
|
+
return subtreeHash(this.hasher, 0, n, getNode);
|
|
111
|
+
}
|
|
112
|
+
append(leafBytes) {
|
|
113
|
+
const leafIndex = this.storage.size();
|
|
114
|
+
const leafHash = this.hasher.hashLeaf(leafBytes);
|
|
115
|
+
this.storage.appendLeaf(leafIndex, leafHash);
|
|
116
|
+
// Propagate completed internal nodes up the right edge. RFC 9162
|
|
117
|
+
// §2.1.1 makes the tree fill left-to-right; whenever a node lands
|
|
118
|
+
// at an odd index its left sibling already exists, so the parent
|
|
119
|
+
// becomes computable for free.
|
|
120
|
+
let level = 0;
|
|
121
|
+
let idx = leafIndex;
|
|
122
|
+
while ((idx & 1) === 1) {
|
|
123
|
+
const left = this.storage.getNode(level, idx - 1);
|
|
124
|
+
const right = this.storage.getNode(level, idx);
|
|
125
|
+
const parent = this.hasher.hashInternal(left, right);
|
|
126
|
+
this.storage.putNode(level + 1, idx >>> 1, parent);
|
|
127
|
+
idx = idx >>> 1;
|
|
128
|
+
level++;
|
|
129
|
+
}
|
|
130
|
+
return { leafIndex, leafHash };
|
|
131
|
+
}
|
|
132
|
+
getInclusionProof(leafIndex, treeSize) {
|
|
133
|
+
const ts = treeSize ?? this.storage.size();
|
|
134
|
+
if (!Number.isInteger(ts) || ts < 1 || ts > this.storage.size())
|
|
135
|
+
throw new RangeError(`Sha256Tree.getInclusionProof: treeSize ${ts} out of range [1, ${this.storage.size()}]`);
|
|
136
|
+
const getNode = (level, index) => this.storage.getNode(level, index);
|
|
137
|
+
return buildInclusionProof({ hasher: this.hasher, leafIndex, treeSize: ts, getNode });
|
|
138
|
+
}
|
|
139
|
+
getConsistencyProof(oldSize, newSize) {
|
|
140
|
+
if (!Number.isInteger(newSize) || newSize < 0 || newSize > this.storage.size())
|
|
141
|
+
throw new RangeError(`Sha256Tree.getConsistencyProof: newSize ${newSize} out of range [0, ${this.storage.size()}]`);
|
|
142
|
+
const getNode = (level, index) => this.storage.getNode(level, index);
|
|
143
|
+
return buildConsistencyProof({ hasher: this.hasher, oldSize, newSize, getNode });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { SignedTreeHead } from './sth.js';
|
|
2
|
+
import type { MerkleTree } from './tree.js';
|
|
3
|
+
import type { SignatureSuite } from '../sign/types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Constructor options for `SignedLog`.
|
|
6
|
+
*/
|
|
7
|
+
export interface SignedLogOpts<S extends SignatureSuite> {
|
|
8
|
+
/** Underlying Merkle tree, holds the stateful append + proof surface. */
|
|
9
|
+
tree: MerkleTree;
|
|
10
|
+
/**
|
|
11
|
+
* Signature suite. Must have an entry in the C2SP cosignature
|
|
12
|
+
* algorithm-byte registry (currently `Ed25519Suite` and
|
|
13
|
+
* `MlDsa44Suite`); other suites throw `SigningError`.
|
|
14
|
+
*/
|
|
15
|
+
suite: S;
|
|
16
|
+
/**
|
|
17
|
+
* Log identity, the first line of every checkpoint body. Validated
|
|
18
|
+
* at construction (non-empty, no whitespace, no plus characters)
|
|
19
|
+
* per c2sp.org/tlog-checkpoint §Note text.
|
|
20
|
+
*/
|
|
21
|
+
origin: string;
|
|
22
|
+
/**
|
|
23
|
+
* Signing key, exactly `suite.skSize` bytes. The SignedLog stores
|
|
24
|
+
* a private copy; `dispose()` zeroes that copy. The caller's view
|
|
25
|
+
* of the buffer is left untouched.
|
|
26
|
+
*/
|
|
27
|
+
signingKey: Uint8Array;
|
|
28
|
+
/**
|
|
29
|
+
* Public key, exactly `suite.pkSize` bytes. Used to derive the
|
|
30
|
+
* 4-byte keyId stamped on every emitted signature line and to
|
|
31
|
+
* match incoming signature lines during verify.
|
|
32
|
+
*/
|
|
33
|
+
pubkey: Uint8Array;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Signed transparency log substrate. Combines a `MerkleTree` with a
|
|
37
|
+
* registered cosignature `SignatureSuite` and an origin string;
|
|
38
|
+
* exposes append, proof, and cosignature sign / verify operations.
|
|
39
|
+
*
|
|
40
|
+
* Per-call WASM lifecycle is enforced by the suite itself (see the
|
|
41
|
+
* SignatureSuite factories under `src/ts/sign/suites/`). `SignedLog`
|
|
42
|
+
* does not wrap additional try/finally around `suite.sign` /
|
|
43
|
+
* `suite.verify` because the suite already does. Internally the
|
|
44
|
+
* SignedLog owns a private copy of the signing key wiped by
|
|
45
|
+
* `dispose()`.
|
|
46
|
+
*/
|
|
47
|
+
export declare class SignedLog<S extends SignatureSuite> {
|
|
48
|
+
readonly tree: MerkleTree;
|
|
49
|
+
readonly suite: S;
|
|
50
|
+
readonly origin: string;
|
|
51
|
+
readonly pubkey: Uint8Array;
|
|
52
|
+
readonly wasmModules: readonly string[];
|
|
53
|
+
private readonly _algoEntry;
|
|
54
|
+
private readonly _keyId;
|
|
55
|
+
private _signingKey;
|
|
56
|
+
private _disposed;
|
|
57
|
+
constructor(opts: SignedLogOpts<S>);
|
|
58
|
+
/**
|
|
59
|
+
* Append a leaf to the underlying tree and return the new leaf's
|
|
60
|
+
* index, hash, and inclusion proof against the post-append tree size.
|
|
61
|
+
*/
|
|
62
|
+
append(leafBytes: Uint8Array): {
|
|
63
|
+
leafIndex: number;
|
|
64
|
+
leafHash: Uint8Array;
|
|
65
|
+
inclusionProof: Uint8Array[];
|
|
66
|
+
};
|
|
67
|
+
size(): number;
|
|
68
|
+
rootHash(): Uint8Array;
|
|
69
|
+
getInclusionProof(leafIndex: number, treeSize?: number): Uint8Array[];
|
|
70
|
+
getConsistencyProof(oldSize: number, newSize: number): Uint8Array[];
|
|
71
|
+
/**
|
|
72
|
+
* Issue a cosignature over the current checkpoint and emit the
|
|
73
|
+
* signed-note envelope per c2sp.org/signed-note §Format. The
|
|
74
|
+
* signature line carries the `timestamped_signature` payload
|
|
75
|
+
* from c2sp.org/tlog-cosignature §Format; the bytes the suite
|
|
76
|
+
* signs are dispatched on the algorithm's
|
|
77
|
+
* `messageConstruction`:
|
|
78
|
+
*
|
|
79
|
+
* - `'cosig'` → `buildCosigSignedMessage(body, ts)`
|
|
80
|
+
* (Ed25519, §"Ed25519 signed message")
|
|
81
|
+
* - `'cosigned-message'` → `buildCosignedMessage(...)`
|
|
82
|
+
* (ML-DSA-44, §"ML-DSA-44 signed message")
|
|
83
|
+
*
|
|
84
|
+
* `timestamp` defaults to current wall-clock POSIX seconds. The
|
|
85
|
+
* c2sp.org/tlog-witness `add-checkpoint` rule mandates a non-zero
|
|
86
|
+
* timestamp on production cosignatures; `0` is accepted by this
|
|
87
|
+
* function for test reproducibility but witness verifiers will
|
|
88
|
+
* reject envelopes that carry it. Tests and vector generators
|
|
89
|
+
* pass an explicit value to lock byte stability.
|
|
90
|
+
*/
|
|
91
|
+
signCheckpoint(opts?: {
|
|
92
|
+
timestamp?: number;
|
|
93
|
+
}): Uint8Array;
|
|
94
|
+
/**
|
|
95
|
+
* Parse a signed-note envelope into the structured `SignedTreeHead`
|
|
96
|
+
* form per c2sp.org/signed-note §Format. Surfaces the body's
|
|
97
|
+
* decoded `Checkpoint`, the signature lines that survived the
|
|
98
|
+
* permissive signed-note parse, and the primary log cosignature's
|
|
99
|
+
* POSIX-seconds timestamp (extracted via
|
|
100
|
+
* `parseCosigSignaturePayload` on the line whose keyId matches
|
|
101
|
+
* this log's pubkey-derived keyId).
|
|
102
|
+
*
|
|
103
|
+
* If no signature line matches, `timestamp` is reported as 0. The
|
|
104
|
+
* field is informational at parse time; cryptographic verification
|
|
105
|
+
* lives in `verifyCheckpoint`. Throws `RangeError` on whole-envelope
|
|
106
|
+
* structural failure (the parseSignedNote / parseCheckpointBody
|
|
107
|
+
* contract); does not throw on signature line content issues.
|
|
108
|
+
*/
|
|
109
|
+
parseCheckpoint(bytes: Uint8Array): SignedTreeHead;
|
|
110
|
+
/**
|
|
111
|
+
* Verify a signed-note envelope against this SignedLog's origin,
|
|
112
|
+
* pubkey, suite, and tree hasher. Returns `true` iff the envelope
|
|
113
|
+
* parses, carries a signature line whose keyId matches this log's
|
|
114
|
+
* pubkey-derived keyId, the `timestamped_signature` payload on
|
|
115
|
+
* that line decodes cleanly, and the signature verifies under
|
|
116
|
+
* `suite.verify` over the cosignature signed message reconstructed
|
|
117
|
+
* with the parsed timestamp.
|
|
118
|
+
*
|
|
119
|
+
* Returns `false` on every soft-fail mode: wrong origin, wrong
|
|
120
|
+
* root-hash length, no matching keyId line, malformed payload,
|
|
121
|
+
* signature failure. Throws only on this log's own disposed
|
|
122
|
+
* state; never on envelope content (envelope content is public,
|
|
123
|
+
* so timing distinctions on its content are not security-sensitive).
|
|
124
|
+
*
|
|
125
|
+
* The keyId comparison uses `constantTimeEqual` for hygiene around
|
|
126
|
+
* key-material-adjacent state; the origin and root-hash-length
|
|
127
|
+
* early returns are intentional non-constant-time exits since
|
|
128
|
+
* both fields are public per the spec.
|
|
129
|
+
*/
|
|
130
|
+
verifyCheckpoint(bytes: Uint8Array): boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Zero the stored signing-key copy. Idempotent. Subsequent calls
|
|
133
|
+
* to any public method throw.
|
|
134
|
+
*/
|
|
135
|
+
dispose(): void;
|
|
136
|
+
private _assertNotDisposed;
|
|
137
|
+
/**
|
|
138
|
+
* Dispatch the cosignature signed-message construction on the
|
|
139
|
+
* algorithm-byte registry entry's `messageConstruction`. The
|
|
140
|
+
* `body` argument is the canonical checkpoint body from
|
|
141
|
+
* `serializeCheckpointBody`, ending in 0x0A.
|
|
142
|
+
*
|
|
143
|
+
* 'cosig' c2sp.org/tlog-cosignature §"Ed25519 signed
|
|
144
|
+
* message". The full envelope body is
|
|
145
|
+
* embedded verbatim after the
|
|
146
|
+
* cosignature/v1 + time prefix.
|
|
147
|
+
*
|
|
148
|
+
* 'cosigned-message' c2sp.org/tlog-cosignature §"ML-DSA-44
|
|
149
|
+
* signed message". The body is decomposed
|
|
150
|
+
* into origin, tree size, and root hash;
|
|
151
|
+
* cosigner_name == origin (Phase 7 logs sign
|
|
152
|
+
* their own checkpoints); start == 0; end ==
|
|
153
|
+
* tree size; hash == root hash.
|
|
154
|
+
*/
|
|
155
|
+
private _buildSignedMessage;
|
|
156
|
+
}
|