Skip to content

feat(rules): signed v2 rules distribution (engine: producer tooling, --rules-source, trust root)#70

Merged
trustabl merged 5 commits into
mainfrom
cf/amazing-cerf-0c3008
Jun 16, 2026
Merged

feat(rules): signed v2 rules distribution (engine: producer tooling, --rules-source, trust root)#70
trustabl merged 5 commits into
mainfrom
cf/amazing-cerf-0c3008

Conversation

@jhumel-code

Copy link
Copy Markdown
Collaborator

What

The engine half of the signed v2 rules distribution (the production-channel feature). It makes the scanner able to resolve rules from a signature-verified release channel, and adds the producer tooling + the trust root, with all audit hardening applied.

Highlights:

  • cmd/rulesctl (a separate binary, NOT shipped to users) + internal/rulepub: keygen / bundle / sign / verify. Producer and verifier share rulesign's CanonicalDigest / StatementSigningPayload, so they cannot drift. No private-key/signing code links into the scanner binary.
  • internal/rulesign: exported WriteCanonicalTar; ValidateBundlePath rejects non-portable / case-folding paths on both producer and consumer (so the on-disk install always equals the verified digest); embedded the real signing public key in keyring.json + a TestEmbeddedKeyring_IsPopulated guard so a build can never ship an empty trust root.
  • internal/rulesource: build-embedded per-channel genesis version floor (trust-on-first-use rollback guard, enforced online and offline); validate-the-manifest-before-advancing-the-floor; installBundle self-heals a partial cache dir; fail-closed staleness on a corrupt cached expiry.
  • cmd/trustabl: --rules-source flag (--channel kept as a deprecated alias); effectiveRules derives the rulesource.Config and the RulesOrigin from one decision so provenance can never disagree with the resolved source; a signed channel served from a custom repo is watermarked and gets a distinct ScanID.
  • Docs (README/ARCHITECTURE) and tests throughout.

The default scan is unchanged — it still uses the unsigned git source. The flip to signed-production-by-default is a separate, later step (TR-325), gated on a first published statement.

Audit + verification

Two full-precision adversarial audits were run over the whole v2 surface; every confirmed finding was fixed (portability validator, install self-heal, fail-closed staleness, provenance labeling, the producer self-verify, CI script-injection — the last shipped separately in trustabl-rules). Secret hygiene: only the public key is committed; no private-key material is in any tracked file.

go build ./... and gofmt are clean; every v2 package passes. The branch was merged up to date with main (24 commits) with no code conflicts.

CI note: on this developer machine, internal/llm and cmd/trustabl's TestLLM* tests fail because the machine has a real API key configured that those tests read (an existing test-isolation issue in main, not touched by this PR — the v2 commit changes nothing under internal/llm / internal/enrichment). On a clean CI runner with no configured key they pass. The Windows-only TestSave_FilePermissions quirk is likewise pre-existing and Linux-CI-clean.

Companion changes (trustabl-rules)

Once this merges, main carries the embedded key + cmd/rulesctl, which the publish workflow needs to build and self-verify. Refs TR-319..325.

🤖 Generated with Claude Code

…e, audit hardening

Engine half of the signed rules-distribution (production-channel) feature:

- cmd/rulesctl (separate, non-shipped binary) + internal/rulepub: keygen, bundle,
  sign, verify — share rulesign's CanonicalDigest / StatementSigningPayload so
  producer and verifier cannot drift.
- internal/rulesign: export WriteCanonicalTar; ValidateBundlePath rejects
  non-portable / case-folding paths on both producer and consumer; embed the real
  signing public key in keyring.json + TestEmbeddedKeyring_IsPopulated guard.
- internal/rulesource: genesis version floor (TOFU rollback guard, enforced
  online and offline); validate-before-advance-floor; installBundle self-heals a
  partial dest; fail-closed staleness on a corrupt cached expiry.
- cmd/trustabl: --rules-source flag (--channel kept as deprecated alias);
  effectiveRules derives Config and RulesOrigin from one decision; signed-from-
  custom-repo is watermarked + gets a distinct ScanID.
- Docs + tests throughout. Refs TR-319..325.
Completeness/robustness from the maximal-fanout audit (crypto-keys, digest-
portability, rollback-genesis, cache-concurrency). No change to default behavior.

- rulesign: ParseKeyring rejects an inverted validity window; ValidateBundlePath
  rejects '~'-prefixed components; CanonicalDigest/WriteCanonicalTar reject the
  empty bundle and emit an explicit USTAR over-long-path error. New tests lock
  key ROTATION (overlapping windows) and REVOCATION (key removal) end to end.
- rulesource: pruneBundles bounds the signed bundle cache (keeps every channel's
  active digest, clears orphaned .tmp-bundle-* dirs); RecordStatement refreshes
  the offline pointer on an equal-version re-sign while keeping the floor
  monotonic; 0o700 on the bundle/channel-state dirs.
- rulesctl: keygen --not-after + RFC3339 timestamps (rotation tooling).
- docs: key rotation/revocation runbook, bundle-path contract, genesis-floor
  unit, zero-tolerance clock-skew stance. Refs TR-319..325.
- rules pull --rules-source/--channel: pre-warm a signed channel's bundle cache
  (the documented offline story now works for signed rules, not just git).
- mcp: --rules-source/--channel + propagate RulesOrigin/Stale/SchemaNewer into the
  returned ScanResult (MCP scans were dropping all rules provenance/stale signals).
- scan: --require-signed / TRUSTABL_REQUIRE_SIGNED=1 fail-closed gate (refuse the
  unsigned git path); cutover guard test (a signed default must set a genesis floor).
- tests: releaseSource.Pull no-fallback contract; statement freshness boundary.
- docs: README --rules-source + schema-version example; ARCHITECTURE ScanResult
  provenance fields, bad-promote recovery + statement-renewal runbook. Refs TR-319..325.
…-trip

- transport: make the byte/entry/unpacked ceilings test-overridable; add boundary
  tests (over-cap rejected, at-cap succeeds) for the decompression-bomb and
  unbounded-body defenses.
- rulesctl: end-to-end keygen->bundle->sign->verify CLI test (O_EXCL seed,
  --not-after validation, channel-mismatch refusal) — the producer chain the
  publish workflow depends on. Refs TR-319..325.
@trustabl trustabl merged commit cc7f9e7 into main Jun 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants