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,356 @@
|
|
|
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/signed-log.ts
|
|
23
|
+
//
|
|
24
|
+
// `SignedLog<S extends SignatureSuite>` ties a `MerkleTree` (Sha256Tree
|
|
25
|
+
// or Blake3Tree), a `SignatureSuite` registered in the C2SP cosignature
|
|
26
|
+
// algorithm-byte registry, and an origin string into one object that
|
|
27
|
+
// produces signed checkpoints, verifies received checkpoints, and
|
|
28
|
+
// exposes inclusion / consistency proofs.
|
|
29
|
+
//
|
|
30
|
+
// Wire format per c2sp.org/tlog-cosignature §Format and §"Ed25519
|
|
31
|
+
// signed message" / §"ML-DSA-44 signed message":
|
|
32
|
+
//
|
|
33
|
+
// envelope = emitSignedNote(body, [
|
|
34
|
+
// { name: origin,
|
|
35
|
+
// keyId: deriveKeyId(origin, algoByte, pubkey),
|
|
36
|
+
// signature: emitCosigSignaturePayload(timestamp, sig) },
|
|
37
|
+
// ])
|
|
38
|
+
//
|
|
39
|
+
// where `sig` is the result of `suite.sign(sk, signedMessage, EMPTY_CTX)`
|
|
40
|
+
// and `signedMessage` is dispatched on `algoEntry.messageConstruction`:
|
|
41
|
+
//
|
|
42
|
+
// 'cosig' → buildCosigSignedMessage(body, timestamp)
|
|
43
|
+
// "cosignature/v1\ntime <ts>\n<body>"
|
|
44
|
+
// (Ed25519, C2SP algo byte 0x04)
|
|
45
|
+
//
|
|
46
|
+
// 'cosigned-message' → buildCosignedMessage({...})
|
|
47
|
+
// TLS-Presentation `cosigned_message` struct
|
|
48
|
+
// (ML-DSA-44, C2SP algo byte 0x06)
|
|
49
|
+
//
|
|
50
|
+
// Suites without a registered entry (e.g. EcdsaP256Suite, every prehash
|
|
51
|
+
// variant, all SLH-DSA, every hybrid) cannot construct a `SignedLog`
|
|
52
|
+
// and throw `SigningError('sig-unsupported-suite')` at construction.
|
|
53
|
+
//
|
|
54
|
+
// C2SP commit pinned for this implementation:
|
|
55
|
+
// 3752ba5b3590dc3754e04fcc8369bd3612897c02 (github.com/C2SP/C2SP).
|
|
56
|
+
import { isInitialized } from '../init.js';
|
|
57
|
+
import { SigningError, MerkleCodecError } from '../errors.js';
|
|
58
|
+
import { constantTimeEqual, wipe } from '../utils.js';
|
|
59
|
+
import { emitSignedNote, parseSignedNote, deriveKeyId, lookupAlgoEntryByFormatEnum, buildCosigSignedMessage, buildCosignedMessage, emitCosigSignaturePayload, parseCosigSignaturePayload, } from './signed-note.js';
|
|
60
|
+
import { serializeCheckpointBody, parseCheckpointBody, } from './checkpoint.js';
|
|
61
|
+
// ── Module surface union ────────────────────────────────────────────────────
|
|
62
|
+
const SHA2_MODULE = 'sha2';
|
|
63
|
+
// Empty ctx passed to suite.sign / suite.verify. Domain separation
|
|
64
|
+
// for the signed message is built in to the cosignature/v1 header
|
|
65
|
+
// (Ed25519 case) or the cosigned_message label (ML-DSA-44 case);
|
|
66
|
+
// the suite-level ctx adds no additional binding.
|
|
67
|
+
const EMPTY_CTX = new Uint8Array(0);
|
|
68
|
+
function unionModules(...lists) {
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
for (const list of lists)
|
|
71
|
+
for (const m of list)
|
|
72
|
+
seen.add(m);
|
|
73
|
+
return Object.freeze([...seen]);
|
|
74
|
+
}
|
|
75
|
+
function validateOriginAtConstruction(origin) {
|
|
76
|
+
if (origin.length === 0)
|
|
77
|
+
throw new RangeError('SignedLog: origin must be non-empty');
|
|
78
|
+
// c2sp.org/tlog-checkpoint §Note text MUSTs; fail at construction
|
|
79
|
+
// not serialize time.
|
|
80
|
+
if (/\s/.test(origin) || origin.includes('+'))
|
|
81
|
+
throw new RangeError('SignedLog: origin must not contain whitespace or plus characters');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Signed transparency log substrate. Combines a `MerkleTree` with a
|
|
85
|
+
* registered cosignature `SignatureSuite` and an origin string;
|
|
86
|
+
* exposes append, proof, and cosignature sign / verify operations.
|
|
87
|
+
*
|
|
88
|
+
* Per-call WASM lifecycle is enforced by the suite itself (see the
|
|
89
|
+
* SignatureSuite factories under `src/ts/sign/suites/`). `SignedLog`
|
|
90
|
+
* does not wrap additional try/finally around `suite.sign` /
|
|
91
|
+
* `suite.verify` because the suite already does. Internally the
|
|
92
|
+
* SignedLog owns a private copy of the signing key wiped by
|
|
93
|
+
* `dispose()`.
|
|
94
|
+
*/
|
|
95
|
+
export class SignedLog {
|
|
96
|
+
tree;
|
|
97
|
+
suite;
|
|
98
|
+
origin;
|
|
99
|
+
pubkey;
|
|
100
|
+
wasmModules;
|
|
101
|
+
_algoEntry;
|
|
102
|
+
_keyId;
|
|
103
|
+
_signingKey;
|
|
104
|
+
_disposed = false;
|
|
105
|
+
constructor(opts) {
|
|
106
|
+
const { tree, suite, origin, signingKey, pubkey } = opts;
|
|
107
|
+
validateOriginAtConstruction(origin);
|
|
108
|
+
if (!(signingKey instanceof Uint8Array))
|
|
109
|
+
throw new TypeError('SignedLog: signingKey must be a Uint8Array');
|
|
110
|
+
if (!(pubkey instanceof Uint8Array))
|
|
111
|
+
throw new TypeError('SignedLog: pubkey must be a Uint8Array');
|
|
112
|
+
if (signingKey.length !== suite.skSize)
|
|
113
|
+
throw new RangeError(`SignedLog: signingKey length ${signingKey.length} != suite.skSize ${suite.skSize}`);
|
|
114
|
+
if (pubkey.length !== suite.pkSize)
|
|
115
|
+
throw new RangeError(`SignedLog: pubkey length ${pubkey.length} != suite.pkSize ${suite.pkSize}`);
|
|
116
|
+
const algoEntry = lookupAlgoEntryByFormatEnum(suite.formatEnum);
|
|
117
|
+
if (algoEntry === undefined)
|
|
118
|
+
throw new SigningError('sig-unsupported-suite', `SignedLog: suite formatEnum 0x${suite.formatEnum.toString(16).padStart(2, '0')} `
|
|
119
|
+
+ `(${suite.formatName}) has no C2SP signed-note algorithm byte registered; `
|
|
120
|
+
+ 'see c2sp.org/tlog-cosignature §Format for the supported algorithms');
|
|
121
|
+
const wasmModules = unionModules(tree.hasher.wasmModules, suite.wasmModules, [SHA2_MODULE]);
|
|
122
|
+
for (const mod of wasmModules) {
|
|
123
|
+
if (!isInitialized(mod))
|
|
124
|
+
throw new Error(`SignedLog: WASM module '${mod}' is not initialized; `
|
|
125
|
+
+ 'call init() with the appropriate sources before constructing SignedLog');
|
|
126
|
+
}
|
|
127
|
+
this.tree = tree;
|
|
128
|
+
this.suite = suite;
|
|
129
|
+
this.origin = origin;
|
|
130
|
+
this.pubkey = pubkey.slice();
|
|
131
|
+
this.wasmModules = wasmModules;
|
|
132
|
+
this._signingKey = signingKey.slice();
|
|
133
|
+
this._algoEntry = algoEntry;
|
|
134
|
+
this._keyId = deriveKeyId(origin, algoEntry.algoByte, this.pubkey);
|
|
135
|
+
}
|
|
136
|
+
// ── Tree passthroughs ──────────────────────────────────────────────
|
|
137
|
+
/**
|
|
138
|
+
* Append a leaf to the underlying tree and return the new leaf's
|
|
139
|
+
* index, hash, and inclusion proof against the post-append tree size.
|
|
140
|
+
*/
|
|
141
|
+
append(leafBytes) {
|
|
142
|
+
this._assertNotDisposed();
|
|
143
|
+
const { leafIndex, leafHash } = this.tree.append(leafBytes);
|
|
144
|
+
const inclusionProof = this.tree.getInclusionProof(leafIndex, this.tree.size());
|
|
145
|
+
return { leafIndex, leafHash, inclusionProof };
|
|
146
|
+
}
|
|
147
|
+
size() {
|
|
148
|
+
this._assertNotDisposed();
|
|
149
|
+
return this.tree.size();
|
|
150
|
+
}
|
|
151
|
+
rootHash() {
|
|
152
|
+
this._assertNotDisposed();
|
|
153
|
+
return this.tree.rootHash();
|
|
154
|
+
}
|
|
155
|
+
getInclusionProof(leafIndex, treeSize) {
|
|
156
|
+
this._assertNotDisposed();
|
|
157
|
+
return this.tree.getInclusionProof(leafIndex, treeSize);
|
|
158
|
+
}
|
|
159
|
+
getConsistencyProof(oldSize, newSize) {
|
|
160
|
+
this._assertNotDisposed();
|
|
161
|
+
return this.tree.getConsistencyProof(oldSize, newSize);
|
|
162
|
+
}
|
|
163
|
+
// ── Sign + verify ──────────────────────────────────────────────────
|
|
164
|
+
/**
|
|
165
|
+
* Issue a cosignature over the current checkpoint and emit the
|
|
166
|
+
* signed-note envelope per c2sp.org/signed-note §Format. The
|
|
167
|
+
* signature line carries the `timestamped_signature` payload
|
|
168
|
+
* from c2sp.org/tlog-cosignature §Format; the bytes the suite
|
|
169
|
+
* signs are dispatched on the algorithm's
|
|
170
|
+
* `messageConstruction`:
|
|
171
|
+
*
|
|
172
|
+
* - `'cosig'` → `buildCosigSignedMessage(body, ts)`
|
|
173
|
+
* (Ed25519, §"Ed25519 signed message")
|
|
174
|
+
* - `'cosigned-message'` → `buildCosignedMessage(...)`
|
|
175
|
+
* (ML-DSA-44, §"ML-DSA-44 signed message")
|
|
176
|
+
*
|
|
177
|
+
* `timestamp` defaults to current wall-clock POSIX seconds. The
|
|
178
|
+
* c2sp.org/tlog-witness `add-checkpoint` rule mandates a non-zero
|
|
179
|
+
* timestamp on production cosignatures; `0` is accepted by this
|
|
180
|
+
* function for test reproducibility but witness verifiers will
|
|
181
|
+
* reject envelopes that carry it. Tests and vector generators
|
|
182
|
+
* pass an explicit value to lock byte stability.
|
|
183
|
+
*/
|
|
184
|
+
signCheckpoint(opts) {
|
|
185
|
+
this._assertNotDisposed();
|
|
186
|
+
const timestamp = opts?.timestamp ?? Math.floor(Date.now() / 1000);
|
|
187
|
+
const body = serializeCheckpointBody({
|
|
188
|
+
origin: this.origin,
|
|
189
|
+
treeSize: this.tree.size(),
|
|
190
|
+
rootHash: this.tree.rootHash(),
|
|
191
|
+
});
|
|
192
|
+
const signedMessage = this._buildSignedMessage(body, timestamp);
|
|
193
|
+
const sig = this.suite.sign(this._signingKey, signedMessage, EMPTY_CTX);
|
|
194
|
+
if (sig.length !== this._algoEntry.sigSize)
|
|
195
|
+
throw new SigningError('sig-malformed-input', `SignedLog.signCheckpoint: suite.sign returned ${sig.length} bytes, `
|
|
196
|
+
+ `expected ${this._algoEntry.sigSize} per c2sp.org/tlog-cosignature §Format`);
|
|
197
|
+
const payload = emitCosigSignaturePayload(timestamp, sig);
|
|
198
|
+
const line = {
|
|
199
|
+
name: this.origin,
|
|
200
|
+
keyId: this._keyId,
|
|
201
|
+
signature: payload,
|
|
202
|
+
};
|
|
203
|
+
return emitSignedNote(body, [line]);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Parse a signed-note envelope into the structured `SignedTreeHead`
|
|
207
|
+
* form per c2sp.org/signed-note §Format. Surfaces the body's
|
|
208
|
+
* decoded `Checkpoint`, the signature lines that survived the
|
|
209
|
+
* permissive signed-note parse, and the primary log cosignature's
|
|
210
|
+
* POSIX-seconds timestamp (extracted via
|
|
211
|
+
* `parseCosigSignaturePayload` on the line whose keyId matches
|
|
212
|
+
* this log's pubkey-derived keyId).
|
|
213
|
+
*
|
|
214
|
+
* If no signature line matches, `timestamp` is reported as 0. The
|
|
215
|
+
* field is informational at parse time; cryptographic verification
|
|
216
|
+
* lives in `verifyCheckpoint`. Throws `RangeError` on whole-envelope
|
|
217
|
+
* structural failure (the parseSignedNote / parseCheckpointBody
|
|
218
|
+
* contract); does not throw on signature line content issues.
|
|
219
|
+
*/
|
|
220
|
+
parseCheckpoint(bytes) {
|
|
221
|
+
this._assertNotDisposed();
|
|
222
|
+
const env = parseSignedNote(bytes);
|
|
223
|
+
const checkpoint = parseCheckpointBody(env.body, this.tree.hasher.outputSize);
|
|
224
|
+
let timestamp = 0;
|
|
225
|
+
const matching = env.signatures.find(s => s.keyId.length === this._keyId.length
|
|
226
|
+
&& constantTimeEqual(s.keyId, this._keyId));
|
|
227
|
+
if (matching) {
|
|
228
|
+
try {
|
|
229
|
+
const parsed = parseCosigSignaturePayload(matching.signature, this._algoEntry.sigSize);
|
|
230
|
+
timestamp = parsed.timestamp;
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
// Soft-fail timestamp to 0 on payload codec error; verifyCheckpoint surfaces hard fail.
|
|
234
|
+
if (!(err instanceof MerkleCodecError))
|
|
235
|
+
throw err;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return { checkpoint, signatures: env.signatures, timestamp };
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Verify a signed-note envelope against this SignedLog's origin,
|
|
242
|
+
* pubkey, suite, and tree hasher. Returns `true` iff the envelope
|
|
243
|
+
* parses, carries a signature line whose keyId matches this log's
|
|
244
|
+
* pubkey-derived keyId, the `timestamped_signature` payload on
|
|
245
|
+
* that line decodes cleanly, and the signature verifies under
|
|
246
|
+
* `suite.verify` over the cosignature signed message reconstructed
|
|
247
|
+
* with the parsed timestamp.
|
|
248
|
+
*
|
|
249
|
+
* Returns `false` on every soft-fail mode: wrong origin, wrong
|
|
250
|
+
* root-hash length, no matching keyId line, malformed payload,
|
|
251
|
+
* signature failure. Throws only on this log's own disposed
|
|
252
|
+
* state; never on envelope content (envelope content is public,
|
|
253
|
+
* so timing distinctions on its content are not security-sensitive).
|
|
254
|
+
*
|
|
255
|
+
* The keyId comparison uses `constantTimeEqual` for hygiene around
|
|
256
|
+
* key-material-adjacent state; the origin and root-hash-length
|
|
257
|
+
* early returns are intentional non-constant-time exits since
|
|
258
|
+
* both fields are public per the spec.
|
|
259
|
+
*/
|
|
260
|
+
verifyCheckpoint(bytes) {
|
|
261
|
+
this._assertNotDisposed();
|
|
262
|
+
let env;
|
|
263
|
+
try {
|
|
264
|
+
env = parseSignedNote(bytes);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
let checkpoint;
|
|
270
|
+
try {
|
|
271
|
+
checkpoint = parseCheckpointBody(env.body, this.tree.hasher.outputSize);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
if (checkpoint.origin !== this.origin)
|
|
277
|
+
return false;
|
|
278
|
+
if (checkpoint.rootHash.length !== this.tree.hasher.outputSize)
|
|
279
|
+
return false;
|
|
280
|
+
const matching = env.signatures.find(s => s.keyId.length === this._keyId.length
|
|
281
|
+
&& constantTimeEqual(s.keyId, this._keyId));
|
|
282
|
+
if (!matching)
|
|
283
|
+
return false;
|
|
284
|
+
let payload;
|
|
285
|
+
try {
|
|
286
|
+
payload = parseCosigSignaturePayload(matching.signature, this._algoEntry.sigSize);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
let signedMessage;
|
|
292
|
+
try {
|
|
293
|
+
signedMessage = this._buildSignedMessage(env.body, payload.timestamp);
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
// Reconstruction failed (e.g. timestamp out of range, or
|
|
297
|
+
// the cosigned_message branch's start/end/state contract
|
|
298
|
+
// violated by some attacker-influenced field). Treat as
|
|
299
|
+
// verify-false.
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
return this.suite.verify(this.pubkey, signedMessage, payload.signature, EMPTY_CTX);
|
|
303
|
+
}
|
|
304
|
+
// ── Lifecycle ──────────────────────────────────────────────────────
|
|
305
|
+
/**
|
|
306
|
+
* Zero the stored signing-key copy. Idempotent. Subsequent calls
|
|
307
|
+
* to any public method throw.
|
|
308
|
+
*/
|
|
309
|
+
dispose() {
|
|
310
|
+
if (this._disposed)
|
|
311
|
+
return;
|
|
312
|
+
wipe(this._signingKey);
|
|
313
|
+
this._signingKey = new Uint8Array(0);
|
|
314
|
+
this._disposed = true;
|
|
315
|
+
}
|
|
316
|
+
_assertNotDisposed() {
|
|
317
|
+
if (this._disposed)
|
|
318
|
+
throw new Error('SignedLog: instance has been disposed');
|
|
319
|
+
}
|
|
320
|
+
// ── Internal: message construction dispatch ────────────────────────
|
|
321
|
+
/**
|
|
322
|
+
* Dispatch the cosignature signed-message construction on the
|
|
323
|
+
* algorithm-byte registry entry's `messageConstruction`. The
|
|
324
|
+
* `body` argument is the canonical checkpoint body from
|
|
325
|
+
* `serializeCheckpointBody`, ending in 0x0A.
|
|
326
|
+
*
|
|
327
|
+
* 'cosig' c2sp.org/tlog-cosignature §"Ed25519 signed
|
|
328
|
+
* message". The full envelope body is
|
|
329
|
+
* embedded verbatim after the
|
|
330
|
+
* cosignature/v1 + time prefix.
|
|
331
|
+
*
|
|
332
|
+
* 'cosigned-message' c2sp.org/tlog-cosignature §"ML-DSA-44
|
|
333
|
+
* signed message". The body is decomposed
|
|
334
|
+
* into origin, tree size, and root hash;
|
|
335
|
+
* cosigner_name == origin (Phase 7 logs sign
|
|
336
|
+
* their own checkpoints); start == 0; end ==
|
|
337
|
+
* tree size; hash == root hash.
|
|
338
|
+
*/
|
|
339
|
+
_buildSignedMessage(body, timestamp) {
|
|
340
|
+
if (this._algoEntry.messageConstruction === 'cosig')
|
|
341
|
+
return buildCosigSignedMessage(body, timestamp);
|
|
342
|
+
// 'cosigned-message' branch: decompose the body to feed the
|
|
343
|
+
// TLS-Presentation struct. `parseCheckpointBody` is the
|
|
344
|
+
// authoritative source of truth for the three-line body
|
|
345
|
+
// layout.
|
|
346
|
+
const cp = parseCheckpointBody(body, this.tree.hasher.outputSize);
|
|
347
|
+
return buildCosignedMessage({
|
|
348
|
+
cosignerName: this.origin,
|
|
349
|
+
timestamp,
|
|
350
|
+
logOrigin: this.origin,
|
|
351
|
+
start: 0,
|
|
352
|
+
end: cp.treeSize,
|
|
353
|
+
hash: cp.rootHash,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* c2sp.org/signed-note §Format signature type for plain Ed25519
|
|
3
|
+
* signatures over the raw note text per RFC 8032. Listed for spec
|
|
4
|
+
* completeness; no leviathan SignatureSuite currently routes here
|
|
5
|
+
* because Phase 7 cosignatures use the timestamped Ed25519 variant
|
|
6
|
+
* (`ALGO_BYTE_ED25519_COSIG`) per c2sp.org/tlog-cosignature §Format.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ALGO_BYTE_ED25519_NOTE = 1;
|
|
9
|
+
/**
|
|
10
|
+
* c2sp.org/tlog-cosignature §Format signature type for timestamped
|
|
11
|
+
* Ed25519 checkpoint cosignatures. The signature payload is
|
|
12
|
+
* `u64_be(timestamp) || ed25519_signature(64)` for a total of 72
|
|
13
|
+
* bytes, base64-encoded together with the 4-byte key ID on the
|
|
14
|
+
* signature line.
|
|
15
|
+
*/
|
|
16
|
+
export declare const ALGO_BYTE_ED25519_COSIG = 4;
|
|
17
|
+
/**
|
|
18
|
+
* c2sp.org/tlog-cosignature §Format signature type for timestamped
|
|
19
|
+
* ML-DSA-44 (sub)tree cosignatures. The signature payload is
|
|
20
|
+
* `u64_be(timestamp) || ml_dsa_44_signature(2420)` for a total of
|
|
21
|
+
* 2428 bytes, base64-encoded together with the 4-byte key ID.
|
|
22
|
+
*/
|
|
23
|
+
export declare const ALGO_BYTE_MLDSA44_COSIG = 6;
|
|
24
|
+
/**
|
|
25
|
+
* How the cosigner constructs the bytes it signs.
|
|
26
|
+
*
|
|
27
|
+
* 'cosig' c2sp.org/tlog-cosignature §"Ed25519 signed
|
|
28
|
+
* message". Produced by `buildCosigSignedMessage`.
|
|
29
|
+
* 'cosigned-message' c2sp.org/tlog-cosignature §"ML-DSA-44 signed
|
|
30
|
+
* message", `cosigned_message` struct.
|
|
31
|
+
*/
|
|
32
|
+
export type MessageConstruction = 'cosig' | 'cosigned-message';
|
|
33
|
+
/**
|
|
34
|
+
* Per-signature payload encoding bundled with the 4-byte key ID.
|
|
35
|
+
*
|
|
36
|
+
* 'timestamped' c2sp.org/tlog-cosignature §Format
|
|
37
|
+
* `timestamped_signature` struct, shared by 0x04 and 0x06.
|
|
38
|
+
*/
|
|
39
|
+
export type SignaturePayload = 'timestamped';
|
|
40
|
+
/**
|
|
41
|
+
* Per c2sp.org/tlog-cosignature §Format algorithm-byte registry. One
|
|
42
|
+
* entry per registered (leviathan suite, C2SP byte) pair; the entry
|
|
43
|
+
* carries the message-construction and payload-encoding rules a
|
|
44
|
+
* cosigner needs to sign and a verifier needs to parse.
|
|
45
|
+
*/
|
|
46
|
+
export interface AlgoEntry {
|
|
47
|
+
/** Leviathan `SignatureSuite.formatEnum`. */
|
|
48
|
+
readonly formatEnum: number;
|
|
49
|
+
/** C2SP signed-note algorithm byte from §Format. */
|
|
50
|
+
readonly algoByte: number;
|
|
51
|
+
/** How the cosigner constructs the bytes it signs. */
|
|
52
|
+
readonly messageConstruction: MessageConstruction;
|
|
53
|
+
/** How the per-signature payload is encoded on the signature line. */
|
|
54
|
+
readonly signaturePayload: SignaturePayload;
|
|
55
|
+
/**
|
|
56
|
+
* Raw signature size in bytes from the underlying primitive. For
|
|
57
|
+
* Ed25519 this is 64 (RFC 8032 §5.1.6); for ML-DSA-44 this is
|
|
58
|
+
* 2420 (FIPS 204 Table 1). The `timestamped` payload encoding
|
|
59
|
+
* adds an 8-byte BE timestamp prefix per `timestamped_signature`,
|
|
60
|
+
* so the total payload length on the wire is `8 + sigSize`.
|
|
61
|
+
*/
|
|
62
|
+
readonly sigSize: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Look up the algo-entry for a leviathan `SignatureSuite.formatEnum`.
|
|
66
|
+
* Returns `undefined` for suites not registered in the catalog;
|
|
67
|
+
* callers that need a hard guarantee should check the return value
|
|
68
|
+
* and raise an issue per AGENTS.md rather than locally mint a byte
|
|
69
|
+
* for a suite the C2SP spec has not registered.
|
|
70
|
+
*/
|
|
71
|
+
export declare function lookupAlgoEntryByFormatEnum(formatEnum: number): AlgoEntry | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Look up the algo-entry for a wire-format C2SP algorithm byte. Used
|
|
74
|
+
* by verifiers that see an unknown signature line and need to decide
|
|
75
|
+
* how to reshape the payload (or whether to defer to
|
|
76
|
+
* `parseSignedNote`'s "unknown signatures MUST be ignored" rule).
|
|
77
|
+
*/
|
|
78
|
+
export declare function lookupAlgoEntryByByte(algoByte: number): AlgoEntry | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Resolve a leviathan SignatureSuite formatEnum to its C2SP signed-note
|
|
81
|
+
* algorithm byte. Thin shim over `lookupAlgoEntryByFormatEnum`; kept for
|
|
82
|
+
* the call sites that only need the byte (e.g. `deriveKeyId` callers).
|
|
83
|
+
*/
|
|
84
|
+
export declare function suiteFormatEnumToAlgoByte(formatEnum: number): number | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Per c2sp.org/signed-note §Signatures and c2sp.org/tlog-cosignature
|
|
87
|
+
* §Format, the recommended key ID is:
|
|
88
|
+
*
|
|
89
|
+
* key_id = SHA-256(utf8(name) || 0x0A || algo_byte || pubkey)[:4]
|
|
90
|
+
*
|
|
91
|
+
* The leading newline byte is U+000A (0x0A); `algo_byte` is the
|
|
92
|
+
* signature-type identifier from `c2sp.org/signed-note` §Signatures
|
|
93
|
+
* §Signature types. The key ID is intentionally short (4 bytes); it
|
|
94
|
+
* is an identifier, not a collision-resistant hash, and key ID
|
|
95
|
+
* collisions only produce verification failures, not forgeries (the
|
|
96
|
+
* verifier holds the authoritative public key).
|
|
97
|
+
*
|
|
98
|
+
* Acquires the sha2 module per call inside try / finally and disposes;
|
|
99
|
+
* does not hold long-lived state. The `name` argument must satisfy the
|
|
100
|
+
* signed-note key-name MUSTs (non-empty, no Unicode whitespace, no
|
|
101
|
+
* plus characters).
|
|
102
|
+
*/
|
|
103
|
+
export declare function deriveKeyId(name: string, algoByte: number, pubkey: Uint8Array): Uint8Array;
|
|
104
|
+
/**
|
|
105
|
+
* Decoded signed-note signature line per c2sp.org/signed-note §Format.
|
|
106
|
+
* `name` is the verified UTF-8 key name from the line; `keyId` is the
|
|
107
|
+
* 4-byte prefix extracted from the base64 payload; `signature` is the
|
|
108
|
+
* remaining bytes after the prefix, opaque to the parser (the format
|
|
109
|
+
* is defined by whatever algorithm corresponds to this key, which the
|
|
110
|
+
* parser does not look up).
|
|
111
|
+
*/
|
|
112
|
+
export interface SignatureLine {
|
|
113
|
+
readonly name: string;
|
|
114
|
+
readonly keyId: Uint8Array;
|
|
115
|
+
readonly signature: Uint8Array;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Decoded signed-note envelope. `body` includes the body's terminating
|
|
119
|
+
* U+000A but NOT the blank line that separates body from signatures;
|
|
120
|
+
* `signatures` contains every signature line that parsed structurally;
|
|
121
|
+
* `ignoredCount` is the number of signature lines that failed structural
|
|
122
|
+
* validation and were discarded per the signed-note §Signatures rule
|
|
123
|
+
* that unknown signatures MUST be ignored.
|
|
124
|
+
*/
|
|
125
|
+
export interface SignedNote {
|
|
126
|
+
readonly body: Uint8Array;
|
|
127
|
+
readonly signatures: SignatureLine[];
|
|
128
|
+
readonly ignoredCount: number;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Emit a signed-note envelope per c2sp.org/signed-note §Format. The
|
|
132
|
+
* caller supplies the body bytes (which MUST end in U+000A; the
|
|
133
|
+
* checkpoint body codec already enforces this) and one or more
|
|
134
|
+
* signature lines. The wire layout is:
|
|
135
|
+
*
|
|
136
|
+
* body || '\n' || (— name b64(keyId||sig) '\n')+
|
|
137
|
+
*
|
|
138
|
+
* The blank line that separates body from signature lines is the
|
|
139
|
+
* extra newline between the body's own trailing newline and the
|
|
140
|
+
* first signature line; both `serializeCheckpointBody` and this
|
|
141
|
+
* function MUST agree on this convention.
|
|
142
|
+
*
|
|
143
|
+
* Throws RangeError on a body that does not end in U+000A, on an
|
|
144
|
+
* empty signatures array, or on any signature whose key name violates
|
|
145
|
+
* the signed-note key-name MUSTs.
|
|
146
|
+
*/
|
|
147
|
+
export declare function emitSignedNote(body: Uint8Array, sigs: readonly SignatureLine[]): Uint8Array;
|
|
148
|
+
/**
|
|
149
|
+
* Parse a signed-note envelope per c2sp.org/signed-note §Format. The
|
|
150
|
+
* input must be valid UTF-8 and MUST NOT contain ASCII control
|
|
151
|
+
* characters below U+0020 other than newline. The body is everything
|
|
152
|
+
* up to and including the first blank line, MINUS the blank line
|
|
153
|
+
* itself, MINUS the newline that immediately precedes the blank line
|
|
154
|
+
* (no, including it; see body convention below).
|
|
155
|
+
*
|
|
156
|
+
* Per the body convention in `emitSignedNote`, the returned `body`
|
|
157
|
+
* field includes the body's terminating U+000A but excludes the
|
|
158
|
+
* blank-line separator.
|
|
159
|
+
*
|
|
160
|
+
* Signature-line parsing is permissive: a line that does not match
|
|
161
|
+
* `— <name> <base64>\n` exactly, or whose base64 payload decodes to
|
|
162
|
+
* fewer than 4 bytes (no room for a key ID), is counted in
|
|
163
|
+
* `ignoredCount` and discarded rather than throwing. The signed-note
|
|
164
|
+
* §Signatures rule is that unknown signatures MUST be ignored, and
|
|
165
|
+
* "unknown" subsumes any line a future spec extension might add in
|
|
166
|
+
* a format leviathan does not recognize.
|
|
167
|
+
*
|
|
168
|
+
* Whole-envelope structural errors (missing blank separator, body
|
|
169
|
+
* not ending in newline, ASCII control bytes, invalid UTF-8) throw
|
|
170
|
+
* RangeError. The behaviour of "throw on envelope, ignore on line"
|
|
171
|
+
* is what makes the codec forward-compatible with future cosignature
|
|
172
|
+
* algorithms without changing the byte-stable body region.
|
|
173
|
+
*/
|
|
174
|
+
export declare function parseSignedNote(bytes: Uint8Array): SignedNote;
|
|
175
|
+
/**
|
|
176
|
+
* Build the bytes a cosigner signs when issuing a cosignature for a
|
|
177
|
+
* checkpoint, per c2sp.org/tlog-cosignature §"Ed25519 signed message".
|
|
178
|
+
*
|
|
179
|
+
* Layout (each `\n` is U+000A):
|
|
180
|
+
*
|
|
181
|
+
* cosignature/v1\n
|
|
182
|
+
* time <decimal_timestamp>\n
|
|
183
|
+
* <body>
|
|
184
|
+
*
|
|
185
|
+
* `body` is the canonical checkpoint body produced by
|
|
186
|
+
* `serializeCheckpointBody` and already terminates in `\n`; the
|
|
187
|
+
* function adds no separator between the timestamp line and the
|
|
188
|
+
* body. Decimal carries no leading zeroes per the §Format rule on
|
|
189
|
+
* the timestamp line (mirrored from checkpoint §Note text).
|
|
190
|
+
*
|
|
191
|
+
* Spec-correct only for Ed25519 cosignatures (C2SP algo byte 0x04).
|
|
192
|
+
* ML-DSA-44 cosignatures sign the separate `cosigned_message` struct
|
|
193
|
+
* defined in §"ML-DSA-44 signed message" (codec not in this patch);
|
|
194
|
+
* callers reaching for this function with an ML-DSA-44 suite are
|
|
195
|
+
* producing the wrong wire format and should branch on the
|
|
196
|
+
* `messageConstruction` field of the suite's `AlgoEntry`.
|
|
197
|
+
*
|
|
198
|
+
* Throws `MerkleCodecError('timestamp-out-of-range')` if `timestamp`
|
|
199
|
+
* is not a non-negative safe integer.
|
|
200
|
+
*/
|
|
201
|
+
export declare function buildCosigSignedMessage(body: Uint8Array, timestamp: number): Uint8Array;
|
|
202
|
+
/**
|
|
203
|
+
* Encode the `timestamped_signature` struct payload per
|
|
204
|
+
* c2sp.org/tlog-cosignature §Format. Layout (per RFC 8446 §3.3,
|
|
205
|
+
* Presentation Language; integers in network byte order):
|
|
206
|
+
*
|
|
207
|
+
* u64_be(timestamp) || signature[N]
|
|
208
|
+
*
|
|
209
|
+
* The result is the opaque payload portion of a signed-note signature
|
|
210
|
+
* line: prefixed by the 4-byte key ID and then base64-encoded by
|
|
211
|
+
* `emitSignedNote`. `signature` length is suite-dependent (64 for
|
|
212
|
+
* Ed25519, 2420 for ML-DSA-44); the encoder does not validate length
|
|
213
|
+
* here because both registry-allowed sizes round-trip correctly.
|
|
214
|
+
*
|
|
215
|
+
* Throws `MerkleCodecError('timestamp-out-of-range')` if `timestamp`
|
|
216
|
+
* is not a non-negative safe integer.
|
|
217
|
+
*/
|
|
218
|
+
export declare function emitCosigSignaturePayload(timestamp: number, signature: Uint8Array): Uint8Array;
|
|
219
|
+
/**
|
|
220
|
+
* Decode a `timestamped_signature` payload per c2sp.org/tlog-cosignature
|
|
221
|
+
* §Format. Inverse of `emitCosigSignaturePayload`; round-trips
|
|
222
|
+
* byte-for-byte.
|
|
223
|
+
*
|
|
224
|
+
* `sigSize` is suite-locked (64 for Ed25519, 2420 for ML-DSA-44); the
|
|
225
|
+
* caller supplies it via the suite's `AlgoEntry.sigSize`. The decoder
|
|
226
|
+
* asserts `payload.length === 8 + sigSize` and throws
|
|
227
|
+
* `MerkleCodecError('cosig-payload-length-mismatch')` otherwise so a
|
|
228
|
+
* wrong-length payload fails loudly rather than producing a silently
|
|
229
|
+
* truncated signature.
|
|
230
|
+
*
|
|
231
|
+
* The wire timestamp is u64-BE; values exceeding `Number.MAX_SAFE_INTEGER`
|
|
232
|
+
* cannot round-trip through JavaScript Number and throw
|
|
233
|
+
* `MerkleCodecError('timestamp-exceeds-safe-integer')`. The cutoff is
|
|
234
|
+
* `tsHi >= 0x200000` (i.e. `2^53 / 2^32`).
|
|
235
|
+
*/
|
|
236
|
+
export declare function parseCosigSignaturePayload(payload: Uint8Array, sigSize: number): {
|
|
237
|
+
timestamp: number;
|
|
238
|
+
signature: Uint8Array;
|
|
239
|
+
};
|
|
240
|
+
/**
|
|
241
|
+
* Inputs to `buildCosignedMessage`, one named field per `cosigned_message`
|
|
242
|
+
* struct member from c2sp.org/tlog-cosignature §"ML-DSA-44 signed
|
|
243
|
+
* message". `start` and `end` are numbers in [0, Number.MAX_SAFE_INTEGER];
|
|
244
|
+
* they encode on the wire as big-endian u64. `hash` is exactly 32 bytes.
|
|
245
|
+
*/
|
|
246
|
+
export interface CosignedMessageInput {
|
|
247
|
+
/**
|
|
248
|
+
* UTF-8 cosigner identity, 1-255 bytes after encoding. For a log's
|
|
249
|
+
* cosignature on its own checkpoint this matches `logOrigin`; for a
|
|
250
|
+
* witness cosignature it identifies the witness.
|
|
251
|
+
*/
|
|
252
|
+
readonly cosignerName: string;
|
|
253
|
+
/**
|
|
254
|
+
* POSIX-seconds timestamp. Per c2sp.org/tlog-cosignature §"ML-DSA-44
|
|
255
|
+
* signed message", `timestamp` MUST be zero when `start` is not
|
|
256
|
+
* zero (subtree case); both MAY be zero (the cosigner is making no
|
|
257
|
+
* statement about being the largest observed tree).
|
|
258
|
+
*/
|
|
259
|
+
readonly timestamp: number;
|
|
260
|
+
/**
|
|
261
|
+
* UTF-8 log identity, 1-255 bytes after encoding. Matches the
|
|
262
|
+
* checkpoint body's origin line (without the trailing newline).
|
|
263
|
+
*/
|
|
264
|
+
readonly logOrigin: string;
|
|
265
|
+
/**
|
|
266
|
+
* Index of the first leaf included in the signed range. MUST be 0
|
|
267
|
+
* for a checkpoint cosignature; non-zero only for subtree
|
|
268
|
+
* cosignatures (out of Phase 7 scope but supported by the codec).
|
|
269
|
+
*/
|
|
270
|
+
readonly start: number;
|
|
271
|
+
/**
|
|
272
|
+
* Exclusive upper bound of the leaf indexes in the signed range.
|
|
273
|
+
* For a checkpoint cosignature, equals the tree size.
|
|
274
|
+
*/
|
|
275
|
+
readonly end: number;
|
|
276
|
+
/** 32-byte Merkle root hash of the signed range. */
|
|
277
|
+
readonly hash: Uint8Array;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Build the bytes a cosigner signs when issuing an ML-DSA-44
|
|
281
|
+
* cosignature, per c2sp.org/tlog-cosignature §"ML-DSA-44 signed
|
|
282
|
+
* message". Layout (TLS-Presentation per RFC 8446 §3.3, lengths in
|
|
283
|
+
* big-endian network order):
|
|
284
|
+
*
|
|
285
|
+
* uint8 label[12] = "subtree/v1\n\0"
|
|
286
|
+
* opaque cosigner_name<1..2^8-1>
|
|
287
|
+
* uint64 timestamp
|
|
288
|
+
* opaque log_origin<1..2^8-1>
|
|
289
|
+
* uint64 start
|
|
290
|
+
* uint64 end
|
|
291
|
+
* uint8 hash[32]
|
|
292
|
+
*
|
|
293
|
+
* Total length is `70 + utf8(cosignerName).length + utf8(logOrigin).length`.
|
|
294
|
+
*
|
|
295
|
+
* Spec-correct for both checkpoint (start=0) and subtree (start>0)
|
|
296
|
+
* ML-DSA-44 cosignatures. Phase 7 uses only the checkpoint case;
|
|
297
|
+
* subtree cosignatures land with the witness-protocol work. The
|
|
298
|
+
* codec is agnostic so future TASKs do not re-cut the surface.
|
|
299
|
+
*
|
|
300
|
+
* Throws `MerkleCodecError`:
|
|
301
|
+
* 'timestamp-out-of-range' timestamp / start / end not safe non-negative
|
|
302
|
+
* 'cosigner-name-length' UTF-8 cosignerName empty or > 255 bytes
|
|
303
|
+
* 'log-origin-length' UTF-8 logOrigin empty or > 255 bytes
|
|
304
|
+
* 'cosigned-message-state' start > 0 and timestamp != 0 (spec MUST)
|
|
305
|
+
*
|
|
306
|
+
* Throws `RangeError` on a `hash` whose length is not 32 (the
|
|
307
|
+
* `cosigned_message.hash` field is fixed-length per the struct).
|
|
308
|
+
*/
|
|
309
|
+
export declare function buildCosignedMessage(input: CosignedMessageInput): Uint8Array;
|