Files
skills/docs/security-stack/crypto/crypto-keys/src/codecs/recursive-ed25519/recursive-ed25519.md
2026-05-29 08:20:56 +01:00

4.8 KiB

RecursiveEd25519Codec

(no submodules)

Purpose

RecursiveEd25519Codec is a fixed-Ed25519 alternate wire format for signed key chains in @suaveplan/crypto-keys. It trades the algorithm-flexibility of TlvChainCodec for a denser, predictable byte layout suitable for embedded systems, replicated logs, and any context where chains are known to be purely Ed25519. The codec walks bytes linearly leaf-first using the fixed 32-byte public key and 64-byte signature lengths defined by Ed25519, with a single u16 length prefix for each variable-length payload.

Features

  • Fixed 32-byte Ed25519 public keys and 64-byte Ed25519 signatures — no per-level algorithm byte.
  • Compact 2-byte header carrying version=1 plus the active hasherName byte (raw/domain/merkle).
  • u16 big-endian payload length (up to 65,535 bytes per level).
  • Leaf-first emission; the parent chain is appended back-to-back with no inter-level separator.
  • Strict validation: bad version byte, unknown hasher byte, truncated buffers, and out-of-bound payload lengths all raise CHAIN_KEYS.INVALID_FORMAT.
  • Non-Ed25519 algorithms raise CHAIN_KEYS.UNSUPPORTED_ALGORITHM with the algorithm discriminant populated, mirroring the signWithSlot failure surface.
  • After decode(...), hasherName is set from the wire byte so a downstream ChainedKeySigner can reconfigure its hasher without an out-of-band channel.

Usage: Basic

import { RecursiveEd25519Codec } from "@suaveplan/crypto-keys/codecs/recursive-ed25519/recursive-ed25519";

const codec = new RecursiveEd25519Codec();
const level = {
    algorithm: "ed25519" as const,
    signerPublicKey: new Uint8Array(32),
    payload: new Uint8Array([1, 2, 3, 4]),
    signature: new Uint8Array(64),
};

const bytes = codec.encode([level]);
const decoded = codec.decode(bytes);
// decoded[0].algorithm === "ed25519"

Usage: Advanced

Round-tripping a multi-level chain and surfacing hasher state:

import { RecursiveEd25519Codec } from "@suaveplan/crypto-keys/codecs/recursive-ed25519/recursive-ed25519";
import { ChainKeysError } from "@suaveplan/crypto-keys/error-codes/error-codes";

const writer = new RecursiveEd25519Codec();
writer.hasherName = "merkle";
const wire = writer.encode([leafLevel, midLevel, rootLevel]);

const reader = new RecursiveEd25519Codec();
try {
    const levels = reader.decode(wire);
    // reader.hasherName is now "merkle" — propagate to the ChainedKeySigner
    // before verifying signatures.
    handleChain(levels, reader.hasherName);
} catch (err) {
    if (err instanceof ChainKeysError && err.code === "CHAIN_KEYS.UNSUPPORTED_ALGORITHM") {
        // Routing decision: switch to TlvChainCodec or CborChainCodec.
        fallback(err.algorithm);
    } else {
        throw err;
    }
}

API Reference

class RecursiveEd25519Codec implements ChainCodec

  • readonly name: "recursive-ed25519" — codec identifier surfaced in diagnostics.
  • hasherName: "raw" | "domain" | "merkle" — hasher byte written on encode and overwritten on decode. Default "raw".
  • encode(levels: readonly ChainLevel[]): Uint8Array — emits [version=1, hasher, (pub32, payload_len_u16_be, payload, sig64)+] leaf-first. Throws ChainKeysError with code CHAIN_KEYS.UNSUPPORTED_ALGORITHM for any non-Ed25519 level (with algorithm discriminant set), or CHAIN_KEYS.INVALID_FORMAT for wrong public-key/signature sizes or oversized payloads.
  • decode(bytes: Uint8Array): readonly ChainLevel[] — walks the buffer from offset 2 until end-of-buffer. Every decoded level carries algorithm: "ed25519" unconditionally. Throws CHAIN_KEYS.INVALID_FORMAT on a wrong version byte, unknown hasher byte, truncated read, or an in-stream payload_len exceeding the remaining bytes.

Implementation Notes

  • The encoder pre-computes total length and writes into a single Uint8Array, avoiding per-level allocations and intermediate concatenations.
  • payload_len uses u16 big-endian via DataView.setUint16(offset, len, false) — keeping a network-byte-order convention consistent with TLV.
  • decode(...) calls bytes.slice(...) for each field so the returned Uint8Arrays do not alias the input buffer; consumers may safely mutate or zero the wire bytes after decoding.
  • The hasher byte is validated as < HASHER_FROM_BYTE.length rather than a hard-coded <= 2 so adding a fourth hasher only requires extending the lookup table.
  • Encoding a non-Ed25519 level raises UNSUPPORTED_ALGORITHM even when the supplied key/signature happen to be 32/64 bytes — the algorithm check fires first to keep the error surface predictable.
  • Decoded levels are returned readonly at the type level but the underlying Uint8Arrays are fresh copies; callers can freeze them at a higher layer if mutation must be statically forbidden.