Initial Commit
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
## Why
|
||||
|
||||
The `verifyChain(bundleBytes, trustAnchorBundleBytes, options)` walker in `packages/crypto/crypto/src/bundles/chain-bundle/chain-bundle.ts` accepts a trust anchor as raw `Uint8Array`. There is no shipped helper to **load** that buffer from disk or environment, so every consumer (CLI, daemons, drive sync, network plugins) hand-rolls the same `readFile + parsePublicBundle + self-verify` sequence — or worse, skips the self-verify and trusts whatever bytes happen to live at the path. The umbrella change `restore-crypto-keychain-packer-umbrella` named this surface (`loadTrustAnchor()` / `loadTrustAnchorFromEnv()` / `SUAVEPLAN_TRUST_ANCHOR_PATH`) but the implementation slice was deferred. The `crypto-spec-restore-versioning` archive (2026-05-03) explicitly excluded the requirement because the `src/trust-anchors/` directory does not exist on genesis. The chain-bundle source already references a `TRUST_ANCHOR.MISMATCH` error code (line 199 of `chain-bundle.ts`) that is never defined in `errors/codes/codes.ts` — a stale doc-only signal that the surface was planned and never built.
|
||||
|
||||
This change fills the gap with a small, dependency-free loader module that takes injectable filesystem and environment readers (no direct `node:fs` import in the universal package), self-verifies the bundle on load, and surfaces stable structured errors. The module is the single auditable point through which trust anchors enter a Suaveplan process — a security-relevant choke point.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **MODIFIED `crypto` capability**: add 1 requirement covering trust-anchor bootstrap. New surface in `packages/crypto/crypto/src/trust-anchors/`:
|
||||
- `TrustAnchor` type — `{ bytes: Uint8Array; bundle: PublicBundle<TLayoutFields> }`
|
||||
- `TrustAnchorReader` interface — `read(path: string): Promise<Uint8Array>`
|
||||
- `loadTrustAnchor(reader, path): Promise<TrustAnchor>` — reads bytes, parses, self-verifies
|
||||
- `loadTrustAnchorFromEnv(reader, getenv?, envVar?): Promise<TrustAnchor>` — env-driven path, defaults to `SUAVEPLAN_TRUST_ANCHOR_PATH`
|
||||
- 3 new error codes: `TRUST_ANCHOR.PATH_MISSING`, `TRUST_ANCHOR.READ_FAILED`, `TRUST_ANCHOR.SELF_VERIFY_FAILED`
|
||||
- Subpath export `./trust-anchors`
|
||||
- **MODIFIED `crypto` capability**: extend the existing `Chain bundle traversal` requirement's `Verified by:` list to include the new trust-anchor self-verify scenario (the loader is what produces the `trustAnchorBundleBytes` `verifyChain` consumes).
|
||||
- This change ships **source + tests + docs**, not just spec. Every requirement carries a `Verified by:` pointer to a real test name, per the convention re-affirmed by `crypto-spec-restore-versioning`.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
(none)
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `crypto`: add 1 requirement (`Trust-anchor bootstrap`); no requirements modified or removed.
|
||||
|
||||
## Impact
|
||||
|
||||
- **Affected specs**: `openspec/specs/crypto/spec.md` (1 requirement added).
|
||||
- **Affected code**:
|
||||
- `packages/crypto/crypto/src/trust-anchors/` (new directory — 5 files: types, impl, tests, docs, barrel).
|
||||
- `packages/crypto/crypto/src/errors/codes/codes.ts` (3 codes added).
|
||||
- `packages/crypto/crypto/src/index.ts` (re-export the new surface).
|
||||
- `packages/crypto/crypto/package.json` (subpath export `./trust-anchors`).
|
||||
- `packages/crypto/crypto/vite.config.ts` (build entry).
|
||||
- `packages/crypto/crypto/tests/no-duplicates.test.ts` (whitelist `trust-anchors/`).
|
||||
- **Affected tests**: 2 new test files (round-trip, error-code conformance). Coverage gate stays at 100/100/100/100.
|
||||
- **Downstream**: `crypto-cli` and any daemon that consumes a chain bundle can replace its hand-rolled loader with the shipped one. No breaking change — the existing `verifyChain` signature is unchanged; consumers opt in to the loader.
|
||||
- **Risk**: low. The loader is a thin wrapper over an existing tested verify-path. Filesystem and env access are caller-injected, so the universal package picks up no platform deps.
|
||||
@@ -0,0 +1,67 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: crypto (crypto-trust-anchor-bootstrap)
|
||||
|
||||
**Package**: `@suaveplan/crypto`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time, pointing to a real test in `packages/crypto/crypto/src/trust-anchors/`.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Trust-anchor bootstrap
|
||||
|
||||
`@suaveplan/crypto` SHALL ship a trust-anchor loader at `src/trust-anchors/` that reads a self-verifying root public bundle from a caller-supplied filesystem reader and returns both the raw bytes (for direct use with `verifyChain`) and the parsed `PublicBundle` (for diagnostics). The loader SHALL self-verify the bundle in root-mode (`verifyPublicBundle(bytes, null)`) on every load — bytes that do not self-verify SHALL be rejected with `CRYPTO.TRUST_ANCHOR.SELF_VERIFY_FAILED`.
|
||||
|
||||
The package SHALL expose:
|
||||
|
||||
- `loadTrustAnchor(reader, path): Promise<TrustAnchor>` — explicit-path entry.
|
||||
- `loadTrustAnchorFromEnv(reader, options): Promise<TrustAnchor>` — env-driven entry. `options.getenv` is required (the universal package never reads `process.env` directly); production wires `(name) => process.env[name]`, tests inject deterministic stubs. `options.envVar` defaults to `SUAVEPLAN_TRUST_ANCHOR_PATH`.
|
||||
- `DEFAULT_TRUST_ANCHOR_ENV_VAR` — the canonical env-var name as a `const`.
|
||||
- Types `TrustAnchor`, `TrustAnchorReader`, `TrustAnchorEnvGetter`, `LoadTrustAnchorFromEnvOptions`.
|
||||
|
||||
The universal-tier package MUST NOT import `node:fs` or read `process.env` at the module-import level; all platform access is caller-injected via the `TrustAnchorReader` interface and the optional `getenv` callback. Production wires `(path) => fs.readFile(path)`; tests inject deterministic stubs.
|
||||
|
||||
#### Scenario: round-trip self-verify
|
||||
|
||||
- **WHEN** a self-signed root public bundle is supplied via the reader and `loadTrustAnchor(reader, path)` is called
|
||||
- **THEN** the call returns `{ bytes, bundle }` where `bytes` is byte-identical to the input and `bundle` has a 32-byte `sign` slot public key
|
||||
|
||||
#### Scenario: env-var default name
|
||||
|
||||
- **WHEN** `loadTrustAnchorFromEnv(reader, { getenv })` is called without `options.envVar`
|
||||
- **THEN** `getenv` is invoked with `"SUAVEPLAN_TRUST_ANCHOR_PATH"` and the returned path is read
|
||||
|
||||
#### Scenario: env-var name override
|
||||
|
||||
- **WHEN** `loadTrustAnchorFromEnv(reader, { envVar: "VAULT_ANCHOR", getenv })` is called
|
||||
- **THEN** `getenv` is invoked with `"VAULT_ANCHOR"` and the returned path is read
|
||||
|
||||
#### Scenario: missing env-var rejected
|
||||
|
||||
- **WHEN** `loadTrustAnchorFromEnv(reader, { envVar, getenv: () => undefined })` is called
|
||||
- **THEN** the call throws `CryptoError` with code `CRYPTO.TRUST_ANCHOR.PATH_MISSING` and `context.envVar` reflects the requested name
|
||||
|
||||
#### Scenario: empty env-var string rejected
|
||||
|
||||
- **WHEN** `loadTrustAnchorFromEnv(reader, { envVar, getenv: () => "" })` is called
|
||||
- **THEN** the call throws `CryptoError` with code `CRYPTO.TRUST_ANCHOR.PATH_MISSING`
|
||||
|
||||
#### Scenario: reader failure surfaces structured error
|
||||
|
||||
- **WHEN** the reader's `read(path)` rejects (e.g. ENOENT)
|
||||
- **THEN** the call throws `CryptoError` with code `CRYPTO.TRUST_ANCHOR.READ_FAILED`, with the reader's error attached via `cause` and `context.path` equal to the requested path
|
||||
|
||||
#### Scenario: tampered bytes rejected
|
||||
|
||||
- **WHEN** the loader receives bytes that parse but fail `verifyPublicBundle(bytes, null)`
|
||||
- **THEN** the call throws `CryptoError` with code `CRYPTO.TRUST_ANCHOR.SELF_VERIFY_FAILED` and `context.byteLength` reflects the actual length
|
||||
|
||||
#### Scenario: garbage bytes rejected
|
||||
|
||||
- **WHEN** the loader receives bytes that fail to parse as a public bundle
|
||||
- **THEN** the call throws `CryptoError` with code `CRYPTO.TRUST_ANCHOR.SELF_VERIFY_FAILED`
|
||||
|
||||
**Verified by:** `packages/crypto/crypto/src/trust-anchors/trust-anchor.test.ts::"loads, self-verifies, and returns byte-identical bytes"` AND `packages/crypto/crypto/src/trust-anchors/trust-anchor.test.ts::"the returned bundle parses identically to a fresh verify on the bytes"` AND `packages/crypto/crypto/src/trust-anchors/trust-anchor.test.ts::"uses the default env var name SUAVEPLAN_TRUST_ANCHOR_PATH"` AND `packages/crypto/crypto/src/trust-anchors/trust-anchor.test.ts::"honors a caller-supplied envVar override"` AND `packages/crypto/crypto/src/trust-anchors/trust-anchor.test.ts::"getenv is invoked exactly once with the resolved env-var name"` AND `packages/crypto/crypto/src/trust-anchors/trust-anchor.spec-conformance.test.ts::"throws TRUST_ANCHOR.PATH_MISSING when env var is unset"` AND `packages/crypto/crypto/src/trust-anchors/trust-anchor.spec-conformance.test.ts::"throws TRUST_ANCHOR.PATH_MISSING when env var is empty string"` AND `packages/crypto/crypto/src/trust-anchors/trust-anchor.spec-conformance.test.ts::"throws TRUST_ANCHOR.READ_FAILED when reader rejects"` AND `packages/crypto/crypto/src/trust-anchors/trust-anchor.spec-conformance.test.ts::"throws TRUST_ANCHOR.SELF_VERIFY_FAILED on tampered bundle bytes"` AND `packages/crypto/crypto/src/trust-anchors/trust-anchor.spec-conformance.test.ts::"throws TRUST_ANCHOR.SELF_VERIFY_FAILED on garbage bytes"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
@@ -0,0 +1,35 @@
|
||||
## 1. Source
|
||||
|
||||
- [ ] 1.1 Create `packages/crypto/crypto/src/trust-anchors/trust-anchor.types.ts` with `TrustAnchor`, `TrustAnchorReader`, `LoadTrustAnchorOptions`, `LoadTrustAnchorFromEnvOptions`
|
||||
- [ ] 1.2 Create `packages/crypto/crypto/src/trust-anchors/trust-anchor.ts` with `loadTrustAnchor(reader, path): Promise<TrustAnchor>` and `loadTrustAnchorFromEnv(reader, options?): Promise<TrustAnchor>`. Default env var: `SUAVEPLAN_TRUST_ANCHOR_PATH`. Self-verify via existing `verifyPublicBundle(bytes, null)` (root-mode).
|
||||
- [ ] 1.3 Add error codes to `packages/crypto/crypto/src/errors/codes/codes.ts`: `TRUST_ANCHOR.PATH_MISSING`, `TRUST_ANCHOR.READ_FAILED`, `TRUST_ANCHOR.SELF_VERIFY_FAILED`
|
||||
- [ ] 1.4 Create `packages/crypto/crypto/src/trust-anchors/index.ts` (barrel — explicit named exports per `openspec/AGENTS.md §6a`)
|
||||
- [ ] 1.5 Re-export the public surface from `packages/crypto/crypto/src/index.ts`
|
||||
|
||||
## 2. Build wiring
|
||||
|
||||
- [ ] 2.1 Add subpath export `./trust-anchors` to `packages/crypto/crypto/package.json`
|
||||
- [ ] 2.2 Add `trust-anchors/index` build entry to `packages/crypto/crypto/vite.config.ts`
|
||||
- [ ] 2.3 Whitelist `trust-anchors` in `packages/crypto/crypto/tests/no-duplicates.test.ts` (allowed top-level src dirs)
|
||||
|
||||
## 3. Tests + docs
|
||||
|
||||
- [ ] 3.1 Create `packages/crypto/crypto/src/trust-anchors/trust-anchor.test.ts` covering: round-trip self-verify; deterministic injectable `reader`; explicit-path and env-path entry points; verified bundle is byte-identical to the input.
|
||||
- [ ] 3.2 Create `packages/crypto/crypto/src/trust-anchors/trust-anchor.spec-conformance.test.ts` covering all 3 new error codes (`PATH_MISSING`, `READ_FAILED`, `SELF_VERIFY_FAILED`).
|
||||
- [ ] 3.3 Create `packages/crypto/crypto/src/trust-anchors/trust-anchor.md` (≥200 words, basic + advanced examples, full API reference).
|
||||
|
||||
## 4. Spec delta
|
||||
|
||||
- [ ] 4.1 Author `openspec/changes/crypto-trust-anchor-bootstrap/specs/crypto/spec.md` with the `## ADDED Requirements` block for `Trust-anchor bootstrap`.
|
||||
- [ ] 4.2 Run `openspec validate crypto-trust-anchor-bootstrap --strict` — green.
|
||||
|
||||
## 5. Verification gate
|
||||
|
||||
- [ ] 5.1 `bunx turbo lint typecheck test --filter=@suaveplan/crypto` — green.
|
||||
- [ ] 5.2 `bunx turbo test:coverage --filter=@suaveplan/crypto` — 100/100/100/100.
|
||||
- [ ] 5.3 `bunx turbo build --filter=...@suaveplan/crypto` — every dependent compiles green.
|
||||
|
||||
## 6. Archive
|
||||
|
||||
- [ ] 6.1 `openspec archive crypto-trust-anchor-bootstrap --yes` — fold the requirement into `openspec/specs/crypto/spec.md`.
|
||||
- [ ] 6.2 Confirm post-archive: `openspec validate --specs` shows `✓ spec/crypto`.
|
||||
Reference in New Issue
Block a user