From 010e253d66fb6faef5596eb06461a8ccadf36f51 Mon Sep 17 00:00:00 2001 From: claude Date: Sun, 26 Jul 2026 02:22:35 +0000 Subject: [PATCH] docs: sharpen the attribution guarantee to a byte<->activation-key binding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revised per PR #480 (#5607 owner clarification + #5608 codex resolution; #5612 directs the update): - The audit row no longer implies upstream observation or agent-only authorship. Reworded the guarantee: the row cryptographically binds commit bytes (control-plane-RECOMPUTED SHA) to access to the activation signing key, and binds that key to control-plane-owned activation metadata. An agent can sign arbitrary contents but cannot verify as a different activation or choose the recorded metadata. - Control plane accepts gateway-delivered opaque bytes, independently recomputes the Git object ID, verifies the embedded signature against the activation key, and stamps its own metadata. Trusts no gateway SHA/key/verdict/metadata. No upstream fetch. - Purged overclaims: removed "a compromised gateway cannot fabricate an audit binding" (the sidecar holds the signing capability, so it can — and that's acceptable under the intended guarantee), plus "accepted push" / "introduced upstream" framing. - Resolved the control-plane-transport open question in-PRD (was left open; codex asked to resolve): transport is gateway bytes + recompute + verify; mirror-read is no stronger. Issue: #423 Co-Authored-By: Claude Opus 4.8 --- .../prd-new-signed-commits-attribution.md | 177 +++++++++++------- 1 file changed, 110 insertions(+), 67 deletions(-) diff --git a/docs/prds/prd-new-signed-commits-attribution.md b/docs/prds/prd-new-signed-commits-attribution.md index c98f0006..108dad00 100644 --- a/docs/prds/prd-new-signed-commits-attribution.md +++ b/docs/prds/prd-new-signed-commits-attribution.md @@ -8,15 +8,18 @@ ## Summary Give each bottled agent a **per-activation signing key** so that every commit it -introduces is signed in the git-gate trust boundary (outside the bottle) and +produces is signed in the git-gate trust boundary (outside the bottle) and recorded in bot-bottle's own **host-owned audit store**, which is the portable -source of truth binding each signed commit to a bottle, host, manifest, agent, -activation interval, and retained public key — along with the commit's *claimed* -author. The gate mints a short-lived Ed25519 key at spin-up, holds the private -half in the sidecar `ssh-agent`, and forwards only `SSH_AUTH_SOCK` into the -bottle. The gate rejects any newly-introduced commit that is not signed by the -activation key; the **control plane** independently re-verifies each signature -before recording attribution. +source of truth. Each row cryptographically binds a commit's bytes (and their +control-plane-recomputed SHA) to **access to that activation's signing key**, and +binds the key to control-plane-owned activation metadata — bottle, host, +manifest, agent, activation interval, retained public key — plus the commit's +*claimed* author. The gate mints a short-lived Ed25519 key at spin-up, holds the +private half in the sidecar `ssh-agent`, and forwards only `SSH_AUTH_SOCK` into +the bottle. The gate rejects any commit it forwards that is not signed by the +activation key; separately, the **control plane** independently recomputes each +commit's object ID and verifies its signature before recording attribution — it +never trusts a SHA, key, or verdict asserted by the gate. This PRD deliberately does **not** enforce or vouch author/committer identity. Author/committer name/email are recorded as claims carried inside the signed @@ -43,26 +46,38 @@ Successor to: The crisp property this feature provides: -> Every new commit introduced through this bottle's gate is **signed by the -> activation key**, and the **host-owned audit store** — after the control plane -> independently verifies that signature — records it, binding the SHA to the -> bottle, host, manifest, agent, activation interval, and retained public key. -> The commit's author/committer identity is **recorded as a claim**, not enforced -> or vouched. The forge remains only the repository transport/capability layer. +> The **host-owned audit store** binds a set of commit bytes — whose Git object +> ID the control plane **recomputes** itself — to **access to this activation's +> signing key**, and binds that key to **control-plane-owned activation +> metadata**: bottle, host, manifest, agent, activation interval, retained public +> key. An agent may author and sign arbitrary commit contents, but it cannot make +> that signature verify as a *different* activation, and it cannot choose the +> activation metadata the control plane records. The commit's author/committer +> identity is **recorded as a claim**, not enforced or vouched. The forge remains +> only the repository transport/capability layer. -What the signature does and does not prove (issue #423, comment #5554): +What this does and does not prove (issue #423, comments #5554 / #5607 / #5608): -- The signature proves **bottle/activation provenance**: the commit was created - with access to activation *Y*'s signing key, i.e. inside this bottle during - this activation. Independent verification binds SHA *X* to activation *Y*. -- The signature does **not** make the author name/email cryptographically - vouched. The bottle chooses every byte sent through the forwarded agent, so a - raw signature could sign `author Mallory ` just as validly. - Author/committer identity is a claim carried inside the signed object and - recorded as-is. -- The audit record is a cryptographic binding only because the **control plane** - verifies the signature before recording — not because the data-plane gate - asserts it (see **Trust boundary**). +- It proves **access to activation *Y*'s signing key**: whoever assembled these + commit bytes could sign with that key. Recomputing the object ID and verifying + the embedded signature binds the SHA to activation *Y*, and the control plane's + own records bind *Y*'s key to *Y*'s metadata. +- It does **not** prove the commit was ever pushed, observed upstream, kept + (vs. later reverted or dropped), or produced by the *agent* rather than by any + other holder of the activation signing capability (the sidecar itself). The + store deliberately makes no claim about publication or sole-agent authorship + — the owner's requirement is attribution of *what manifest/agent/etc. was in + use when a commit was signed*, not proof of where the commit went (#5607). +- It does **not** make author/committer identity cryptographically vouched. The + bottle chooses every byte sent through the forwarded agent, so a signature over + `author Mallory ` is just as valid. Those fields are a claim + carried inside the signed object and recorded as-is. +- The binding is trustworthy because the **control plane** supplies the SHA (it + recomputes it), the public key, and the activation metadata from its own state + — never from a value the gateway asserts (see **Trust boundary**). The + residual, by design: anything that holds the activation signing capability can + produce commits that attribute to that activation. That is inherent to a + binding on *activation-key access*, not a defect. ## Problem @@ -94,10 +109,12 @@ tamper-evidence over it: unsigned or wrong-key new commit is rejected, loudly, with the offending SHA. This is a **signature** check only — no author/committer matching. - **Control-plane-owned attribution.** The orchestrator/control plane (sole - owner of `bot-bottle.db`, PRD 0070) independently verifies each new commit's - signature and only then writes the attribution row. A gateway assertion alone - never creates an audit record. -- **Host is the source of truth.** The audit record binds each attributed SHA to + owner of `bot-bottle.db`, PRD 0070) recomputes each commit's object ID from the + bytes, verifies the embedded signature against the activation public key it + holds, and attaches activation metadata from its own state — accepting no SHA, + key, verdict, or metadata asserted by the gateway. No upstream fetch is + required. +- **Host is the source of truth.** The audit record binds each recomputed SHA to the bottle, host, manifest, agent, activation interval, and retained public key, and records the commit's claimed author/committer. - **Verifiable after teardown.** The audit record retains the **full public @@ -166,18 +183,30 @@ and forwards pushes. The orchestrator is the **control plane** and, per PRD and the audit record must be anchored in the control plane: - The gate performs a **synchronous pre-forward signature check** (below) and - can reject a push before it reaches the upstream. This is a data-plane gate, - not the audit binding. -- The **control plane independently verifies** each new commit's signature — - receiving the commit object + signature (or independently reading/fetching the - objects it is asked to attribute) — and only then writes `attributed_commit`. - A gateway assertion alone never produces a row. If the gateway is compromised, - the worst it can do is forward or reject pushes; it cannot fabricate an audit - binding, because the control plane re-verifies against the activation public - key it minted and holds. + can reject a push before it reaches the upstream. This is a data-plane gate on + what leaves the bottle, not the audit binding. +- The **control plane** takes the commit bytes to attribute (gateway-delivered + opaque bytes are fine), **recomputes the Git object ID**, **verifies the + embedded signature** against the activation public key it minted and holds, and + writes `attributed_commit` attaching metadata from its own state. It accepts + **no** gateway-supplied `verified` flag, claimed SHA, public key, or activation + identity. -The gate cannot stand in for host-side audit verification (issue #423, review by -didericis-codex on d8362ec). +The precise trust statement (issue #423, review by didericis-codex on d8362ec, +resolved in #5608): the row binds *these commit bytes / this recomputed SHA* to +*access to this activation's signing key*, and the control plane binds that key +to the recorded activation metadata. It does **not** assert forge observation or +that only the agent (not the signing sidecar) authored the commit — so this PRD +does **not** claim a compromised gateway cannot obtain an attribution row. +Because the sidecar holds the activation signing capability, a compromised +gateway *can* assemble and sign a commit and have it attributed to that +activation; what it cannot do is make the signature verify as a *different* +activation or choose the metadata the control plane records. That residual is +acceptable under the intended guarantee (#5607) and is why the guarantee is +worded as activation-key access, not agent-only authorship or upstream +publication. The gate therefore cannot stand in for host-side verification: the +control plane recomputes the object ID and verifies the signature itself rather +than trusting the gate's word. ### Identity model @@ -266,20 +295,25 @@ record. ### Control-plane verification & recording -On an accepted push (or asynchronously, driven by the accepted-push event), the -orchestrator/control plane: +For each commit to attribute (the gate hands the control plane the commit bytes; +opaque gateway-delivered bytes are acceptable because nothing the gateway *says* +about them is trusted), the orchestrator/control plane: -1. Obtains the commit object and its signature for each newly-introduced SHA — - either delivered by the gate as opaque bytes to be checked, or read/fetched - independently from the mirrored repository. Either way the control plane does - not trust a "verified" flag from the gate. -2. **Independently verifies** the signature against the activation public key it - minted and holds for that activation (via a generated allowed-signers file). -3. Writes `attributed_commit` only for SHAs that pass, recording the commit's - claimed author/committer alongside the activation binding. +1. **Recomputes the Git object ID** from the bytes itself. The stored `sha` is + this recomputed value, never a SHA the gateway claims. +2. **Verifies the embedded signature** against the activation public key it + minted and holds for that activation (via a generated allowed-signers file) — + ignoring any `verified` flag, key, or activation identity supplied by the + gateway. +3. Writes `attributed_commit` only for bytes that pass, stamping the activation + metadata (bottle/manifest/agent/host/interval) from its **own** state — not + from anything the gateway provides — and recording the commit's claimed + author/committer. -A SHA the control plane cannot independently verify is **not** recorded as -attributed (it may be logged as an anomaly instead). +No upstream fetch is required: the guarantee is a byte↔activation-key binding, so +the object does not need to come from the forge (issue #423, #5608). Bytes that +do not verify against the activation key are **not** recorded as attributed (they +may be logged as an anomaly instead). ### Audit trail @@ -307,7 +341,7 @@ CREATE TABLE bottled_agent_activation ( ); CREATE TABLE attributed_commit ( - sha TEXT NOT NULL, + sha TEXT NOT NULL, -- control-plane-RECOMPUTED object ID, not gateway-claimed bottled_agent_slug TEXT NOT NULL, activation_id TEXT NOT NULL, repo TEXT NOT NULL, @@ -378,11 +412,12 @@ validating signing broker in front of the key would be required for the latter. **passes the gate** (identity is not enforced here). 5. **Control-plane verification + audit.** `bottled_agent_activation` / `attributed_commit` tables (PRD 0067 store, control-plane-owned per PRD 0070); - the control plane independently verifies each new commit's signature before - writing a row; retain full pubkey + fingerprint + principal + validity - interval; record claimed author/committer; allowed-signers generation + a - post-teardown `verify-commit` helper. Test that a gateway assertion without an - independently-verifiable signature produces **no** attribution row. + the control plane recomputes each commit's object ID and verifies the + signature before writing a row; retain full pubkey + fingerprint + principal + + validity interval; record claimed author/committer; allowed-signers generation + + a post-teardown `verify-commit` helper. Tests: a gateway-claimed SHA/key/ + verdict is ignored — the row's `sha` is the recomputed ID and bytes not signed + by the activation key produce **no** row. 6. **Docs.** Glossary entry ("per-bottle signed commits"); README manifest section; ADR note that signing-enabled bottles gain signed *provenance* and a host-owned audit record while authorship stays *claimed* (ADR 0002 unchanged). @@ -398,9 +433,10 @@ validating signing broker in front of the key would be required for the latter. the upstream-reachable exclusion (pull + merge human history and push a signed merge); a correctly-signed foreign-author commit **passes** (no identity enforcement); a clean all-signed push succeeds. -- **Control plane (must):** independent verification records an attribution row - for a genuinely-signed SHA; a SHA presented with an invalid/foreign signature - (or a bare gateway assertion) produces **no** row. +- **Control plane (must):** the control plane recomputes the object ID and + records a row for bytes genuinely signed by the activation key; a gateway- + supplied SHA/key/verdict is ignored (the stored `sha` is the recomputed value); + bytes signed by a foreign/invalid key produce **no** row. - **Lifecycle:** activation mints the key and writes an `active` row; teardown retires it (`valid_until`) and revokes deploy keys fail-loud; a restart re-attaches the same key (no new row); a fresh activation mints a new key and @@ -408,13 +444,20 @@ validating signing broker in front of the key would be required for the latter. - **Post-teardown verification:** regenerate the allowed-signers file from a `retired` row and confirm `verify-commit` still succeeds for an attributed SHA. +## Resolved: control-plane transport + +Raised in review and **resolved** (issue #423, #5608): the commit object does not +need to come from the forge, and reading the gateway-owned mirror is no stronger +than accepting gateway-delivered bytes — both are fabricatable, and neither +matters because the control plane trusts nothing the gateway *asserts*. The +transport is therefore: the gate hands the control plane the raw commit bytes, +the control plane **recomputes the object ID** and **verifies the signature** +against the activation key, and stamps its own activation metadata. No upstream +fetch. This is exactly what makes the byte↔activation-key binding sound +regardless of transport. + ## Open questions -- **Control-plane transport for verification.** How does the orchestrator obtain - the commit object + signature to verify — pushed to it by the gate as opaque - bytes, or read/fetched independently from the mirror? Independent read is the - stronger boundary; confirm it composes with the mirror layout (PRD 0008) and - the orchestrator's repo access (PRD 0070). - **Where the gate check slots into PRD 0008 ordering.** Modeled as a pre-forward step after gitleaks; confirm it composes with the existing access-hook / mirror ordering rather than needing a separate hook.