Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
# SignedBitSchema
|
||||
|
||||
Bit-packed payload signed by an `Authority` under a fixed `DomainTag`. Wire
|
||||
format is `[bit-packed body][signature]`. Verifies before decoding so a
|
||||
tampered payload never even reaches the body codec. Replaces the ad-hoc
|
||||
`BitPacker.pack(...) → sign(...) → concat(...)` pattern that crypto
|
||||
chain bundles, public bundles, packer chunks, and game-server state-sync
|
||||
encoders had been re-authoring.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Every signed wire format in the stack follows the same recipe:
|
||||
bit-pack a structured body, sign the bytes with an authority slot, frame
|
||||
`[body][signature]`, then on the verify side strip the signature, verify it
|
||||
against a parent's public slot, and only then decode the body. Five plus
|
||||
suaveplan/upstream call-sites had been manually re-authoring this dance, each
|
||||
slightly differently. `SignedBitSchema` consolidates the pattern so the
|
||||
domain separation, signature length, sign-slot dispatch, and verify-then-decode
|
||||
order are all driven by a single primitive.
|
||||
|
||||
The domain `DomainTag` is required, not optional, so a signature produced
|
||||
under one domain (e.g. `DOMAIN_PUBLIC_KEYCHAIN_V1`) cannot be replayed
|
||||
under a different domain (e.g. `DOMAIN_AUDIT_V1`) — the tag bytes
|
||||
participate in the signing input via
|
||||
`@suaveplan/crypto::buildSigningInput`. Cross-protocol replay is rejected
|
||||
at the bytes-don't-match level rather than at any application-layer check.
|
||||
|
||||
## Features
|
||||
|
||||
- Pluggable body codec (`BodyCodec<T>`) so the body can be a legacy
|
||||
`defineBitSchema` codec, a modern `BitPacker`-backed codec, an outer
|
||||
envelope, or any future encoder satisfying the two-method contract
|
||||
- `pack(data, authority, signal?)` is async — delegates signing to
|
||||
`Authority.sign(domain, slot, payload, signal)` so HSM/threshold backends
|
||||
Just Work without the primitive having to know
|
||||
- `unpack(bytes, parentPublicSlot)` enforces verify-then-decode order —
|
||||
body codec is NEVER invoked on a tampered payload
|
||||
- `verify(bytes, parentPublicSlot)` returns `boolean` so callers can
|
||||
enumerate candidate parent keys without exception flow
|
||||
- `unpackSelf(bytes, getEmbeddedSlot)` supports trust-anchor bundles whose
|
||||
own embedded sign slot is the verifier
|
||||
- AbortSignal threaded through `Authority.sign` per suaveplan cancellation
|
||||
rules
|
||||
- `signatureLength` configurable (defaults to ed25519's 64) so future
|
||||
primitives with different signature shapes inherit the schema for free
|
||||
- OTel spans `bitpacker-crypto.signed-pack` and
|
||||
`bitpacker-crypto.signed-verify` with `domain` + `payload_bytes` attrs
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { defineSignedBitSchema } from "@suaveplan/bitpacker-crypto";
|
||||
import { defineBitSchema } from "@suaveplan/bitpacker";
|
||||
import { DOMAIN_PUBLIC_KEYCHAIN_V1 } from "@suaveplan/crypto/domain-tags";
|
||||
import { verifyAuthoritySignature } from "@suaveplan/crypto/authority";
|
||||
|
||||
interface LinkBody {
|
||||
parentSeq: number;
|
||||
purposeBits: number;
|
||||
}
|
||||
const linkSchema = defineBitSchema<LinkBody>([
|
||||
{ name: "parentSeq", type: "uint", bits: 32 },
|
||||
{ name: "purposeBits", type: "uint", bits: 16 },
|
||||
]);
|
||||
|
||||
const link = defineSignedBitSchema<LinkBody>(
|
||||
{
|
||||
pack: (b) => linkSchema.pack(b),
|
||||
unpack: (bytes) => linkSchema.unpack(bytes),
|
||||
},
|
||||
{
|
||||
domain: DOMAIN_PUBLIC_KEYCHAIN_V1,
|
||||
verifier: verifyAuthoritySignature,
|
||||
}
|
||||
);
|
||||
|
||||
const packed = await link.pack(
|
||||
{ parentSeq: 7, purposeBits: 0xf0 },
|
||||
authority
|
||||
);
|
||||
const decoded = link.unpack(packed, parentPublicSlot);
|
||||
```
|
||||
|
||||
The `verifier` option is required and accepts an `AuthorityVerifier`
|
||||
function (`(domain, publicSlot, payload, signature) => boolean`). The
|
||||
canonical implementation is `verifyAuthoritySignature` from
|
||||
`@suaveplan/crypto/authority`. Passing it via DI keeps
|
||||
`@suaveplan/bitpacker-crypto` free of any runtime dependency on
|
||||
`@suaveplan/crypto`, which is what allows future
|
||||
`crypto::ChainBundle`-style consumers to adopt this primitive without
|
||||
inducing a circular package dependency.
|
||||
|
||||
## Usage: Advanced Example — trust anchor via `unpackSelf`
|
||||
|
||||
```ts
|
||||
const trustAnchor = defineSignedBitSchema<TrustAnchorBundle>(codec, {
|
||||
domain: DOMAIN_BITPACKER_SIGNED_V1,
|
||||
});
|
||||
|
||||
// Verify against the bundle's OWN embedded sign slot.
|
||||
const bundle = trustAnchor.unpackSelf(bytes, (decoded) => decoded.embeddedSlot);
|
||||
```
|
||||
|
||||
`unpackSelf` decodes the body once to extract the embedded sign slot, then
|
||||
verifies the signature against that slot. Only used for the root link of a
|
||||
chain (every other link verifies against its parent).
|
||||
|
||||
## API Reference
|
||||
|
||||
### `interface SignedBitSchemaOptions`
|
||||
|
||||
- `domain: DomainTag` (required) — the domain under which the body is signed
|
||||
- `verifier: AuthorityVerifier` (required) — pure verifier function;
|
||||
typically `verifyAuthoritySignature` from `@suaveplan/crypto/authority`
|
||||
- `slot?: SlotId = "sign"` — the authority slot used for signing
|
||||
- `signatureLength?: number = 64` — wire-format signature length
|
||||
|
||||
### `defineSignedBitSchema<T>(bodyCodec, options): SignedBitSchema<T>`
|
||||
|
||||
Returns:
|
||||
|
||||
- `pack(data: T, authority: Authority, signal?: AbortSignal): Promise<Uint8Array>`
|
||||
- throws `CRYPTO_SIGN_FAILED` if the authority returns a wrong-length
|
||||
signature
|
||||
- `unpack(bytes: Uint8Array, parentPublicSlot: KeySlotPublic<unknown>): T`
|
||||
- throws `CRYPTO_VERIFY_FAILED` on verify failure (BEFORE body decode)
|
||||
- `verify(bytes: Uint8Array, parentPublicSlot: KeySlotPublic<unknown>): boolean`
|
||||
- `unpackSelf(bytes: Uint8Array, getEmbeddedSlot: (decoded: T) => KeySlotPublic<unknown>): T`
|
||||
- `domain: DomainTag`, `slot: SlotId`, `signatureLength: number`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The order `verify → decode` is load-bearing. A tampered byte string must
|
||||
never reach the body codec because the codec may have side effects, may
|
||||
log unexpected payloads, or may be slow. Verify-then-decode is asserted
|
||||
by a spec-conformance test that injects a tracking codec and checks
|
||||
`decodeCalls === 0` after a tampered unpack.
|
||||
- `verify(bytes, slot)` deliberately swallows structural exceptions and
|
||||
returns `false` so callers can probe candidate parent keys against an
|
||||
arbitrary byte string without try/catch boilerplate. `unpack(...)` is
|
||||
the throw-on-failure variant.
|
||||
- The body codec is held by reference; mutating it after construction
|
||||
changes future pack/unpack behavior. Treat the codec as immutable.
|
||||
- `Authority.sign` is async because real backends (HSM, TPM, threshold
|
||||
signing, network-attached signers) are async. Even the in-memory v1
|
||||
authority returns a Promise to preserve the contract.
|
||||
- Signature length is enforced on pack to surface a misbehaving authority
|
||||
early. The check is identity-checking the `Promise<Uint8Array>.length`,
|
||||
not the algorithm — a future ECDSA-backed authority signing into an
|
||||
ed25519-shaped slot would be caught here.
|
||||
- OTel spans cost essentially nothing under the no-op tracer; they
|
||||
materialise once a real provider is wired. The spans deliberately do
|
||||
NOT carry the signature itself (sensitive material) or the body bytes
|
||||
(potentially sensitive); only metadata: domain, slot, payload_bytes.
|
||||
Reference in New Issue
Block a user