Repository navigation
feat(bep): T130 — Hold the proxy's keys in Secret Service where the host has no usable TPM - #3330
minimal-foundry[bot] wants to merge 7 commits into
Conversation
Add a Secret Service key store for a Linux host with no usable TPM. It generates each proxy key as a software P-256 key and holds it through `secret-tool`, under the role's label. A locked keyring and an unreachable Secret Service refuse the start, keep the keys, and name what to do, with no replacement command (BEP-089).
|
Important Review skippedBot user detected. To trigger a single review, invoke the
|
|
Reviewed: 2 findings, none blocking at Address the comments on the diff. Clobber: none seen. |
There was a problem hiding this comment.
arch-review: CONDITIONS · 1 condition · the full ruling
The Secret Service store is in the right crate, and the explicit locked and unreachable refusals keep the keys as BEP-089 requires. One path fails open. lookup reads an exit 1 with empty stderr as "no key", and a dismissed unlock prompt on a locked keyring gives that result. Then the start names bep --replace-key or overwrites the held key. Gatehouse §6.10 line 832 says the sealing key changes only by re-enrollment. Make the change in C1.
Ruled at c7d6a14 against gominimal/arch@15b603e6afb8 by claude-opus-5-5.
When secret-tool store fails before it reads the key, the write gets a broken pipe. Wait for the tool and classify its exit and error output, so a locked or unreachable keyring prints its refusal on the first start.
|
Round 1 of 10 on the runtime's review findings, pushed as
Verify Review threads the coder judged:
With a fake Notes from the agent: Revise round 1: SecretService::run now waits for the tool after a failed write. If the tool failed, run returns its output so that the caller classifies stderr as Locked, Unreachable or Store. A write failure with a successful exit stays a Store error. The new test sends 1 MiB to a fake that exits without reading, so the broken pipe always happens. cargo clippy -p bep --all-targets is clean. |
There was a problem hiding this comment.
arch-review: CONDITIONS · 2 conditions · the full ruling
The locked and unreachable refusals conform. They keep the key and do not name bep --replace-key. Two items stay open. SecretServiceKey::exportable() returns false for a key that Secret Service lets out, so the custody rule records it as non-exportable (§6.10 line 837). Make the exception explicit in custody() with C1. A dismissed unlock prompt reads as a lost key, and the start then tells the operator to replace a key that still exists. Fix that in lookup with C2.
Ruled at 1921d6e against gominimal/arch@15b603e6afb8 by claude-opus-5-5.
Kill a secret-tool call that outlives 60 s and refuse the start as a locked keyring, so an unanswered unlock prompt never hangs the proxy. Read a lookup as an empty slot only on exit 1 with no message, and only when `secret-tool search` lists no item. A listed item is a locked one. Narrow the error phrases that sort a failure as locked or unreachable.
…roxy-s-keys-in-secret-service-where-th
|
Round 2 of 10 on the runtime's review findings, pushed as
Verify Review threads the coder judged:
With a fake Notes from the agent: Revise round 2. Only crates/bep/src/secret_service.rs changed. A secret-tool call now has a 60 s limit (SecretService::with_timeout overrides it for tests). A call that outlives the limit is killed and is a Locked refusal. Stdout and stderr drain on their own threads. A silent |
There was a problem hiding this comment.
arch-review: CONDITIONS · 1 condition · the full ruling
The Secret Service store and its locked and unreachable refusals conform to BEP-089. They fail closed, keep the keys, and do not name a replacement. One point blocks the merge. SecretServiceKey::exportable() returns false, so the custody gate records a key that any program of the user can copy as non-exportable. Gatehouse §6.10 line 837 says non-exportable custody means no export. Make exportable() return true as C1 says, and leave the release gate closed until the store-choice task adds a software-custody case.
Ruled at e6f7857 against gominimal/arch@15b603e6afb8 by claude-opus-5-5.
A process that secret-tool starts, such as an autolaunched D-Bus daemon, can hold its pipes open. The drain now waits one second after the tool exits and then uses the output it read.
|
Round 3 of 10 on the runtime's review findings, pushed as
Verify Review threads the coder judged:
With a fake Notes from the agent: Round 3. One change: |
There was a problem hiding this comment.
arch-review: CONDITIONS · 1 condition · the full ruling
The refusals for a locked or unreachable keyring conform, and the new store is in the right place. The new output drain fails open. When the grace ends before the reader thread reads the output, run returns the partial output as a success. Then lookup reads a held key as an empty slot. Gatehouse §6.10 line 832 keeps the key unchanged except by re-enrollment, and principle 7 makes the store safe by default. Make the change in C1: an incomplete drain is a refusal, and a lookup that exits 0 with no output is unreadable, not empty.
Ruled at 7da0429 against gominimal/arch@15b603e6afb8 by claude-opus-5-5.
|
/revise arch C1 (ruling 1202 at 7da0429; threads PRRT_kwDOPdfcxM620m85, PRRT_kwDOPdfcxM620m86): the output drain fails open. Evidence,
Do:
Do not change |
…roxy-s-keys-in-secret-service-where-th
A pipe that is still open after secret-tool exits can still carry the key, so run() no longer returns the partial output as the whole answer. A lookup that exits 0 with no secret is now unreadable, not an empty slot.
|
Round 4 of 10 on a /revise note, pushed as
Verify Review threads the coder judged:
With a fake Notes from the agent: Round 4. Drain::collect now returns the bytes and whether the pipe reached its end. SecretService::run returns failure(role, &out) when a pipe stays open and stderr is not blank, otherwise KeyError::Store with a detail that says an output pipe of |
There was a problem hiding this comment.
arch-review: PASS · the full ruling
This head conforms to Gatehouse §6.10 and to design principle 7. A locked keyring or an unreachable Secret Service refuses the start. The refusal keeps every key and does not name bep --replace-key. The compare from 7da04290 closes the last two fail-open lookup paths: an output pipe that stays open, and exit 0 with no secret. The selection of this store at start is BEP-088's task, and the report of software custody is BEP-090's.
Ruled at 167aa38 against gominimal/arch@15b603e6afb8 by claude-opus-5-5.
|
/revise arch C1 (re-ruling on threads PRRT_kwDOPdfcxM62z7rm, PRRT_kwDOPdfcxM620Snl; supersedes ruling 1202 on this point): Evidence: Do:
|
|
That What the platform saidcould not start the coder: Error: there is no container instance that can be provided to this durable object Nothing to type again: the command gets a rocket when its round starts, or a refusal that names the reason. No round was counted for it yet. |
|
Round 5 of 10 on a /revise note, pushed as
Verify Review threads the coder judged:
With a fake Notes from the agent: Revise round 5 (arch C1): |
T130 — Hold the proxy's keys in Secret Service where the host has no usable TPM
slice: S18
kind: task
covers: BEP-089
size: M
est: 260 code, 200 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: each refusal prints to the person who started
bep, pinned by its test: "The proxy did not start: the login keyring is locked, and it holds the proxy's key. Unlock the login keyring, then start the proxy again." and "The proxy did not start: Secret Service cannot be reached from this session, and it holds the proxy's key. Start the proxy from the desktop session that holds the login keyring."observability:
beplogs one line for each key it generates or loads from Secret Service, with the role, the key's fingerprint andstore=secret-servicelayers:
- store:
- crates/bep/src/secret_service.rs (new): a key store that generates each proxy key as a software P-256 key and holds it in Secret Service through
secret-tool, assignin.rsholds the sign-in, under the role's label, with the locked and unreachable cases asKeyErrors that name the role and keep the key- crates/bep/src/lib.rs: declares the
secret_servicemodule for Linux targets- crates/bep/src/keys.rs:
StartErrorprints the locked and unreachable refusals above, and names no replacementtest: cargo nextest run -p bep a_locked_secret_service_refuses_start
test: cargo nextest run -p bep an_unreachable_secret_service_refuses_start
produces:
- crates/bep/src/secret_service.rs
consumes: none
proof: With a fake
secret-tool, the proxy's keys are generated and held in Secret Service, and a locked or unreachable keyring refuses to start, keeps the keys and names what to do.Proof
With a fake
secret-tool(the tests in crates/bep/src/secret_service.rs), the proxy's three keys are generated as software P-256 keys and held as Secret Service items labelled dev.gominimal.bep.. A restart loads the same keys. A locked keyring or an unreachable Secret Service refuses the start with the two pinned messages, which do not namebep --replace-key, and the keys are unchanged when the keyring comes back.Notes from the agent: The new store is not yet chosen at start:
bep serveon Linux does not use it. The store choice (TPM first, then Secret Service) is BEP-088 and is outside this task's layers. SecretServiceKey::exportable() returns false. The custody rule refuses an exportable key in a release build, and BEP-088 requires a software key here, so a true value would make a release build refuse every Secret Service key. BEP-090 reports the reduced custody. secret-tool errors are put into a class by their stderr text: 'locked' is the locked class, and D-Bus or missing-service text is the unreachable class. If a person dismisses an unlock prompt, libsecret can return no item and no error. In that case a lookup reads as 'no key', and load_with reports the key as missing. The refusal for an unreachable store says 'it holds the proxy's key' also on a first start. That wording is copied from the task's diagnostics line. The task's load line has no store field, so a generated key gets two log lines: the store's line with store=secret-service, and the existing 'generated a proxy key' line in load_with. The changedmissing_incfg now also includes Linux, and the function is pub(crate), so the new store can use it.Tests
cargo nextest run -p bep a_locked_secret_service_refuses_startcargo nextest run -p bep an_unreachable_secret_service_refuses_startVerify
min task run agent-verifypassed ([tasks.verify]).Closes #3325