Repository navigation
feat(bep): rotate the signing CA under an unchanged root - #2508
norrietaylor merged 10 commits into
Conversation
Add `bep --rotate-signing`, which issues a new signing certificate under the same root and key identifier, and have `bep serve` renew its signing CA 30 days before it expires. Each rotation logs the old and new signing fingerprints. Boxes trust only the root, so they see no change.
|
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 · 2 conditions · the full ruling
The renewal in bep serve conforms to Gatehouse §6.10 line 839. The root and the key identifier stay the same, so running boxes keep their trust. The one-shot bep --rotate-signing does not rotate anything that a proxy uses. It renews a throwaway Ca in memory, then prints "Signing certificate rotated" and logs fingerprints of certificates that never signed a leaf. Remove that path (C1). File the control-socket command and the open end-to-end leg as residuals with issues (C2).
Ruled at 9e74da7 against gominimal/arch@109541a48c31 by claude-opus-5-5.
`bep --rotate-signing` built a CA in its own process and exited, so the serving proxy kept its signing certificate. It also issued the root again and published the identity as a side effect. Now `bep serve` holds a lock file with its pid beside its control socket and records its signing fingerprint there. On SIGUSR1 it rotates its signing CA. `bep --rotate-signing` signals that process and prints the fixed line only after the fingerprint changes. It refuses when no proxy runs.
|
Round 1 of 10 on the runtime's review findings, pushed as
Verify Review threads the coder judged:
An operator runs Notes from the agent: Revise round 1. The finding was valid: the old |
There was a problem hiding this comment.
arch-review: PASS · the full ruling
The change conforms to Gatehouse §6.10. A rotation re-issues the signing CA under the same key, subject and root, so a box that holds the root sees no change (line 839, line 821). bep --rotate-signing now asks the running bep serve to rotate, and it prints success only after the proxy records a new fingerprint. Each other path refuses. One residual is recorded against #2285: after T38 runs the BEP as its own OS user, the signal path refuses and T38 must carry the request across that split.
Ruled at 440cc95 against gominimal/arch@109541a48c31 by claude-opus-5-5.
bep --rotate-signing read ~/.minimal/bep and had no --control, so a bep serve on a custom control socket was out of its reach. Two proxies with sockets in one directory also shared one lock. The lock and the signing fingerprint now sit beside the control socket, named after it. bep --rotate-signing takes --control to find them.
|
Round 2 of 10 on the runtime's review findings, pushed as
Verify Review threads the coder judged:
Notes from the agent: Revise round 2. The bep serve state files are now |
There was a problem hiding this comment.
arch-review: PASS · the full ruling
The new commit keys the bep serve state files to the control socket path. It also adds --control to bep --rotate-signing. The rotation path does not change, and it still keeps every running box valid under the unchanged root (§6.10 line 839). The PASS on 440cc955 stands for the rest of the change. No conditions.
Ruled at ed2f4a0 against gominimal/arch@109541a48c31 by claude-opus-5-5.
Sleep at most one hour per step before the signing CA renewal, so time spent suspended does not delay it past expiry. Refuse to signal a pid from a lock file or directory that another user owns or can write.
|
Round 3 of 10 on the runtime's review findings, pushed as
Verify Review threads the coder judged:
A person can run Notes from the agent: Revise round 3. Both open threads were valid and are fixed in crates/bep/src/main.rs. The renewal thread now sleeps at most one hour (RENEW_CHECK) per step and reads the clock again after each step. |
|
The runtime will not revise this pull request again on its own: the diff is +582/-11 (593 lines), past 3x the plan's estimate of 180 lines (est: 120 code, 60 tests, as #2283 reads now), at round 3 (the last trigger was the runtime's review findings). A person decides now. Remove |
There was a problem hiding this comment.
arch-review: PASS · the full ruling
The compare from ed2f4a04 changes nothing that BEP-061 governs. The change sets private modes on the state directory and the lock file, and refuses a rotation request when another user can write them. The renewal thread now reads the clock every hour. Signing-CA rotation still keeps the unchanged root, as §6.10 line 839 requires. The PASS on the earlier head stands. Update the PR body note about the one-shot rotation, because the SIGUSR1 path replaced it.
Ruled at 12b4724 against gominimal/arch@109541a48c31 by claude-opus-5-5.
|
/revise Fix the two findings minimal-review posted at 12b4724 after round 3. The size cap is lifted for this walk, so the rounds continue.
Keep the rest of 12b4724 as it is: arch ruled PASS there. Generated by Claude Code |
bep serve opens its lock and staged fingerprint with O_NOFOLLOW, creates the staged file fresh, and refuses a control directory that another user owns or can write. It records the signing fingerprint before the lock names its pid, so bep --rotate-signing never reads a fingerprint from an earlier run as the one in use.
|
Round 1 of 10 on a /revise note, pushed as
Verify Review threads the coder judged:
A person can run Notes from the agent: The directive excerpt for the failed ci-success check shows only the runner preamble, so I cannot see the cause. On this branch, |
|
The runtime will not revise this pull request again on its own: the diff is +645/-11 (656 lines), past 3x the plan's estimate of 180 lines (est: 120 code, 60 tests, as #2283 reads now), at round 1 (the last trigger was a /revise note). A person decides now. Remove |
|
Revise round 1 ran against |
There was a problem hiding this comment.
Babysitter review at 893acc5.
Two files, both in the task's ca layer. ca.rs: renew_signing logs the old and new signing fingerprints and its doc says why a box keeps trusting the leaves. main.rs: bep --rotate-signing [--control PATH] asks the serving proxy over its lock file and SIGUSR1; bep serve claims <control>.lock and <control>.signing, installs the SIGUSR1 handler before the state names its pid, rotates on request and renews 30 days before expiry in hourly steps.
Checked against the diff:
- The signing CA stays under the unchanged root and key identifier (
Ca::renew_signing), so a box that holds the root trusts leaves from either signing CA; the ca.rs test pins old and new leaves against the box's trust store. ServeState::claimrefuses a directory another user owns or can write, opens the lock with O_NOFOLLOW, retries a busy lock for 2 s in 50 ms steps, truncates, records the current fingerprint and only then writes the pid.recordremoves the staged file and creates it fresh with O_NOFOLLOW before the rename.request_rotationreleases the probe before any wait, refuses a lock file or directory another user can write before it trusts the pid, reads the fingerprint before the signal, and reports success only when the fingerprint changes within 10 s. Every refusal names the situation and one action.- The renewal thread recomputes the wait from the clock every hour, stops when the signing CA ends with the root, retries hourly on a keychain error.
- Linux builds mark the macOS-only paths
allow(dead_code); the unit test drives claim, probe, symlink refusal, shared-lock retry, two sockets in one directory, a 0777 directory, a shared lock file, a silent proxy and a stopped proxy.
One nit for the owner, not a round: the fixed line Signing certificate rotated. Running boxes keep working; nothing inside them changes. joins two facts with a semicolon, which AGENTS.md's user-facing text rule asks to split. The task text pins those words, so changing it means changing #2283 first.
No findings. Arch PASS stands at d411e16; the compare to 893acc5 changes the refusal text only.
Generated by Claude Code
The test made the control socket directory with create_dir_all, so the umask set its mode. Under umask 002 the directory is group writable, ServeState::claim refuses it, and the test fails. Create it with mode 0700, as bep serve does.
|
Round 1 of 10 on a failed check suite, pushed as
Verify A person can run Notes from the agent: Revise round 1 (failed checks, no review threads). tests: the CI excerpt does not name the failing test. All 139 bep tests pass locally under |
|
The runtime will not revise this pull request again on its own: the diff is +727/-11 (738 lines), past 3x the plan's estimate of 180 lines (est: 120 code, 60 tests, as #2283 reads now), at round 1 (the last trigger was a failed check suite). A person decides now. Remove |
There was a problem hiding this comment.
arch-review: PASS · the full ruling
This push changes only the setup of one test. The test now makes the state directory private, as bep serve does. The privacy check in ServeState::claim is not changed, so the proof still tests the gate. The PASS on 893acc5 stays for the rest of the change, under §6.10 line 839. The three residuals keep their issues: #2509, #2510 and #2285.
Ruled at 25f41f2 against gominimal/arch@109541a48c31 by claude-opus-5-5.
|
Revise round 1 ran against |
|
/revise Fix the one MEDIUM minimal-review posted at 25f41f2 (thread 4366256381, correctness, The SIGUSR1 task calls Thread 4366256387 is the pid-reuse finding ruled not worth its code on thread 4366222826; leave it. Arch PASS stands at d411e16 and the compare since changes the refusal text and a test directory mode only. Generated by Claude Code |
The SIGUSR1 handler and the state claim at start now run in spawn_blocking. A slow or prompting keychain signature no longer stalls a tokio worker, and the claim retry no longer sleeps on one.
|
Round 1 of 10 on a /revise note, pushed as
Verify Review threads the coder judged:
A person can rotate the signing certificate under the same root with Notes from the agent: Revise round 1. The SIGUSR1 loop in |
|
The runtime will not revise this pull request again on its own: the diff is +865/-11 (876 lines), past 3x the plan's estimate of 180 lines (est: 120 code, 60 tests, as #2283 reads now), at round 1 (the last trigger was a /revise note). A person decides now. Remove |
There was a problem hiding this comment.
arch-review: PASS · the full ruling
This push moves the keychain rotation and the state claim off the runtime threads. The rotation itself does not change. The root stays the same, so running boxes see no change, as Gatehouse §6.10 line 839 requires. The new test checks the root, the new signing CA, and the recorded fingerprint, and it does not relax a gate. The earlier PASS stands, and the three residuals keep their issues.
Ruled at 091a4ad against gominimal/arch@109541a48c31 by claude-opus-5-5.
|
Revise round 1 ran against |
There was a problem hiding this comment.
arch-review: PASS · the full ruling
This head merges main into the branch. It does not change the bep code. The crates/bep diff is identical to the diff on 091a4ad9, which passed against Gatehouse §6.10 lines 821 and 839. The ruling on that head stands for this head. The residuals #2509, #2510 and #2285 stay open as filed. The e2e leg in #2510 is open, not passed.
Ruled at 810f4a4 against gominimal/arch@109541a48c31 by claude-opus-5-5.
There was a problem hiding this comment.
Babysitter verification at 810f4a4. This head is GitHub's merge of main fa31776 (crypto-bigint 0.7.7, the macOS reap) onto 091a4ad, which the earlier review covered; arch re-ruled PASS here and minimal-review confirms the head only merged the base. Zero open threads, cargo-deny green, ci-success green. Squash title feat(bep): rotate the signing CA under an unchanged root passes commitlint. Residuals #2509 and #2510 are scheduled as T51 and T52 in slice S9.
Generated by Claude Code
There was a problem hiding this comment.
Babysitter approval at 810f4a4: every merge gate passes at this head (arch PASS, zero open threads, every lane aggregator green, bot resolutions re-verified). Squash title feat(bep): rotate the signing CA under an unchanged root and the squashed message pass commitlint. Queued for squash merge.
Generated by Claude Code
…reap #2508 moved the control socket's default into default_control(). The stdin lifeline watch stays ahead of it in serve(). Claude-Session: https://claude.ai/code/session_01WhCC5TEBNzPeWgytRsU4rc
T36 — Rotate the signing certificate under an unchanged root
slice: S6
kind: task
covers: BEP-061
size: S
est: 120 code, 60 tests
executor: agent
difficulty: needs care
model: standard
diagnostics:
bep --rotate-signingprints "Signing certificate rotated. Running boxes keep working; nothing inside them changes." on stdout, pinned by signing_ca_rotation_invisible_to_boxesobservability: one info log line per rotation naming the old and new signing fingerprints
layers:
- ca:
- crates/bep/src/ca.rs: issues a new signing CA under the same root and key identifier
- crates/bep/src/main.rs:
bep --rotate-signing- crates/bep/src/main.rs:
bep serverenews its signing CA throughCa::renew_signingbeforesigning_expiry(), so a long-running proxy keeps signing leaves past the 365-day signing validity (arch C2 on #2493, residual filed here)test: cargo nextest run -p bep signing_ca_rotation_invisible_to_boxes
produces: none
consumes: none
proof: Rotate the signing certificate under the same root, and every running box keeps cloning and pushing unchanged.
depends on:
- blocked by #2276
Proof
An operator runs
bep --rotate-signingand gets a new signing certificate under the same root certificate and root key identifier. The command prints "Signing certificate rotated. Running boxes keep working; nothing inside them changes." A runningbep serverenews its signing CA 30 days before each expiry. Leaves from the old and the new signing CA both validate against the root that a box got at creation, so clone and push from running boxes continue unchanged. The test pins this at the certificate level only. No end-to-end git run was made.Notes from the agent: Rotation keeps the signing key. It re-issues the signing certificate through the existing
Ca::renew_signing, which now logs one info line per rotation with old_fingerprint and new_fingerprint.bep --replace-key signingalready replaces the key itself. Limitation: the proxy does not persist the signing certificate, so a one-shotbep --rotate-signingchanges no state that a runningbep servereads. The running proxy rotates through its own renewal thread. To signal a running proxy, a later task must change the control socket (crates/bep/src/control.rs), which is outside this task's layers. The renewal thread stops with a warning when the signing CA already ends with the root, because a renewal then gains nothing. It also stops when the root has expired, and it retries every hour on a keychain error. The task fixes the printed wording, and it contains a semicolon. The printed-text rule asks for no semicolon chains. I shipped the text exactly as the task gives it. The rotation path runs only on macOS, becauserunis macOS-only. On this Linux box the new code is covered by unit 8000 tests only.Tests
cargo nextest run -p bep signing_ca_rotation_invisible_to_boxesVerify
min task run agent-verifypassed ([tasks.verify]).Residuals
bep --rotate-signingsignals the serving proxy over a lock file and SIGUSR1; the control-socket command stays the durable path.Closes #2283
Generated by Claude Code