first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,147 @@
# factory
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Hierarchical-keychain factory helpers that compose `generateKeychain`
from `@suaveplan/crypto` with `ChainedKeySigner.signWithSlot` to
produce signed chains in one call. Three functions cover the common
pattern: bootstrap a root keychain, derive a server keychain bound to
the root, and roll a server keychain forward to the next version
without changing the root.
These functions are Ed25519-only in v0.0.0 because the crypto
keychain v1 grammar only generates Ed25519 sign-slots. Callers that
need a different curve at any chain level should reach for
`ChainedKeySigner.signRaw` directly instead.
## Features
- `createRootKeysWithChain` — fresh keychain + self-signed chain
- `createServerKeysWithChain` — derived keychain + chain bound to root
- `rollServerKeysWithChain` — re-derive at next slot version, re-sign
- All three accept an injected `signer?` for custom hasher/codec choices
- Strict `INVALID_KEYCHAIN` errors when root slots are missing or empty
- Sensible `LayoutV1Fields` defaults (`masterVersion: 1, userId: 0, depth: 0, slotVersion: 1`)
- Default `Clock` is `Date.now` — caller can override via `generate.clock`
## Usage: Basic
```ts
import {
ChainedKeySigner,
createRootKeysWithChain,
createServerKeysWithChain,
} from "@suaveplan/crypto-keys";
// Bootstrap a root keychain + self-signed chain
const { keys: rootKeys, chain: rootChain } = await createRootKeysWithChain();
// Derive a server keychain whose chain anchors to the root
const { keys: serverKeys, chain: serverChain } = await createServerKeysWithChain({
rootKeys,
rootChain,
});
// Verify end-to-end
const verifier = new ChainedKeySigner();
const result = await verifier.verify(serverChain, {
algorithm: "ed25519",
publicKey: (rootKeys as Record<string, { publicKey: Uint8Array }>).sign.publicKey,
});
console.log(result.valid, result.depth); // true 2
```
## Usage: Advanced
```ts
import {
ChainedKeySigner,
DomainHasher,
createRootKeysWithChain,
createServerKeysWithChain,
rollServerKeysWithChain,
} from "@suaveplan/crypto-keys";
const signer = new ChainedKeySigner({
hasher: new DomainHasher({ domainTag: "tenant.acme.v1" }),
});
// Tagged root
const { keys: rootKeys, chain: rootChain } = await createRootKeysWithChain({
signer,
generate: { userId: 42 as never }, // custom layout fields per app
});
// First server keychain (slot version 1)
const v1 = await createServerKeysWithChain({ rootKeys, rootChain, signer });
// Six months later: roll to slot version 2, same root binding
const v2 = await rollServerKeysWithChain({
rootKeys,
rootChain,
currentVersion: 1,
signer,
});
// Issuance chain: root -> v1 -> v2 -> leaf user
const leaf = await createServerKeysWithChain({
rootKeys,
rootChain: v2.chain,
signer,
});
const verifier = new ChainedKeySigner({
hasher: new DomainHasher({ domainTag: "tenant.acme.v1" }),
});
const result = await verifier.verify(leaf.chain, {
algorithm: "ed25519",
publicKey: (rootKeys as Record<string, { publicKey: Uint8Array }>).sign.publicKey,
});
```
## API Reference
### `async function createRootKeysWithChain(options?)`
| Field | Type | Default |
| ----------------- | ------------------------------------- | ----------------------- |
| `signer?` | `ChainedKeySigner` | `new ChainedKeySigner()` |
| `generate?` | `Partial<GenerateKeychainOptions>` | sensible defaults |
Returns `{ keys: Keychain<LayoutV1Fields>; chain: Uint8Array }`.
### `async function createServerKeysWithChain(options)`
| Field | Type | Required |
| ------------- | ------------------------------------- | -------- |
| `rootKeys` | `Keychain<LayoutV1Fields>` | yes |
| `rootChain` | `Uint8Array` | yes |
| `signer?` | `ChainedKeySigner` | no |
| `generate?` | `Partial<GenerateKeychainOptions>` | no |
Raises `CHAIN_KEYS.INVALID_KEYCHAIN` if `rootKeys.master` or `rootKeys.sign` is missing, has an empty `secretKey`, or has a non-Ed25519 `algorithm`.
### `async function rollServerKeysWithChain(options)`
| Field | Type | Required |
| ----------------- | -------------------------- | -------- |
| `rootKeys` | `Keychain<LayoutV1Fields>` | yes |
| `rootChain` | `Uint8Array` | yes |
| `currentVersion` | `number` (≥ 0, integer) | yes |
| `signer?` | `ChainedKeySigner` | no |
Generates a new keychain with `slotVersion: currentVersion + 1` and re-signs against the same root. Raises `CHAIN_KEYS.INVALID_KEYCHAIN` when `currentVersion` is negative or non-integer, or when the root slots are missing/invalid.
## Implementation Notes
crypto's `Keychain` is `Record<PurposeTag, KeySlotPrivate>` where `PurposeTag` is a branded string type. The factory module uses a small `getSlot(keys, tag)` helper that casts through `Record<string, KeySlotPrivate>` to access individual slots; this keeps the TypeScript narrowing clean without pulling in the `PurposeTag` brand machinery.
`LayoutV1Fields` is re-declared locally because it's not currently re-exported from `@suaveplan/crypto/keychain`. The shape is identical to the upstream definition and `generateKeychain` accepts any object satisfying it structurally. If a future crypto release adds a `./derivation` subpath export, this declaration can be replaced with the canonical import.
The factory functions trust `generateKeychain`'s contract — that every generated keychain contains all 8 default-purpose slots with valid Ed25519 sign-slots — and do not re-validate the freshly-generated output. Validation is concentrated at the boundaries: `signWithSlot` rejects non-Ed25519 slots, and `assertRootSlots` checks every caller-supplied root before use. This keeps the happy path lean and pushes the cost of bad input onto the caller's stack frame.
`signWithSlot` takes an optional `parent: Uint8Array`; the factory functions pass `rootChain` when extending and omit it for the self-signed root. `ChainedKeySigner` distinguishes a root signature (no parent) from a non-root signature (parent supplied) by the presence of the argument — there's no separate "root" API.