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
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// ▄▄▄▄▄▄▄▄▄▄
|
|
2
|
+
// ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
|
|
3
|
+
// ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
|
|
4
|
+
// ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
|
|
5
|
+
// ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
|
|
6
|
+
// ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
|
|
7
|
+
// ███████▌ ▀██▀ ███
|
|
8
|
+
// ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
|
|
9
|
+
// ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
|
|
10
|
+
// ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
|
|
11
|
+
// ▀████▄ ▄██▄
|
|
12
|
+
// ▐████ ▐███ Author: xero (https://x-e.ro)
|
|
13
|
+
// ▄▄██████████ ▐███ ▄▄ License: MIT
|
|
14
|
+
// ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
|
|
15
|
+
// ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
|
|
16
|
+
// ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀ free, "as is", and without
|
|
17
|
+
// ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
|
|
18
|
+
// █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
|
|
19
|
+
// ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
|
|
20
|
+
// ▀█████▀▀
|
|
21
|
+
//
|
|
22
|
+
// src/ts/merkle/merkle-verifier.ts
|
|
23
|
+
//
|
|
24
|
+
// `MerkleVerifier`, verify-only normie surface. Wire format per
|
|
25
|
+
// c2sp.org/signed-note §Format, c2sp.org/tlog-checkpoint §Note text,
|
|
26
|
+
// and c2sp.org/tlog-cosignature §Format.
|
|
27
|
+
import { isInitialized } from '../init.js';
|
|
28
|
+
import { MerkleLogError, MerkleCodecError } from '../errors.js';
|
|
29
|
+
import { constantTimeEqual } from '../utils.js';
|
|
30
|
+
import { parseSignedNote, lookupAlgoEntryByFormatEnum, deriveKeyId, buildCosigSignedMessage, buildCosignedMessage, parseCosigSignaturePayload, } from './signed-note.js';
|
|
31
|
+
import { parseCheckpointBody } from './checkpoint.js';
|
|
32
|
+
import { verifyInclusionProof, verifyConsistencyProof } from './proof.js';
|
|
33
|
+
import { Sha256Hasher } from './sha256-tree.js';
|
|
34
|
+
import { Blake3Hasher } from './blake3-tree.js';
|
|
35
|
+
// Empty ctx for suite.verify; domain separation lives in the
|
|
36
|
+
// cosignature signed-message construction (cosignature/v1 prefix for
|
|
37
|
+
// Ed25519, cosigned_message label for ML-DSA-44) per
|
|
38
|
+
// c2sp.org/tlog-cosignature §Format.
|
|
39
|
+
const EMPTY_CTX = new Uint8Array(0);
|
|
40
|
+
const SHA2_MODULE = 'sha2';
|
|
41
|
+
/**
|
|
42
|
+
* Trust-anchored verifier for c2sp.org/tlog-checkpoint envelopes.
|
|
43
|
+
* Takes a fixed log identity at construction and exposes three verify
|
|
44
|
+
* methods (`verifyCheckpoint`, `verifyInclusion`, `verifyConsistency`)
|
|
45
|
+
* that return `boolean`.
|
|
46
|
+
*
|
|
47
|
+
* Construction is the only place this class throws; every verify path
|
|
48
|
+
* returns `false` on any failure mode including malformed bytes,
|
|
49
|
+
* tampered envelopes, wrong origin, wrong leaf, and signature failure.
|
|
50
|
+
* The convention matches `SignatureSuite.verify` and lets normie
|
|
51
|
+
* callers write a single `if (!verifier.verifyX(...)) reject()` line
|
|
52
|
+
* per check without a try / catch.
|
|
53
|
+
*/
|
|
54
|
+
export class MerkleVerifier {
|
|
55
|
+
origin;
|
|
56
|
+
pubkey;
|
|
57
|
+
hasher;
|
|
58
|
+
suite;
|
|
59
|
+
_algoEntry;
|
|
60
|
+
_keyId;
|
|
61
|
+
constructor(opts) {
|
|
62
|
+
const { origin, pubkey, hashing, suite } = opts;
|
|
63
|
+
if (typeof origin !== 'string' || origin.length === 0)
|
|
64
|
+
throw new MerkleLogError('origin-invalid', 'MerkleVerifier: origin must be a non-empty string');
|
|
65
|
+
// c2sp.org/tlog-checkpoint §Note text MUSTs, mirrored from
|
|
66
|
+
// `SignedLog`'s constructor: the origin is the first body line
|
|
67
|
+
// and may not contain whitespace or plus characters.
|
|
68
|
+
if (/\s/.test(origin) || origin.includes('+'))
|
|
69
|
+
throw new MerkleLogError('origin-invalid', 'MerkleVerifier: origin must not contain whitespace or plus characters');
|
|
70
|
+
if (!(pubkey instanceof Uint8Array))
|
|
71
|
+
throw new MerkleLogError('pubkey-size', 'MerkleVerifier: pubkey must be a Uint8Array');
|
|
72
|
+
const hasher = resolveHasher(hashing);
|
|
73
|
+
const algoEntry = lookupAlgoEntryByFormatEnum(suite.formatEnum);
|
|
74
|
+
if (algoEntry === undefined)
|
|
75
|
+
throw new MerkleLogError('unsupported-suite', `MerkleVerifier: suite '${suite.formatName}' (formatEnum 0x${suite.formatEnum
|
|
76
|
+
.toString(16)
|
|
77
|
+
.padStart(2, '0')}) has no c2sp.org/tlog-cosignature §Format algorithm byte; `
|
|
78
|
+
+ 'use Ed25519Suite or MlDsa44Suite, or open an issue for a newly C2SP-registered suite');
|
|
79
|
+
if (pubkey.length !== suite.pkSize)
|
|
80
|
+
throw new MerkleLogError('pubkey-size', `MerkleVerifier: pubkey length ${pubkey.length} != suite.pkSize ${suite.pkSize}`);
|
|
81
|
+
// Same modules `SignedLog` requires: the suite's modules, the
|
|
82
|
+
// hasher's module, and sha2 for `deriveKeyId`. Constructor-time
|
|
83
|
+
// check so a verifier built before `init()` fails at construction
|
|
84
|
+
// rather than on first `verifyCheckpoint` call.
|
|
85
|
+
assertModulesInitialized([
|
|
86
|
+
...suite.wasmModules,
|
|
87
|
+
...hasher.wasmModules,
|
|
88
|
+
SHA2_MODULE,
|
|
89
|
+
]);
|
|
90
|
+
this.origin = origin;
|
|
91
|
+
this.pubkey = pubkey.slice();
|
|
92
|
+
this.hasher = hasher;
|
|
93
|
+
this.suite = suite;
|
|
94
|
+
this._algoEntry = algoEntry;
|
|
95
|
+
this._keyId = deriveKeyId(origin, algoEntry.algoByte, this.pubkey);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Verify a signed-note envelope against this verifier's identity.
|
|
99
|
+
* Returns `true` iff the envelope parses, the body's origin equals
|
|
100
|
+
* the constructor origin, the body's root-hash length equals the
|
|
101
|
+
* hasher's `outputSize`, a signature line's keyId equals the
|
|
102
|
+
* pubkey-derived keyId, the `timestamped_signature` payload on
|
|
103
|
+
* that line decodes cleanly, and `suite.verify` accepts the
|
|
104
|
+
* reconstructed cosignature signed message.
|
|
105
|
+
*
|
|
106
|
+
* Returns `false` on every other path. Never throws on envelope
|
|
107
|
+
* content.
|
|
108
|
+
*/
|
|
109
|
+
verifyCheckpoint(envelopeBytes) {
|
|
110
|
+
const parsed = this._parseAndVerify(envelopeBytes);
|
|
111
|
+
return parsed !== null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Verify a leaf's inclusion in the tree committed by an envelope.
|
|
115
|
+
* Runs `verifyCheckpoint` first; on failure returns `false`
|
|
116
|
+
* without examining the proof. On success, hashes `leafBytes`
|
|
117
|
+
* with the verifier's `Hasher` and calls `verifyInclusionProof`
|
|
118
|
+
* against the body's `treeSize` and `rootHash` per RFC 9162 §2.1.3.
|
|
119
|
+
*
|
|
120
|
+
* The "verify checkpoint first" ordering is the security-critical
|
|
121
|
+
* step: the proof is bound to the root hash inside the signed body,
|
|
122
|
+
* so trusting the proof before checking the signature would let any
|
|
123
|
+
* forger pair a malicious proof with their own root.
|
|
124
|
+
*/
|
|
125
|
+
verifyInclusion(opts) {
|
|
126
|
+
const parsed = this._parseAndVerify(opts.envelopeBytes);
|
|
127
|
+
if (parsed === null)
|
|
128
|
+
return false;
|
|
129
|
+
if (!(opts.leafBytes instanceof Uint8Array))
|
|
130
|
+
return false;
|
|
131
|
+
if (!Number.isInteger(opts.leafIndex) || opts.leafIndex < 0)
|
|
132
|
+
return false;
|
|
133
|
+
if (opts.leafIndex >= parsed.treeSize)
|
|
134
|
+
return false;
|
|
135
|
+
if (!Array.isArray(opts.proof))
|
|
136
|
+
return false;
|
|
137
|
+
for (const h of opts.proof)
|
|
138
|
+
if (!(h instanceof Uint8Array))
|
|
139
|
+
return false;
|
|
140
|
+
// RFC 9162 §2.1.1: leaf-hash domain separation happens here;
|
|
141
|
+
// the proof verifier expects the MTH({d}) of the leaf, not the
|
|
142
|
+
// raw leaf bytes. Computing it locally rather than accepting a
|
|
143
|
+
// caller-supplied leaf hash closes the "we trust the proof
|
|
144
|
+
// because we trust the leaf hash the caller gave us" gap.
|
|
145
|
+
const leafHash = this.hasher.hashLeaf(opts.leafBytes);
|
|
146
|
+
try {
|
|
147
|
+
return verifyInclusionProof({
|
|
148
|
+
hasher: this.hasher,
|
|
149
|
+
leafHash,
|
|
150
|
+
leafIndex: opts.leafIndex,
|
|
151
|
+
treeSize: parsed.treeSize,
|
|
152
|
+
proof: opts.proof,
|
|
153
|
+
rootHash: parsed.rootHash,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// `verifyInclusionProof` throws on a wrong-sized rootHash or
|
|
158
|
+
// out-of-range leafIndex. Convert to a verify-false: the
|
|
159
|
+
// normie surface keeps a single failure mode.
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Verify that the tree committed by `oldEnvelopeBytes` is a prefix
|
|
165
|
+
* of the tree committed by `newEnvelopeBytes`. Both envelopes must
|
|
166
|
+
* verify under this verifier's identity; if either fails, returns
|
|
167
|
+
* `false`. On success, calls `verifyConsistencyProof` per
|
|
168
|
+
* RFC 9162 §2.1.4 against the two sizes and roots.
|
|
169
|
+
*/
|
|
170
|
+
verifyConsistency(opts) {
|
|
171
|
+
const oldParsed = this._parseAndVerify(opts.oldEnvelopeBytes);
|
|
172
|
+
if (oldParsed === null)
|
|
173
|
+
return false;
|
|
174
|
+
const newParsed = this._parseAndVerify(opts.newEnvelopeBytes);
|
|
175
|
+
if (newParsed === null)
|
|
176
|
+
return false;
|
|
177
|
+
if (!Array.isArray(opts.proof))
|
|
178
|
+
return false;
|
|
179
|
+
for (const h of opts.proof)
|
|
180
|
+
if (!(h instanceof Uint8Array))
|
|
181
|
+
return false;
|
|
182
|
+
try {
|
|
183
|
+
return verifyConsistencyProof({
|
|
184
|
+
hasher: this.hasher,
|
|
185
|
+
oldSize: oldParsed.treeSize,
|
|
186
|
+
newSize: newParsed.treeSize,
|
|
187
|
+
oldRoot: oldParsed.rootHash,
|
|
188
|
+
newRoot: newParsed.rootHash,
|
|
189
|
+
proof: opts.proof,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// ── internal ────────────────────────────────────────────────────────
|
|
197
|
+
/**
|
|
198
|
+
* Parse a signed-note envelope, verify the cosignature, and return
|
|
199
|
+
* the decoded `Checkpoint`. Returns `null` on any failure mode:
|
|
200
|
+
* malformed envelope, malformed body, wrong origin, wrong root-hash
|
|
201
|
+
* length, no matching keyId line, malformed payload, signature
|
|
202
|
+
* failure. Keyed-ID comparison uses `constantTimeEqual` for hygiene
|
|
203
|
+
* around key-material-adjacent state.
|
|
204
|
+
*/
|
|
205
|
+
_parseAndVerify(bytes) {
|
|
206
|
+
if (!(bytes instanceof Uint8Array))
|
|
207
|
+
return null;
|
|
208
|
+
let env;
|
|
209
|
+
try {
|
|
210
|
+
env = parseSignedNote(bytes);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
let checkpoint;
|
|
216
|
+
try {
|
|
217
|
+
checkpoint = parseCheckpointBody(env.body, this.hasher.outputSize);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
if (checkpoint.origin !== this.origin)
|
|
223
|
+
return null;
|
|
224
|
+
if (checkpoint.rootHash.length !== this.hasher.outputSize)
|
|
225
|
+
return null;
|
|
226
|
+
const matching = env.signatures.find(s => s.keyId.length === this._keyId.length
|
|
227
|
+
&& constantTimeEqual(s.keyId, this._keyId));
|
|
228
|
+
if (!matching)
|
|
229
|
+
return null;
|
|
230
|
+
let payload;
|
|
231
|
+
try {
|
|
232
|
+
payload = parseCosigSignaturePayload(matching.signature, this._algoEntry.sigSize);
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
if (err instanceof MerkleCodecError)
|
|
236
|
+
return null;
|
|
237
|
+
throw err;
|
|
238
|
+
}
|
|
239
|
+
let signedMessage;
|
|
240
|
+
try {
|
|
241
|
+
signedMessage = this._buildSignedMessage(env.body, payload.timestamp, checkpoint);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const ok = this.suite.verify(this.pubkey, signedMessage, payload.signature, EMPTY_CTX);
|
|
247
|
+
return ok ? checkpoint : null;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Dispatch cosignature signed-message construction on the algorithm
|
|
251
|
+
* registry entry's `messageConstruction`. Mirrors `SignedLog`'s
|
|
252
|
+
* dispatch so producer and verifier always agree on the bytes the
|
|
253
|
+
* suite verifies against.
|
|
254
|
+
*
|
|
255
|
+
* 'cosig' c2sp.org/tlog-cosignature §"Ed25519 signed
|
|
256
|
+
* message". The full envelope body is embedded
|
|
257
|
+
* verbatim after the cosignature/v1 + time
|
|
258
|
+
* prefix.
|
|
259
|
+
*
|
|
260
|
+
* 'cosigned-message' c2sp.org/tlog-cosignature §"ML-DSA-44
|
|
261
|
+
* signed message". cosigner_name and
|
|
262
|
+
* log_origin both equal the checkpoint origin
|
|
263
|
+
* for a log's self-cosignature; start == 0;
|
|
264
|
+
* end == treeSize; hash == rootHash.
|
|
265
|
+
*/
|
|
266
|
+
_buildSignedMessage(body, timestamp, cp) {
|
|
267
|
+
if (this._algoEntry.messageConstruction === 'cosig')
|
|
268
|
+
return buildCosigSignedMessage(body, timestamp);
|
|
269
|
+
return buildCosignedMessage({
|
|
270
|
+
cosignerName: this.origin,
|
|
271
|
+
timestamp,
|
|
272
|
+
logOrigin: this.origin,
|
|
273
|
+
start: 0,
|
|
274
|
+
end: cp.treeSize,
|
|
275
|
+
hash: cp.rootHash,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function resolveHasher(hashing) {
|
|
280
|
+
if (hashing === 'sha256')
|
|
281
|
+
return Sha256Hasher;
|
|
282
|
+
if (hashing === 'blake3')
|
|
283
|
+
return Blake3Hasher;
|
|
284
|
+
throw new MerkleLogError('unsupported-hashing', `MerkleVerifier: hashing must be 'sha256' or 'blake3', got '${hashing}'`);
|
|
285
|
+
}
|
|
286
|
+
function assertModulesInitialized(modules) {
|
|
287
|
+
const seen = new Set();
|
|
288
|
+
for (const mod of modules) {
|
|
289
|
+
if (seen.has(mod))
|
|
290
|
+
continue;
|
|
291
|
+
seen.add(mod);
|
|
292
|
+
if (!isInitialized(mod))
|
|
293
|
+
throw new MerkleLogError('module-not-initialized', `MerkleVerifier: WASM module '${mod}' is not initialized; `
|
|
294
|
+
+ 'call init() with the appropriate sources before constructing MerkleVerifier');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Hasher } from './tree.js';
|
|
2
|
+
export interface VerifyInclusionInput {
|
|
3
|
+
hasher: Hasher;
|
|
4
|
+
leafHash: Uint8Array;
|
|
5
|
+
leafIndex: number;
|
|
6
|
+
treeSize: number;
|
|
7
|
+
proof: readonly Uint8Array[];
|
|
8
|
+
rootHash: Uint8Array;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* RFC 9162 §2.1.3, Inclusion Proof Verification. Returns true if the
|
|
12
|
+
* proof reconstructs `rootHash` from `leafHash` at position
|
|
13
|
+
* (leafIndex, treeSize). Wrong proof length, wrong leaf-hash size, or
|
|
14
|
+
* a reconstructed root that differs from `rootHash` all return false.
|
|
15
|
+
* Contract violations (negative or out-of-range index, treeSize <= 0,
|
|
16
|
+
* wrong-sized rootHash) throw RangeError.
|
|
17
|
+
*
|
|
18
|
+
* `leafHash` is the leaf's MTH ({d_m} hashed under the leaf prefix), not
|
|
19
|
+
* the raw leaf bytes. Thin verifiers receiving a leaf over the wire
|
|
20
|
+
* should compute `hasher.hashLeaf(bytes)` before calling.
|
|
21
|
+
*/
|
|
22
|
+
export declare function verifyInclusionProof(input: VerifyInclusionInput): boolean;
|
|
23
|
+
export interface VerifyConsistencyInput {
|
|
24
|
+
hasher: Hasher;
|
|
25
|
+
oldSize: number;
|
|
26
|
+
newSize: number;
|
|
27
|
+
oldRoot: Uint8Array;
|
|
28
|
+
newRoot: Uint8Array;
|
|
29
|
+
proof: readonly Uint8Array[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* RFC 9162 §2.1.4, Consistency Proof Verification. Returns true if
|
|
33
|
+
* `proof` proves that the size-`oldSize` tree with root `oldRoot` is a
|
|
34
|
+
* prefix of the size-`newSize` tree with root `newRoot`.
|
|
35
|
+
*
|
|
36
|
+
* Malformed-proof conditions (wrong proof length, non-empty proof when
|
|
37
|
+
* one is forbidden, mismatched old/new root reconstruction) return
|
|
38
|
+
* false. Contract violations (`oldSize > newSize`, wrong-sized root)
|
|
39
|
+
* throw RangeError; the special "consistency from empty tree" form is
|
|
40
|
+
* not part of the wire format and returns false.
|
|
41
|
+
*/
|
|
42
|
+
export declare function verifyConsistencyProof(input: VerifyConsistencyInput): boolean;
|
|
43
|
+
/** Callback the builders use to read the tree without knowing how it is stored. */
|
|
44
|
+
export type GetNode = (level: number, index: number) => Uint8Array;
|
|
45
|
+
export interface BuildInclusionInput {
|
|
46
|
+
hasher: Hasher;
|
|
47
|
+
leafIndex: number;
|
|
48
|
+
treeSize: number;
|
|
49
|
+
getNode: GetNode;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* RFC 9162 §2.1.3: build the inclusion proof for leaf `leafIndex` in
|
|
53
|
+
* a tree of size `treeSize`. The returned bytes are ordered from the
|
|
54
|
+
* lowest level upward (leaf sibling first, root-adjacent last), the
|
|
55
|
+
* order `verifyInclusionProof` consumes.
|
|
56
|
+
*/
|
|
57
|
+
export declare function buildInclusionProof(input: BuildInclusionInput): Uint8Array[];
|
|
58
|
+
export interface BuildConsistencyInput {
|
|
59
|
+
hasher: Hasher;
|
|
60
|
+
oldSize: number;
|
|
61
|
+
newSize: number;
|
|
62
|
+
getNode: GetNode;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* RFC 9162 §2.1.4: build the consistency proof between two tree
|
|
66
|
+
* sizes. Returns an empty array when oldSize equals newSize or
|
|
67
|
+
* oldSize is zero (the verifier rejects the latter, but the builder
|
|
68
|
+
* is symmetric for inspection-time use).
|
|
69
|
+
*/
|
|
70
|
+
export declare function buildConsistencyProof(input: BuildConsistencyInput): Uint8Array[];
|
|
@@ -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
|
+
}
|