# signer _(no submodules)_ ## Purpose `ChainedKeySigner` composes a `ChainHasher` (bytes-to-sign strategy) and a `ChainCodec` (wire-format strategy) to produce and verify signed chains of arbitrary payloads. It dispatches the actual cryptographic work to `@suaveplan/crypto-curve` primitives per the level's algorithm: Ed25519, secp256k1, P-256, P-384, or P-521. Two ergonomic signing surfaces are exposed: `signWithSlot` is a convenience layer over the `@suaveplan/crypto` keychain (Ed25519-only in v0.0.0, extending automatically when the v2 keychain adds the other curves), and `signRaw` is the fully general primitive that takes raw key material plus an algorithm tag. The signer is the package's main entry point. Most consumers construct a `ChainedKeySigner()` with default options (`RawHasher` + `TlvChainCodec`), then call `signWithSlot` or `signRaw` to build chains and `verify` to check them. ## Features - Two signing surfaces: `signWithSlot` (Ed25519-only, slot-based) and `signRaw` (5-algorithm, raw-key) - `verify` returns `{ valid, depth, levels, errors }` — never throws on bad input - `parse` decode-only when verification isn't needed - `stripToDepth` produces a partial-proof chain bounded by depth - Per-algorithm digest dispatch: Ed25519 raw, secp256k1/P-256 with sha256, P-384 with sha384, P-521 with sha512 - Public-key validation: if a `publicKey` is supplied to `signRaw`, the signer derives it from `privateKey` and checks they match - Constant-time root-key compare via `@suaveplan/crypto-utils::constantTimeEqual` - All crypto-curve exceptions wrapped as `ChainKeysError` with `algorithm` discriminant + preserved `cause` ## Usage: Basic ```ts import { ChainedKeySigner, type ChainLevel, } from "@suaveplan/crypto-keys"; import * as curve from "@suaveplan/crypto-curve"; const signer = new ChainedKeySigner(); const privateKey = curve.ed25519.randomPrivateKey(); const publicKey = curve.ed25519.getPublicKey(privateKey); const chain = await signer.signRaw({ payload: new TextEncoder().encode("hello"), algorithm: "ed25519", privateKey, }); const result = await signer.verify(chain, { algorithm: "ed25519", publicKey, }); console.log(result.valid); // true ``` ## Usage: Advanced ```ts import { ChainedKeySigner, DomainHasher, CborChainCodec, } from "@suaveplan/crypto-keys"; import * as curve from "@suaveplan/crypto-curve"; const signer = new ChainedKeySigner({ hasher: new DomainHasher({ domainTag: "acme/chain/v1" }), codec: new CborChainCodec(), }); const root = { privateKey: curve.secp256k1.randomPrivateKey(), publicKey: curve.secp256k1.getPublicKey(curve.secp256k1.randomPrivateKey()), }; const intermediate = { privateKey: curve.p256.randomPrivateKey(), publicKey: curve.p256.getPublicKey(curve.p256.randomPrivateKey()), }; // Root self-signs (secp256k1) const rootChain = await signer.signRaw({ payload: root.publicKey, algorithm: "secp256k1", privateKey: root.privateKey, }); // Intermediate (P-256) signed by root const interChain = await signer.signRaw({ payload: intermediate.publicKey, algorithm: "p256", privateKey: root.privateKey, parent: rootChain, }); // Strip to depth 1 for distribution const leafOnly = await signer.stripToDepth(interChain, 1); // Recipient verifies (must reproduce same hasher + codec) const verifier = new ChainedKeySigner({ hasher: new DomainHasher({ domainTag: "acme/chain/v1" }), codec: new CborChainCodec(), }); const result = await verifier.verify(interChain, { algorithm: "secp256k1", publicKey: root.publicKey, }); if (!result.valid) { throw new Error(`chain invalid: ${result.errors.join("; ")}`); } ``` ## API Reference ### Class `ChainedKeySigner` | Method | Signature | | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `constructor` | `(options?: { hasher?: ChainHasher; codec?: ChainCodec })` | | `signWithSlot` | `(payload: Uint8Array, signSlot: { algorithm: number; secretKey: Uint8Array; publicKey: Uint8Array }, parent?: Uint8Array) => Promise` | | `signRaw` | `(options: SignRawOptions) => Promise` | | `verify` | `(chainBytes: Uint8Array, trustedRoot: { algorithm: SignAlgorithmId; publicKey: Uint8Array }) => Promise` | | `parse` | `(chainBytes: Uint8Array) => Promise<{ depth: number; levels: readonly ChainLevel[] }>` | | `stripToDepth` | `(chainBytes: Uint8Array, depth: number) => Promise` | ### `SignRawOptions` | Field | Type | Required | Meaning | | ------------ | ------------------- | -------- | ---------------------------------------------------------------- | | `payload` | `Uint8Array` | yes | Bytes to sign — opaque to the package | | `algorithm` | `SignAlgorithmId` | yes | Which signing curve | | `privateKey` | `Uint8Array` | yes | Private key bytes for the chosen algorithm | | `publicKey` | `Uint8Array` | no | If supplied, validated against derived; if omitted, derived | | `parent` | `Uint8Array` | no | Existing parent chain bytes to extend | ### Error contract - `signWithSlot` throws `ChainKeysError(UNSUPPORTED_ALGORITHM)` for non-Ed25519 slots in v0.0.0 - `signWithSlot` / `signRaw` throw `ChainKeysError(INVALID_KEYCHAIN)` for missing/empty private key or pub/priv mismatch - `signRaw` throws `ChainKeysError(UNSUPPORTED_ALGORITHM)` for unknown algorithm strings - Any `crypto-curve` exception is wrapped as `ChainKeysError(INVALID_KEYCHAIN)` with `cause` preserved - `verify` never throws — returns `{ valid: false, errors }` on every failure mode - `parse` throws `ChainKeysError(INVALID_FORMAT)` if codec decoding fails - `stripToDepth` throws `ChainKeysError(INVALID_DEPTH)` for `depth < 1` or `depth > chain.length` ## Implementation Notes The signer keeps a private reference to the configured `ChainHasher` and `ChainCodec`. On construction, it sets `codec.hasherName` to the hasher's `name` so the wire format records which mode is in use; on `verify`, the signer reads `codec.hasherName` after decode to detect hasher mismatches between sender and receiver. Mismatch produces `{ valid: false, errors: ["hasher mismatch: ..."] }` rather than silently verifying with the wrong hash. Per-curve digest pre-hashing is intentional. Ed25519 hashes its input internally (RFC 8032 PureEdDSA), so the signer passes the hasher output straight through. The P-curves and secp256k1 take a fixed-size digest; the package pairs them with the standard hash (`sha256` for secp256k1/P-256, `sha384` for P-384, `sha512` for P-521) per NIST SP 800-186. `signRaw`'s `publicKey` field is optional but recommended. When supplied, the signer derives the expected public key from `privateKey` and rejects with `INVALID_KEYCHAIN` if they differ. This catches caller bugs (mismatched key material) at sign time rather than at downstream verify time. When omitted, the signer derives the public key itself — one extra `getPublicKey` call per sign, which is fast on every supported curve. `verify` walks levels leaf-to-root. For each level it recomputes `hasher.hash(payload, parentLevel)` — passing the codec-parsed parent level if any — then digests via the curve's standard hash and calls `crypto-curve.verify(...)`. The deepest level's `(algorithm, signerPublicKey)` is compared against `trustedRoot` with algorithm equality plus constant-time byte equality on the public key. `stripToDepth(chain, n)` returns the leaf-most `n` levels re-encoded via the configured codec. Useful for distributing partial proofs — e.g. a leaf-only chain whose verifier trusts the immediate parent's public key directly rather than walking back to the root.