/** * c2sp.org/signed-note §Format signature type for plain Ed25519 * signatures over the raw note text per RFC 8032. Listed for spec * completeness; no leviathan SignatureSuite currently routes here * because Phase 7 cosignatures use the timestamped Ed25519 variant * (`ALGO_BYTE_ED25519_COSIG`) per c2sp.org/tlog-cosignature §Format. */ export declare const ALGO_BYTE_ED25519_NOTE = 1; /** * c2sp.org/tlog-cosignature §Format signature type for timestamped * Ed25519 checkpoint cosignatures. The signature payload is * `u64_be(timestamp) || ed25519_signature(64)` for a total of 72 * bytes, base64-encoded together with the 4-byte key ID on the * signature line. */ export declare const ALGO_BYTE_ED25519_COSIG = 4; /** * c2sp.org/tlog-cosignature §Format signature type for timestamped * ML-DSA-44 (sub)tree cosignatures. The signature payload is * `u64_be(timestamp) || ml_dsa_44_signature(2420)` for a total of * 2428 bytes, base64-encoded together with the 4-byte key ID. */ export declare const ALGO_BYTE_MLDSA44_COSIG = 6; /** * How the cosigner constructs the bytes it signs. * * 'cosig' c2sp.org/tlog-cosignature §"Ed25519 signed * message". Produced by `buildCosigSignedMessage`. * 'cosigned-message' c2sp.org/tlog-cosignature §"ML-DSA-44 signed * message", `cosigned_message` struct. */ export type MessageConstruction = 'cosig' | 'cosigned-message'; /** * Per-signature payload encoding bundled with the 4-byte key ID. * * 'timestamped' c2sp.org/tlog-cosignature §Format * `timestamped_signature` struct, shared by 0x04 and 0x06. */ export type SignaturePayload = 'timestamped'; /** * Per c2sp.org/tlog-cosignature §Format algorithm-byte registry. One * entry per registered (leviathan suite, C2SP byte) pair; the entry * carries the message-construction and payload-encoding rules a * cosigner needs to sign and a verifier needs to parse. */ export interface AlgoEntry { /** Leviathan `SignatureSuite.formatEnum`. */ readonly formatEnum: number; /** C2SP signed-note algorithm byte from §Format. */ readonly algoByte: number; /** How the cosigner constructs the bytes it signs. */ readonly messageConstruction: MessageConstruction; /** How the per-signature payload is encoded on the signature line. */ readonly signaturePayload: SignaturePayload; /** * Raw signature size in bytes from the underlying primitive. For * Ed25519 this is 64 (RFC 8032 §5.1.6); for ML-DSA-44 this is * 2420 (FIPS 204 Table 1). The `timestamped` payload encoding * adds an 8-byte BE timestamp prefix per `timestamped_signature`, * so the total payload length on the wire is `8 + sigSize`. */ readonly sigSize: number; } /** * Look up the algo-entry for a leviathan `SignatureSuite.formatEnum`. * Returns `undefined` for suites not registered in the catalog; * callers that need a hard guarantee should check the return value * and raise an issue per AGENTS.md rather than locally mint a byte * for a suite the C2SP spec has not registered. */ export declare function lookupAlgoEntryByFormatEnum(formatEnum: number): AlgoEntry | undefined; /** * Look up the algo-entry for a wire-format C2SP algorithm byte. Used * by verifiers that see an unknown signature line and need to decide * how to reshape the payload (or whether to defer to * `parseSignedNote`'s "unknown signatures MUST be ignored" rule). */ export declare function lookupAlgoEntryByByte(algoByte: number): AlgoEntry | undefined; /** * Resolve a leviathan SignatureSuite formatEnum to its C2SP signed-note * algorithm byte. Thin shim over `lookupAlgoEntryByFormatEnum`; kept for * the call sites that only need the byte (e.g. `deriveKeyId` callers). */ export declare function suiteFormatEnumToAlgoByte(formatEnum: number): number | undefined; /** * Per c2sp.org/signed-note §Signatures and c2sp.org/tlog-cosignature * §Format, the recommended key ID is: * * key_id = SHA-256(utf8(name) || 0x0A || algo_byte || pubkey)[:4] * * The leading newline byte is U+000A (0x0A); `algo_byte` is the * signature-type identifier from `c2sp.org/signed-note` §Signatures * §Signature types. The key ID is intentionally short (4 bytes); it * is an identifier, not a collision-resistant hash, and key ID * collisions only produce verification failures, not forgeries (the * verifier holds the authoritative public key). * * Acquires the sha2 module per call inside try / finally and disposes; * does not hold long-lived state. The `name` argument must satisfy the * signed-note key-name MUSTs (non-empty, no Unicode whitespace, no * plus characters). */ export declare function deriveKeyId(name: string, algoByte: number, pubkey: Uint8Array): Uint8Array; /** * Decoded signed-note signature line per c2sp.org/signed-note §Format. * `name` is the verified UTF-8 key name from the line; `keyId` is the * 4-byte prefix extracted from the base64 payload; `signature` is the * remaining bytes after the prefix, opaque to the parser (the format * is defined by whatever algorithm corresponds to this key, which the * parser does not look up). */ export interface SignatureLine { readonly name: string; readonly keyId: Uint8Array; readonly signature: Uint8Array; } /** * Decoded signed-note envelope. `body` includes the body's terminating * U+000A but NOT the blank line that separates body from signatures; * `signatures` contains every signature line that parsed structurally; * `ignoredCount` is the number of signature lines that failed structural * validation and were discarded per the signed-note §Signatures rule * that unknown signatures MUST be ignored. */ export interface SignedNote { readonly body: Uint8Array; readonly signatures: SignatureLine[]; readonly ignoredCount: number; } /** * Emit a signed-note envelope per c2sp.org/signed-note §Format. The * caller supplies the body bytes (which MUST end in U+000A; the * checkpoint body codec already enforces this) and one or more * signature lines. The wire layout is: * * body || '\n' || (— name b64(keyId||sig) '\n')+ * * The blank line that separates body from signature lines is the * extra newline between the body's own trailing newline and the * first signature line; both `serializeCheckpointBody` and this * function MUST agree on this convention. * * Throws RangeError on a body that does not end in U+000A, on an * empty signatures array, or on any signature whose key name violates * the signed-note key-name MUSTs. */ export declare function emitSignedNote(body: Uint8Array, sigs: readonly SignatureLine[]): Uint8Array; /** * Parse a signed-note envelope per c2sp.org/signed-note §Format. The * input must be valid UTF-8 and MUST NOT contain ASCII control * characters below U+0020 other than newline. The body is everything * up to and including the first blank line, MINUS the blank line * itself, MINUS the newline that immediately precedes the blank line * (no, including it; see body convention below). * * Per the body convention in `emitSignedNote`, the returned `body` * field includes the body's terminating U+000A but excludes the * blank-line separator. * * Signature-line parsing is permissive: a line that does not match * `— \n` exactly, or whose base64 payload decodes to * fewer than 4 bytes (no room for a key ID), is counted in * `ignoredCount` and discarded rather than throwing. The signed-note * §Signatures rule is that unknown signatures MUST be ignored, and * "unknown" subsumes any line a future spec extension might add in * a format leviathan does not recognize. * * Whole-envelope structural errors (missing blank separator, body * not ending in newline, ASCII control bytes, invalid UTF-8) throw * RangeError. The behaviour of "throw on envelope, ignore on line" * is what makes the codec forward-compatible with future cosignature * algorithms without changing the byte-stable body region. */ export declare function parseSignedNote(bytes: Uint8Array): SignedNote; /** * Build the bytes a cosigner signs when issuing a cosignature for a * checkpoint, per c2sp.org/tlog-cosignature §"Ed25519 signed message". * * Layout (each `\n` is U+000A): * * cosignature/v1\n * time \n * * * `body` is the canonical checkpoint body produced by * `serializeCheckpointBody` and already terminates in `\n`; the * function adds no separator between the timestamp line and the * body. Decimal carries no leading zeroes per the §Format rule on * the timestamp line (mirrored from checkpoint §Note text). * * Spec-correct only for Ed25519 cosignatures (C2SP algo byte 0x04). * ML-DSA-44 cosignatures sign the separate `cosigned_message` struct * defined in §"ML-DSA-44 signed message" (codec not in this patch); * callers reaching for this function with an ML-DSA-44 suite are * producing the wrong wire format and should branch on the * `messageConstruction` field of the suite's `AlgoEntry`. * * Throws `MerkleCodecError('timestamp-out-of-range')` if `timestamp` * is not a non-negative safe integer. */ export declare function buildCosigSignedMessage(body: Uint8Array, timestamp: number): Uint8Array; /** * Encode the `timestamped_signature` struct payload per * c2sp.org/tlog-cosignature §Format. Layout (per RFC 8446 §3.3, * Presentation Language; integers in network byte order): * * u64_be(timestamp) || signature[N] * * The result is the opaque payload portion of a signed-note signature * line: prefixed by the 4-byte key ID and then base64-encoded by * `emitSignedNote`. `signature` length is suite-dependent (64 for * Ed25519, 2420 for ML-DSA-44); the encoder does not validate length * here because both registry-allowed sizes round-trip correctly. * * Throws `MerkleCodecError('timestamp-out-of-range')` if `timestamp` * is not a non-negative safe integer. */ export declare function emitCosigSignaturePayload(timestamp: number, signature: Uint8Array): Uint8Array; /** * Decode a `timestamped_signature` payload per c2sp.org/tlog-cosignature * §Format. Inverse of `emitCosigSignaturePayload`; round-trips * byte-for-byte. * * `sigSize` is suite-locked (64 for Ed25519, 2420 for ML-DSA-44); the * caller supplies it via the suite's `AlgoEntry.sigSize`. The decoder * asserts `payload.length === 8 + sigSize` and throws * `MerkleCodecError('cosig-payload-length-mismatch')` otherwise so a * wrong-length payload fails loudly rather than producing a silently * truncated signature. * * The wire timestamp is u64-BE; values exceeding `Number.MAX_SAFE_INTEGER` * cannot round-trip through JavaScript Number and throw * `MerkleCodecError('timestamp-exceeds-safe-integer')`. The cutoff is * `tsHi >= 0x200000` (i.e. `2^53 / 2^32`). */ export declare function parseCosigSignaturePayload(payload: Uint8Array, sigSize: number): { timestamp: number; signature: Uint8Array; }; /** * Inputs to `buildCosignedMessage`, one named field per `cosigned_message` * struct member from c2sp.org/tlog-cosignature §"ML-DSA-44 signed * message". `start` and `end` are numbers in [0, Number.MAX_SAFE_INTEGER]; * they encode on the wire as big-endian u64. `hash` is exactly 32 bytes. */ export interface CosignedMessageInput { /** * UTF-8 cosigner identity, 1-255 bytes after encoding. For a log's * cosignature on its own checkpoint this matches `logOrigin`; for a * witness cosignature it identifies the witness. */ readonly cosignerName: string; /** * POSIX-seconds timestamp. Per c2sp.org/tlog-cosignature §"ML-DSA-44 * signed message", `timestamp` MUST be zero when `start` is not * zero (subtree case); both MAY be zero (the cosigner is making no * statement about being the largest observed tree). */ readonly timestamp: number; /** * UTF-8 log identity, 1-255 bytes after encoding. Matches the * checkpoint body's origin line (without the trailing newline). */ readonly logOrigin: string; /** * Index of the first leaf included in the signed range. MUST be 0 * for a checkpoint cosignature; non-zero only for subtree * cosignatures (out of Phase 7 scope but supported by the codec). */ readonly start: number; /** * Exclusive upper bound of the leaf indexes in the signed range. * For a checkpoint cosignature, equals the tree size. */ readonly end: number; /** 32-byte Merkle root hash of the signed range. */ readonly hash: Uint8Array; } /** * Build the bytes a cosigner signs when issuing an ML-DSA-44 * cosignature, per c2sp.org/tlog-cosignature §"ML-DSA-44 signed * message". Layout (TLS-Presentation per RFC 8446 §3.3, lengths in * big-endian network order): * * uint8 label[12] = "subtree/v1\n\0" * opaque cosigner_name<1..2^8-1> * uint64 timestamp * opaque log_origin<1..2^8-1> * uint64 start * uint64 end * uint8 hash[32] * * Total length is `70 + utf8(cosignerName).length + utf8(logOrigin).length`. * * Spec-correct for both checkpoint (start=0) and subtree (start>0) * ML-DSA-44 cosignatures. Phase 7 uses only the checkpoint case; * subtree cosignatures land with the witness-protocol work. The * codec is agnostic so future TASKs do not re-cut the surface. * * Throws `MerkleCodecError`: * 'timestamp-out-of-range' timestamp / start / end not safe non-negative * 'cosigner-name-length' UTF-8 cosignerName empty or > 255 bytes * 'log-origin-length' UTF-8 logOrigin empty or > 255 bytes * 'cosigned-message-state' start > 0 and timestamp != 0 (spec MUST) * * Throws `RangeError` on a `hash` whose length is not 32 (the * `cosigned_message.hash` field is fixed-length per the struct). */ export declare function buildCosignedMessage(input: CosignedMessageInput): Uint8Array;