8000 feat(bep): hold the proxy keys in the host keychain and publish their identity by minimal-foundry[bot] · Pull Request #2363 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

feat(bep): hold the proxy keys in the host keychain and publish their identity - #2363

Merged
norrietaylor merged 9 commits into
mainfrom
agent/2259-hold-the-proxy-s-keys-non-exportable-in-the-host
Oct 20, 2026
Merged

norrietaylor merged 9 commits into
mainfrom
agent/2259-hold-the-proxy-s-keys-non-exportable-in-the-host

Conversation

@minimal-foundry
@minimal-foundry minimal-foundry Bot commented Oct 19, 2026 •
Copy link
Copy Markdown
Contributor

T2 — Hold the proxy's keys non-exportable in the host keychain and publish their public halves

slice: S0
kind: task
covers: BEP-014, BEP-059, BEP-060, BEP-072
size: L
est: 400 code, 420 tests
executor: agent
difficulty: unknown ground
model: strongest
diagnostics: when the keychain refuses a key, bep refuses to start and prints "The keychain refused the proxy its key. Run bep --replace-key <role> to make a new one; boxes created before then must be re-created." on stderr, pinned by refused_key_names_the_replacement_command
observability: one info log line at start naming each key role and its fingerprint, and one warn line per replacement naming the role
layers:
- keys:
- crates/bep/src/keys.rs (new): a key-store seam; the macOS Keychain backend holds the sealing, root and signing keys non-exportable
- crates/bep/src/publish.rs (new): publishes and parses by role the sealing public key, fingerprints and root (BEP-072)
- crates/bep/src/main.rs (new): the bep binary with --replace-key <role>, which runs without the files a serving proxy reads
- crates/bep/src/lib.rs: module lines
- crates/bep/Cargo.toml: security-framework on macOS
- Cargo.lock: the keychain dependency
test: cargo nextest run -p bep signing_key_is_non_exportable_and_has_no_file
test: cargo nextest run -p bep sealing_and_root_keys_persist_non_exportable
test: cargo nextest run -p bep a_value_sealed_to_the_published_identity_unseals_under_the_keys
test: cargo nextest run -p bep a_published_identity_carrying_no_key_is_refused_by_role
test: cargo nextest run -p bep keys_never_regenerated_on_restart
test: cargo nextest run -p bep key_replacement_kills_outstanding_members
test: cargo nextest run -p bep refused_key_names_the_replacement_command
produces:
- crates/bep/src/keys.rs
- crates/bep/src/publish.rs
- crates/bep/src/main.rs (entry point)
consumes:
- crates/bep/src/seal.rs
proof: The proxy's keys survive a restart and appear in no file, and replacing the sealing key refuses every member sealed to the old one.
depends on:
- blocked by #2258
- assumes OQ4 (v1 binds the macOS Keychain behind a key-store seam; the Linux host stays unverified for the signing-key rule until a later backend)

Proof

The proxy loads its sealing, root and signing keys from the key store at each start and generates a key only when the store holds none. A restart gives the same fingerprints, and no file holds a private key. Only the public halves and fingerprints are published, to ~/.minimal/bep/identity.json. After bep --replace-key sealing, a value sealed to the old key no longer opens. The tests show this on this Linux box against an in-memory store that models the keychain. The macOS Keychain backend is written but was not compiled or run here.

Notes from the agent: 1. The macOS Keychain backend (keys.rs, module keychain, cfg(target_os = "macos")) was NOT compiled. This box has no darwin std. I checked each call by hand against security-framework 3.7.0, security-framework-sys 2.17.0 and core-foundation 0.10.1. The first macOS CI run is its first compile. It declares kSecAttrIsExtractable through its own extern block because security-framework-sys does not export it. After generation it checks that the key is not exportable, and it removes the key if the check fails.
2. seal::open takes a software ProxyKey (a p256 scalar), which a non-exportable keychain key cannot become. Round 6 added seal::open_held (with open_with and OpenError::Key) in crates/bep/src/seal.rs: it opens a sealed value through HeldKey::agree (raw ECDH, SecKeyCopyKeyExchangeResult on macOS), checks kid before the agreement, and fails closed on a store refusal. seal.rs is listed under consumes: only; the change is recorded under Layer exception below. The BEP-072 test still opens through the test-only in-memory key, which delegates to seal::open; the new seal.rs tests cover open_held against the memory store.
3. All seven tests run against keys::memory::MemoryStore (cfg(test)). This store models the keychain: it outlives a load, it can refuse a key, and it gives no export. On a real keychain, nothing tests non-exportability or the refusal path (OQ4). On Linux, bep prints that it needs the macOS Keychain and exits 1.
4. The root certificate …[truncated]

Tests

line exit tail
cargo nextest run -p bep signing_key_is_non_exportable_and_has_no_file 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-5620... Compiling bep v0.0.1 (/workbench/crates/bep) Finished 'test' profile [unoptimized] target(s) in 6.09s ──────────── Nextest run ID be2093db-4893-4397-a0ad-39743c4227bd with nextes
cargo nextest run -p bep sealing_and_root_keys_persist_non_exportable 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-709a... Finished 'test' profile [unoptimized] target(s) in 0.43s ──────────── Nextest run ID b9d277df-5d99-41d0-bfbc-b9603da0a912 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep a_value_sealed_to_the_published_identity_unseals_under_the_keys 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-5ba7... Finished 'test' profile [unoptimized] target(s) in 0.47s ──────────── Nextest run ID d682d043-26f5-4d79-9c35-879124107a35 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep a_published_identity_carrying_no_key_is_refused_by_role 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-b682... Finished 'test' profile [unoptimized] target(s) in 0.44s ──────────── Nextest run ID d23fa698-31ab-49d9-b460-d0e9deb178c1 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep keys_never_regenerated_on_restart 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-a52b... Finished 'test' profile [unoptimized] target(s) in 0.46s ──────────── Nextest run ID 4c9a27b9-281c-4a7c-9d14-ae07495f464e with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep key_replacement_kills_outstanding_members 0 Applying loadouts: default Running task runtime-implement-test-5 in session task-runtime-implement-test-5-488d... Finished 'test' profile [unoptimized] target(s) in 0.48s ──────────── Nextest run ID 49a515ca-0762-4c28-b06d-f5e9c825c4c5 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep refused_key_names_the_replacement_command 0 Applying loadouts: default Running task runtime-implement-test-6 in session task-runtime-implement-test-6-c988... Finished 'test' profile [unoptimized] target(s) in 0.39s ──────────── Nextest run ID faff18a7-c863-4d5f-bed7-539af80c4dc5 with nextest profile: default Starting 1 test across 2

Verify

min task run agent-verify passed ([tasks.verify]).

Closes #2259

Layer exception

Accepted by the epic owner on 2026-10-19: the justfile macOS scope and strict-scope edit (-p bep) is outside the task's planned layers; minimal-arch condition C3 on 7ad0c35 required it so the macOS lane compiles keys::keychain. Round 6 also changed crates/bep/src/seal.rs (open_held, open_with, OpenError::Key), which the task lists under consumes: only; minimal-arch condition C3 on f8aae06 asked for it to be recorded here, and the epic owner's 23:12Z decision to fix the HeldKey open variant in this PR covers it.

Residuals


Generated by Claude Code

Add a key-store seam with a macOS Keychain backend that generates the
sealing, root and signing keys non-exportable, loads them at each start
and replaces one only on `bep --replace-key <role>`. Publish each key's
public half and fingerprint, and seal from that file alone.
@coderabbitai
coderabbitai Bot commented Oct 19, 2026 •
Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 29dc118c-eb24-4a5d-8cb6-e4492517d72d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@minimal-foundry
minimal-foundry Bot commented Oct 19, 2026 •
Copy link
Copy Markdown
Contributor Author

Reviewed: 1 finding, none blocking at f8aae066f01a.

Address the comments on the diff.

Clobber: none found: the justfile change only adds -p bep to the macOS scopes.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 5 findings, 5 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/bep/src/keys.rs Resolved
Comment thread crates/bep/src/main.rs Resolved
Comment thread crates/bep/src/publish.rs Resolved
Comment thread crates/bep/src/keys.rs Resolved
Comment thread crates/bep/src/keys.rs Resolved
@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: CONDITIONS · 3 conditions · the full ruling

The change puts the keys in the right process and holds them non-exportable, as Gatehouse §6.10 lines 831 and 837 require. But keys::load regenerates any key that the store no longer holds on restart. Line 839 says the keys are never regenerated silently, and a replacement must not be a silent failure. C1 makes that start refuse and name bep --replace-key. No CI lane compiles the macOS keychain backend, and C2 adds it to the macOS scope. C3 files issues for the three items of work this change leaves open.

Ruled at 5e778c9 against gominimal/arch@3dab1097cda1 by claude-opus-5-5.

Comment thread crates/bep/src/keys.rs Resolved
Comment thread crates/bep/src/main.rs Resolved
Comment thread crates/bep/src/keys.rs Resolved
Comment thread crates/bep/src/keys.rs Resolved
Comment thread crates/bep/src/main.rs Resolved
A keychain search error that is not an access denial no longer advises
bep --replace-key. --replace-key publishes the new public key, and says
when the old key was deleted but no new key was made. The identity file
is written 0600 through a new staged file, and a file that other users
can change is refused.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 1 of 10 on the runtime's review findings, pushed as 6ffef6ccd083.

line exit tail
cargo nextest run -p bep signing_key_is_non_exportable_and_has_no_file 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-0013... Compiling bep v0.0.1 (/workbench/crates/bep) Finished 'test' profile [unoptimized] target(s) in 3.61s ──────────── Nextest run ID 5fa3c058-bb4a-4f5a-845c-ee00d79ef655 with nextes
cargo nextest run -p bep sealing_and_root_keys_persist_non_exportable 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-d81b... Finished 'test' profile [unoptimized] target(s) in 0.28s ──────────── Nextest run ID 7c5e42e4-8e0b-447f-80a9-d2c34b4ae563 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep a_value_sealed_to_the_published_identity_unseals_under_the_keys 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-1f25... Finished 'test' profile [unoptimized] target(s) in 0.28s ──────────── Nextest run ID 37212698-ca08-4ef0-b9dd-b580b6ede2c7 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep a_published_identity_carrying_no_key_is_refused_by_role 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-ca5b... Finished 'test' profile [unoptimized] target(s) in 0.29s ──────────── Nextest run ID 425c04bb-6ed3-47d7-8164-324daf5abdd6 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep keys_never_regenerated_on_restart 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-0f7c... Finished 'test' profile [unoptimized] target(s) in 0.26s ──────────── Nextest run ID 0a1b8bbb-040b-4fdf-9770-07d876a23aac with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep key_replacement_kills_outstanding_members 0 Applying loadouts: default Running task runtime-implement-test-5 in session task-runtime-implement-test-5-e4cc... Finished 'test' profile [unoptimized] target(s) in 0.28s ──────────── Nextest run ID 5455ba18-3ed5-4678-82d6-84e97cbb2960 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep refused_key_names_the_replacement_command 0 Applying loadouts: default Running task runtime-implement-test-6 in session task-runtime-implement-test-6-2d36... Finished 'test' profile [unoptimized] target(s) in 0.29s ──────────── Nextest run ID db88809c-3fe2-43b5-af27-a10a438a6548 with nextest profile: default Starting 1 test across 2

Verify min task run agent-verify passed.

Review threads the coder judged:

  • 👍 valid — crates/bep/src/keys.rs: Keychain::find maps only errSecAuthFailed, errSecNoAccessForItem and errSecUserCanceled to Refused. Other codes are KeyError::Store, and load returns StartError::Store, which gives no --replace-key advice.
  • 👍 valid — crates/bep/src/main.rs: --replace-key now loads the keys and republishes identity.json. If that fails, it prints that the file still names the old key and tells the operator to run bep.
  • 👍 valid — crates/bep/src/publish.rs:162: publish creates a new directory 0700. It removes a stale staged path and opens the staged file create_new with mode 0600. Identity::read refuses a file that group or other can write (PublishError::Writable).
  • 👍 valid — crates/bep/src/keys.rs: find now returns KeyError::Store when the item under the label is not a key reference, so load never generates a second key beside it.
  • 👍 valid — crates/bep/src/keys.rs: replace returns ReplaceError::Generate after the removal. bep then says the old key was deleted, that no new key was made, and to run --replace-key again. A warn line logs the removal.

The proxy's keys survive a restart and appear in no file, and replacing the sealing key refuses every member sealed to the old one. After this round, bep --replace-key <role> also publishes the new public key, so a mint stops sealing to the old key.

Notes from the agent: Revise round 1. All five findings were valid and are fixed in one commit. This box is Linux, so the macOS Keychain backend (the find error mapping and the non-key result guard) was not compiled or run here. A macOS lane must build it. The denial codes are local constants (errSecAuthFailed -25293, errSecNoAccessForItem -25243, errSecUserCanceled -128) because the security-framework-sys source was not available to confirm its exported names. A probe failure (key use refused at start) still maps to a refused key, as before. New tests: a_store_failure_is_not_a_refused_key, a_failed_generation_reports_the_old_key_removed, the_identity_file_is_the_owners_alone. A full cargo nextest run -p bep passed 22 of 22. The file reader checks the mode for group or other write, but not the owner uid, because the crate has no libc dependency.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 3 findings, 3 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/bep/src/main.rs Resolved
Comment thread crates/bep/src/publish.rs Resolved
Comment thread crates/bep/src/main.rs Resolved
@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: CONDITIONS · 2 conditions · the full ruling

The compare fixes the search path. A search error that is not a denial no longer tells the operator to replace a key. The probe path still does this. load() treats every probe failure as a refused key, and the keychain probe and agree call every error a refusal. Section 6.10 line 839 says a replacement kills every outstanding member, so a locked keychain must not lead to one. Make the change in C1. BEP-072 also asks for the root certificate, and this change publishes only the root public key. Name the issue that owns the certificate, as C2 says.

Ruled at 6ffef6c against gominimal/arch@3dab1097cda1 by claude-opus-5-5.

Comment thread crates/bep/src/keys.rs Resolved
Comment thread crates/bep/src/keys.rs Resolved
Comment thread crates/bep/src/keys.rs Resolved
Comment thread crates/bep/src/publish.rs Resolved
bep reads its published identity before it loads the keys. A role the
identity names and the keychain no longer holds stops the start and names
`bep --replace-key <role>`, so a later start never makes a key again
silently. A missing file is a first start, and any other read error
refuses the start.

`bep --replace-key` publishes the new key beside the other roles' public
halves from the store or the old file, so a second refused key does not
keep the deleted key published. The identity file must be owned by the
current user, and its directory must not be writable by other users.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 2 of 10 on the runtime's review findings, pushed as f79786447546.

line exit tail
cargo nextest run -p bep signing_key_is_non_exportable_and_has_no_file 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-52e6... Compiling bep v0.0.1 (/workbench/crates/bep) Finished 'test' profile [unoptimized] target(s) in 4.35s ──────────── Nextest run ID 972f304f-0f9b-450c-91fd-0509c64fd996 with nextes
cargo nextest run -p bep sealing_and_root_keys_persist_non_exportable 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-26c4... Finished 'test' profile [unoptimized] target(s) in 0.34s ──────────── Nextest run ID 186420c8-0f7f-4c67-b353-c5dde6525d5e with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep a_value_sealed_to_the_published_identity_unseals_under_the_keys 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-b1c8... Finished 'test' profile [unoptimized] target(s) in 0.32s ──────────── Nextest run ID 515e7bcf-63c7-4abe-a0c2-619b02406bba with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep a_published_identity_carrying_no_key_is_refused_by_role 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-bd9c... Finished 'test' profile [unoptimized] target(s) in 0.32s ──────────── Nextest run ID c867f6e3-e01a-42be-840b-277c92d71b0c with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep keys_never_regenerated_on_restart 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-9696... Finished 'test' profile [unoptimized] target(s) in 0.36s ──────────── Nextest run ID d22d9c70-5b8a-4e83-8cc2-a899d27c0d96 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep key_replacement_kills_outstanding_members 0 Applying loadouts: default Running task runtime-implement-test-5 in session task-runtime-implement-test-5-50e0... Finished 'test' profile [unoptimized] target(s) in 0.33s ──────────── Nextest run ID 8fb0f85b-40f4-489e-959b-86c73037d6a0 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep refused_key_names_the_replacement_command 0 Applying loadouts: default Running task runtime-implement-test-6 in session task-runtime-implement-test-6-6801... Finished 'test' profile [unoptimized] target(s) in 0.36s ──────────── Nextest run ID 1639a3f9-fe37-4ee4-b6de-f2df764d6eb8 with nextest profile: default Starting 1 test across 2

Verify min task run agent-verify passed.

Review threads the coder judged:

  • 👍 valid — crates/bep/src/main.rs: Fixed in 6ffef6c: --replace-key republishes identity.json. This round it publishes without a full key load (Identity::after_replacement).
  • 👍 valid — crates/bep/src/publish.rs:209: Fixed in 6ffef6c (0700 dir, 0600 create_new staged file, mode check on read). This round read also checks the owner and refuses a directory that other users can write.
  • 👍 valid — crates/bep/src/keys.rs: load now takes the published roles. A named role that the store does not hold refuses the start (StartError::Unusable, missing). Tested by a_published_key_the_store_lost_refuses_the_start.
  • 👍 valid — crates/bep/src/main.rs: main.rs start() reads the identity before keys::load. NotFound is a first start. Any other read or parse error refuses the start.
  • 👍 valid — crates/bep/src/keys.rs: After a failed generate, the identity still names the role, so the next start refuses and names --replace-key. Tested by a_failed_replacement_refuses_the_next_start.
  • 👍 valid — crates/bep/src/keys.rs:688: Holds: MemoryKey::exportable is false by construction. This session has no GitHub access, so it did not file the issue. The result notes ask a person to file it and name it in the PR body.
  • 👎 invalid — crates/bep/src/main.rs:49: Outside this task's layers: the change is in justfile (scope and strict-scope get -p bep on macOS). The result notes record it for a separate task.
  • 👍 valid — crates/bep/src/main.rs: --replace-key now publishes through Identity::after_replacement: the new key, plus other roles from the store or the old file. No full load. Tested by a_replacement_publishes_the_new_key_beside_a_refused_one.
  • 👍 valid — crates/bep/src/publish.rs:241: Identity::read refuses a file whose uid is not geteuid(), using the open handle. It also refuses a parent directory that others can write and that has no sticky bit.
  • 👍 valid — crates/bep/src/main.rs:53: The message now says bep runs only on macOS, and that neither bep nor bep --replace-key can run on this host.

The proxy's keys stay in the key store and survive a restart. The published identity holds only their public halves, and no file holds a private key. A restart that finds a published key missing refuses to start and names bep --replace-key <role>. It does not make the key again. bep --replace-key sealing publishes the new sealing key, and the proxy refuses every value sealed to the old key.

Notes from the agent: This round: keys::load takes the roles the published identity names. A named role that the store does not hold refuses the start (StartError::Unusable lists refused and missing roles). Generation runs only after every role is usable. main.rs reads the identity before load. A missing file is a first start, and any other read error refuses the start. --replace-key builds the new identity with Identity::after_replacement and does not do a full load. Identity::read also checks that the current user owns the file, and that other users cannot write its directory (a sticky directory is allowed). publish checks the directory too. This adds libc (workspace dep, cfg(unix)) to crates/bep/Cargo.toml and Cargo.lock, both in layers. Printed text: the refused-key line keeps the task's fixed wording, which has a semicolon. The new lost-key line uses two sentences. Not done: (1) the justfile scope/strict-scope change that adds -p bep on macOS. justfile is outside this task's layers, so that change needs its own task. (2) An issue for a test against keys::Keychain itself (C3). This session has no GitHub access, so a person must file it and name it in the PR body. The macOS Keychain branch is not compiled on this Linux box. just clippy-strict was not run.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 1 finding, 1 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/bep/src/publish.rs Resolved
@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: CONDITIONS · 2 conditions · the full ruling

The compare fixes the lost-key start, the republish after --replace-key, and the identity-file ownership checks. One silent-regeneration path is left. load() decides a first start from identity.json alone. A missing, moved or unreadable file lets the next start make a new sealing or root key with no operator command. Gatehouse §6.10 line 839 says these keys are never regenerated silently. C1 makes a store that holds some roles but not others refuse the start. C2 files the seal::open gap from the PR body as a residual with an issue.

Ruled at f797864 against gominimal/arch@3dab1097cda1 by claude-opus-5-5.

Comment thread crates/bep/src/keys.rs Resolved
Comment thread crates/bep/src/main.rs Resolved
Comment thread crates/bep/src/keys.rs Resolved
A probe or key use that fails for any reason other than an access denial
is now a store failure, so a locked keychain no longer advises a
replacement of a healthy key. A replacement no longer overwrites an
identity file it cannot read, and the root entry documents that the root
certificate is still to come.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 3 of 10 on the runtime's review findings, pushed as 8c4458894157.

line exit tail
cargo nextest run -p bep signing_key_is_non_exportable_and_has_no_file 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-db86... Compiling bep v0.0.1 (/workbench/crates/bep) Finished 'test' profile [unoptimized] target(s) in 5.21s ──────────── Nextest run ID 5b1fd912-e62f-474c-97c7-8597239aa3d4 with nextes
cargo nextest run -p bep sealing_and_root_keys_persist_non_exportable 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-7717... Finished 'test' profile [unoptimized] target(s) in 0.37s ──────────── Nextest run ID ac38554a-6571-436e-b5d6-a9ac73c34a14 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep a_value_sealed_to_the_published_identity_unseals_under_the_keys 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-da57... Finished 'test' profile [unoptimized] target(s) in 0.38s ──────────── Nextest run ID 90cc320b-141c-4e2f-967a-276a87be897a with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep a_published_identity_carrying_no_key_is_refused_by_role 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-3ad2... Finished 'test' profile [unoptimized] target(s) in 0.36s ──────────── Nextest run ID 02d01493-6e5d-42df-9567-d0948866b5d3 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep keys_never_regenerated_on_restart 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-6ee4... Finished 'test' profile [unoptimized] target(s) in 0.37s ──────────── Nextest run ID 2f16e188-f2e7-4be9-9f76-370a6b49d5ba with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep key_replacement_kills_outstanding_members 0 Applying loadouts: default Running task runtime-implement-test-5 in session task-runtime-implement-test-5-b629... Finished 'test' profile [unoptimized] target(s) in 0.36s ──────────── Nextest run ID 57aa5a29-2000-4028-a964-40bcb7a54f7a with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep refused_key_names_the_replacement_command 0 Applying loadouts: default Running task runtime-implement-test-6 in session task-runtime-implement-test-6-507b... Finished 'test' profile [unoptimized] target(s) in 0.36s ──────────── Nextest run ID b979b070-4596-4f7f-b2b3-4f62b2841263 with nextest profile: default Starting 1 test across 2

Verify min task run agent-verify passed.

Review threads the coder judged:

  • 👍 valid — crates/bep/src/keys.rs: Fixed in f797864. load refuses a published role that the store lost. This round changed nothing more for it.
  • 👍 valid — crates/bep/src/main.rs: Fixed in f797864. start() reads the identity before keys::load. This round, --replace-key also refuses to overwrite a file that is there and cannot be read.
  • 👍 valid — crates/bep/src/keys.rs: Fixed in f797864. After a failed generate, the identity still names the role, so the next start refuses and names --replace-key.
  • 👍 valid — crates/bep/src/keys.rs:720: The finding holds: MemoryKey::exportable is false by construction. This session has no GitHub access, so a person must file the issue for a test against keys::Keychain and name it in the PR body.
  • 👎 invalid — crates/bep/src/main.rs:49: The change is outside this task's layers. The justfile needs -p bep in the macOS scope and strict-scope. The result notes record it for a separate task.
  • 👍 valid — crates/bep/src/keys.rs:244: The probe arm in load now adds the role to refused only on KeyError::Refused. It returns StartError::Store on KeyError::Store. Tested by a_locked_store_at_probe_is_not_a_refused_key.
  • 👍 valid — crates/bep/src/keys.rs: key_exchange errors go through the new use_error helper. Only a code in DENIED is Refused, and any other code is Store. A peer key that is not accepted is now Store too.
  • 👍 valid — crates/bep/src/keys.rs: create_signature errors in probe now go through use_error with the DENIED classification, so a locked keychain gives StartError::Store.
  • 👍 valid — crates/bep/src/publish.rs:1: The module doc and Identity::root now say that the file holds the root public key only, and that the task covering BEP-013 adds the root certificate. The issue number is not known in this session.
  • 👍 valid — crates/bep/src/publish.rs:133: --replace-key now refuses to publish over an identity file that exists and cannot be read, so previous is None only when no file exists. after_replacement logs a warning for each role it leaves out.

The proxy's keys survive a restart and appear in no file. Replacing the sealing key refuses every member sealed to the old one. A start that finds the keychain locked now fails without the advice to replace a healthy key.

Notes from the agent: The macOS Keychain code (keychain module in keys.rs) is not compiled on this Linux box, and no CI lane compiles it (see thread PRRT_kwDOPdfcxM6uGVed). The new use_error helper assumes that SecKey::key_exchange and create_signature return CFError, and that CFError::code() gives the OSStatus. Both are true in security-framework 3.x, but a macOS build must confirm it. The DENIED codes for a key use are the same codes that find() uses. A real refused key at use time can return a different code. Nobody checked this against a rebuilt binary. A person must still file two issues: a test against keys::Keychain itself (C3), and adding -p bep to the macOS scope and strict-scope in the justfile (outside the layers). The publish.rs docs point to the task that covers BEP-013 for the root certificate. They do not give an issue number, because this session did not know it. A person can add the number to the PR body.

@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: PASS_WITH_CONDITIONS · 2 conditions · the full ruling

The compare fixes the two start-path faults. A locked keychain is now a store failure and not a refused key. A replacement no longer writes an identity that drops a role. The code conforms to Gatehouse §6.10 line 839. Two pieces of work stay undone and have no issue: the root certificate for BEP-072 and the seal.rs open variant over HeldKey::agree. File an issue for each and link it, as C1 and C2 say.

Ruled at 8c44588 against gominimal/arch@3dab1097cda1 by claude-opus-5-5.

Comment thread crates/bep/src/publish.rs Resolved
Comment thread crates/bep/src/keys.rs Resolved
@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 1 finding, 1 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/bep/src/keys.rs Resolved
A start with no identity file, or a file that does not name a role,
generated that role's key again when the store still held the other
keys. A first start finds no key at all, so a missing key beside held
keys was lost. The start now refuses it and names --replace-key.
@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 1 finding, 1 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/bep/src/keys.rs Resolved
@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: PASS_WITH_CONDITIONS · 3 conditions · the full ruling

The compare closes the silent-regeneration path. A lost key beside held keys now refuses the start, so BEP-060 conforms to Gatehouse §6.10 line 839. Three items stay open, and no issue tracks them. C1 and C2 tell you to open the issues for the root certificate and for the keychain-key seal::open variant, and to link them. C3 adds bep to the macOS just scope. Today no lane compiles the keychain backend that line 839 depends on.

Ruled at 7ad0c35 against gominimal/arch@3dab1097cda1 by claude-opus-5-5.

Comment thread crates/bep/src/publish.rs Resolved
Comment thread crates/bep/src/keys.rs Resolved
Comment thread crates/bep/src/keys.rs Resolved
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 4 of 10 on the runtime's review findings, pushed as 7ad0c35e8184.

line exit tail
cargo nextest run -p bep signing_key_is_non_exportable_and_has_no_file 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-9721... Compiling bep v0.0.1 (/workbench/crates/bep) Finished 'test' profile [unoptimized] target(s) in 8.82s ──────────── Nextest run ID 6a761c48-9b8e-4d39-8fa4-1ad823fc3364 with nextes
cargo nextest run -p bep sealing_and_root_keys_persist_non_exportable 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-b01e... Finished 'test' profile [unoptimized] target(s) in 0.64s ──────────── Nextest run ID 9276d59f-0e79-4ddd-b5ee-3166f17112ab with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep a_value_sealed_to_the_published_identity_unseals_under_the_keys 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-4ffc... Finished 'test' profile [unoptimized] target(s) in 0.69s ──────────── Nextest run ID f468abcf-498e-4106-93ec-3bb3afc9290b with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep a_published_identity_carrying_no_key_is_refused_by_role 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-22c5... Finished 'test' profile [unoptimized] target(s) in 0.58s ──────────── Nextest run ID 941934bd-17a0-4d20-9790-bc1fc2ba1881 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep keys_never_regenerated_on_restart 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-537e... Finished 'test' profile [unoptimized] target(s) in 0.54s ──────────── Nextest run ID 1699256a-4eaa-4ca6-994c-914c6cd5acf1 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep key_replacement_kills_outstanding_members 0 Applying loadouts: default Running task runtime-implement-test-5 in session task-runtime-implement-test-5-fe95... Finished 'test' profile [unoptimized] target(s) in 0.54s ──────────── Nextest run ID bea81ed5-e204-442e-b48a-ce8b9012b080 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep refused_key_names_the_replacement_command 0 Applying loadouts: default Running task runtime-implement-test-6 in session task-runtime-implement-test-6-bb8d... Finished 'test' profile [unoptimized] target(s) in 0.66s ──────────── Nextest run ID baaca948-f1df-4de7-a6c9-2b20b2f34d48 with nextest profile: default Starting 1 test across 2

Verify min task run agent-verify passed.

Review threads the coder judged:

  • 👍 valid — crates/bep/src/keys.rs: Fixed in f797864. This round load also refuses a lost key that the identity does not name, when the store holds the other keys.
  • 👍 valid — crates/bep/src/main.rs: Fixed in f797864: start() reads the identity before keys::load and refuses on any read error except NotFound. This round changed nothing more for it.
  • 👍 valid — crates/bep/src/keys.rs: Fixed in f797864. This round the next start refuses after a failed replacement even with no identity file. Tested by a_lost_key_with_no_identity_file_refuses_the_start.
  • 👎 invalid — crates/bep/src/keys.rs:734: The premise holds: MemoryKey::exportable is false by construction. The fix is a GitHub issue, and this session has no GitHub access. A person must file it and name it in the PR body. No code changed for it.
  • 👎 invalid — crates/bep/src/main.rs:49: Outside this task's layers: the change is in justfile (-p bep in the macOS scope and strict-scope). The result notes record it for a separate task.
  • 👍 valid — crates/bep/src/keys.rs:244: Fixed in 8c44588: the probe arm adds the role to refused only on KeyError::Refused, and returns StartError::Store on KeyError::Store.
  • 👍 valid — crates/bep/src/keys.rs: Fixed in 8c44588: key_exchange errors go through use_error. Only a DENIED code is Refused.
  • 👍 valid — crates/bep/src/keys.rs: Fixed in 8c44588: create_signature errors in probe go through use_error, so a locked keychain gives StartError::Store.
  • 👍 valid — crates/bep/src/publish.rs:1: Fixed in 8c44588: the module doc and Identity::root say the file holds the root public key only. The issue URL is still owed (see the later thread on publish.rs:3).
  • 👍 valid — crates/bep/src/keys.rs:258: load now treats Ok(None) as missing when the store holds or refuses a key for any other role, whatever the identity file names.
  • 👍 valid — crates/bep/src/main.rs: Fixed in 8c44588: replace_key keeps a read error other than NotFound, leaves the file as it is and exits non-zero. load now also refuses the lost role with no file.
  • 👍 valid — crates/bep/src/keys.rs: The test now asserts Unusable { missing: [Root] } and generated() == 3, with the identity naming only sealing and with no identity.
  • 👎 invalid — crates/bep/src/publish.rs:3: The fix is a new GitHub issue for the BEP-072 root certificate. This session has no GitHub access and cannot file it or know its URL. A person must file it, then the URL goes in publish.rs and on Identity::root.
  • 👎 invalid — crates/bep/src/keys.rs:122: The premise holds, but the open variant is in seal.rs, which is outside this task's layers. The fix also needs a GitHub issue, and this session cannot file one. A person must file it and add its URL at HeldKey::agree.
  • 👍 valid — crates/bep/src/keys.rs:258: load now refuses a missing role when the store holds a key for any other role, so a deleted or moved identity file cannot cause a silent regeneration.

The proxy's keys survive a restart and appear in no file. Replacing the sealing key refuses every member sealed to the old one. A start now never makes a lost key again, even with no identity file: when the keychain holds the other keys, bep refuses and names bep --replace-key <role>.

Notes from the agent: Round 4. keys::load now treats a role the store does not hold as lost when the store holds or refuses a key for any other role. The identity file no longer decides this alone. The whole bep suite passed (27 tests). These items stay open for a person, because this session has no GitHub access and they are outside the layers: (1) file an issue for the BEP-072 root certificate (BEP-013 work) and put its URL in publish.rs and on Identity::root. (2) file an issue for a seal.rs open variant that takes a key agreement, and put its URL at keys.rs HeldKey::agree. (3) file an issue for a non-exportability test against keys::Keychain. (4) add -p bep to the macOS scope and strict-scope in justfile. The macOS Keychain backend is still not compiled by any lane that this box can run.

Copy link
Copy Markdown
Member

/revise minimal-arch ruled PASS_WITH_CONDITIONS on 7ad0c35 (check run 118318117390). One condition is code and is yours this round; the other two wait on issue numbers that a person has to file, so leave them.

C3, in justfile: change the macOS values of scope (line 36) and strict-scope (line 40) from -p minvmd -p boxes to -p minvmd -p boxes -p bep. The macOS lane runs just test at that scope, so today no lane compiles keys::keychain or runs bep's tests on macOS, and the PR body's claim that the first macOS CI run compiles the backend is false at this head. Make the change, then fix whatever the macOS lane reports on the first compile of keys::keychain against security-framework. The justfile is outside the task's layers; the ruling requires this one-line scope change, so make it and say so in the commit body.

C1 and C2 (issue URLs in the doc comments on Identity::root in crates/bep/src/publish.rs, on HeldKey::agree in crates/bep/src/keys.rs, and under Residuals in the PR body) are not yours: the issues do not exist yet and you cannot file them. A person will file them and the next round will carry the URLs.


Generated by Claude Code

The macOS lane runs `just test` and `just clippy` at the `scope`
variable, which named only minvmd and boxes. No lane compiled the
macOS Keychain backend in crates/bep/src/keys.rs or ran bep's tests on
macOS. Add `-p bep` to `scope` and `strict-scope`.

The justfile is outside the task's layers. The architecture ruling on
7ad0c35 (condition C3) requires this one-line scope change.
On a first start, load generated the three keys in turn. When a later
generation failed, the earlier keys stayed in the store. The next start
found one role and not the others, and refused them as lost keys with
advice to re-create boxes that never existed. Remove the keys that the
failed start generated, so the next start is a first start again.
@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: PASS_WITH_CONDITIONS · 2 conditions · the full ruling

The compare conforms to §6.10 line 839. roll_back() removes only the keys that a failed first start generated, before any key is published. That is not a silent regeneration. A failed removal fails closed. The macOS scope now compiles the Keychain backend, and that meets C3 of the last ruling. Two stated residuals have no issue: the open path for a keychain-held sealing key (C1), and a proof on a real keychain (C2). File each issue and name it in the PR body.

Ruled at 909b8a7 against gominimal/arch@3dab1097cda1 by claude-opus-5-5.

Comment thread crates/bep/src/keys.rs Resolved
Comment thread crates/bep/src/main.rs Resolved
@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 1 finding, 1 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/bep/src/keys.rs Resolved
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 5 of 10 on a /revise note, pushed as 909b8a70dc53.

line exit tail
cargo nextest run -p bep signing_key_is_non_exportable_and_has_no_file 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-321e... Compiling bep v0.0.1 (/workbench/crates/bep) Finished 'test' profile [unoptimized] target(s) in 7.81s ──────────── Nextest run ID 843ef1c8-9eb1-4a24-addf-6779cca4034d with nextes
cargo nextest run -p bep sealing_and_root_keys_persist_non_exportable 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-3d45... Finished 'test' profile [unoptimized] target(s) in 0.70s ──────────── Nextest run ID de45f37b-b847-42e4-9c8e-40dd2c8c1757 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep a_value_sealed_to_the_published_identity_unseals_under_the_keys 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-c4e3... Finished 'test' profile [unoptimized] target(s) in 0.53s ──────────── Nextest run ID 36a098a3-bf57-4990-b433-520a0f5a08cf with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep a_published_identity_carrying_no_key_is_refused_by_role 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-b098... Finished 'test' profile [unoptimized] target(s) in 0.62s ──────────── Nextest run ID fb6457bf-c354-4a6e-af42-c88302d1aae3 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep keys_never_regenerated_on_restart 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-201e... Finished 'test' profile [unoptimized] target(s) in 0.61s ──────────── Nextest run ID b98d09f4-bccf-4aba-8270-651b8d0c35dc with nextest profile: default Starting 2 tests across 2
cargo nextest run -p bep key_replacement_kills_outstanding_members 0 Applying loadouts: default Running task runtime-implement-test-5 in session task-runtime-implement-test-5-04e7... Finished 'test' profile [unoptimized] target(s) in 0.55s ──────────── Nextest run ID 2b0a50e7-4fc5-44c2-8daa-7721eb9733a6 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep refused_key_names_the_replacement_command 0 Applying loadouts: default Running task runtime-implement-test-6 in session task-runtime-implement-test-6-43ae... Finished 'test' profile [unoptimized] target(s) in 0.69s ──────────── Nextest run ID fd9aee4c-078b-4688-877d-2ca3f4cb5f46 with nextest profile: default Starting 1 test across 2

Verify min task run agent-verify passed.

Outside the task's layers::

  • justfile

Converted to a draft: something above needs a person before review.

Review threads the coder judged:

  • 👍 valid — crates/bep/src/keys.rs:303: load now removes the keys that a failed first start generated, so the next start is a first start again. The test keys_never_regenerated_on_restart_after_a_failed_first_start pins this.
  • 👍 valid — crates/bep/src/publish.rs:55: The finding holds, but nothing changed this round. The issue for the BEP-013 root certificate task does not exist yet, and I cannot file it. When a person files it, the next round will put its URL on Identity::root, in the module doc and under Residuals.
  • 👍 valid — crates/bep/src/keys.rs:122: The finding holds, but nothing changed this round. The issue for an open variant that uses agree does not exist yet, and I cannot file it. When a person files it, the next round will put its URL on HeldKey::agree and under Residuals.
  • 👍 valid — crates/bep/src/keys.rs:396: Added -p bep to the macOS scope and strict-scope in the justfile. I checked keys::keychain against the security-framework 3.7.0 API source but did not compile it, because this box has no macOS target.

The proxy's keys survive a restart and are in no file. Replacing the sealing key refuses every member sealed to the old key. A first start that fails part way leaves no key behind, so the next start is a first start and not a refused lost key. On macOS, just test and just clippy now include bep, so the macOS lane compiles the Keychain backend.

Notes from the agent: C3: justfile lines 36 and 40 now say -p minvmd -p boxes -p bep on macOS. The justfile is outside the task's layers. The ruling requires this change, and the commit body says so. This box is Linux x86_64 and has no macOS target, so I did not compile keys::keychain. Instead I checked each call against the source of security-framework 3.7.0, security-framework-sys 2.17.0 and core-foundation 0.10.1, which are the versions in Cargo.lock. I found no mismatch. The calls I checked are ItemSearchOptions, SearchResult and Reference, SecKey generate, key_exchange, create_signature and external_representation, the Algorithm variants, SecKeyCreateWithData, Error::code as OSStatus, CFError::code as CFIndex, and to_untyped. I found no Linux-only code in bep: publish.rs uses only cfg(unix) APIs. The first macOS CI run is still the first real compile, so whatever it reports belongs to the next round. C1 and C2 need issue URLs that a person files. I made no change for them. Foundry thread: load now removes the keys that a failed first start generated (roll_back), and a new test pins this behaviour. I did not run just clippy-strict.

Copy link
Copy Markdown
Member

/revise The epic owner decided (2026-10-19) to fix in this PR the one of minimal-arch's residuals that fits, and filed the other two. This round:

  1. seal::open over a held key. Add an open variant in crates/bep/src/seal.rs that takes a key agreement (&impl HeldKey, through HeldKey::agree at crates/bep/src/keys.rs line 122) in place of a software ProxyKey, so the proxy can open a sealed value with its non-exportable keychain sealing key. Keep the existing open for the software key; share the KDF and AEAD path. Pin it with a test that seals under a MemoryStore key and opens through the agreement path, and one that a wrong key's agreement fails the tag.

  2. Doc-comment links. Put T43 — Issue and publish the un-enrolled host root certificate and hand it to each grant box #2437 in the doc comment on Identity::root in crates/bep/src/publish.rs (lines 55-57) and in the module doc (lines 3-4) as the deferred root-certificate issuance. The real-keychain proof is filed as Prove non-exportable key custody on a real keychain (BEP, Gatehouse §6.10) #2436; both are listed under Residuals in the PR body already, so leave the body alone.

Then resolve the arch threads on publish.rs and keys.rs line 122 with a one-line pointer to the commit or the issue.


Generated by Claude Code

Add seal::open_held, which opens a value with the key agreement of a
key the store holds, so the keychain sealing key opens sealed values
without its private half. It shares the KDF and AEAD path with open.

Remove a keychain key whose public half does not read, so a failed
first start leaves no key behind.
@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: PASS_WITH_CONDITIONS · 3 conditions · the full ruling

The compare conforms to Gatehouse §6.10. seal::open_held opens a sealed value through the held key's agreement, so the sealing key stays non-exportable (line 837). The keychain cleanup fails closed, and the root certificate deferral now names #2437. C1 and C2 are the filed residuals #2436 and #2437. C3 asks you to record seal.rs as a layer exception and to correct agent note 2 in the PR body.

Ruled at f8aae06 against gominimal/arch@3dab1097cda1 by claude-opus-5-5.

Comment thread crates/bep/src/seal.rs Resolved
Comment thread crates/bep/src/publish.rs Resolved
@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 1 finding, 1 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/bep/src/publish.rs Resolved
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 6 of 10 on a /revise note, pushed as f8aae066f01a.

line exit tail
cargo nextest run -p bep signing_key_is_non_exportable_and_has_no_file 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-64bf... Compiling bep v0.0.1 (/workbench/crates/bep) Finished 'test' profile [unoptimized] target(s) in 8.60s ──────────── Nextest run ID a05d87bc-0ad6-4cb7-84a7-e1bfa0d56ac0 with nextes
cargo nextest run -p bep sealing_and_root_keys_persist_non_exportable 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-07b4... Finished 'test' profile [unoptimized] target(s) in 0.88s ──────────── Nextest run ID d1f75ee9-e4f3-4d98-bd77-3fc404a80a86 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep a_value_sealed_to_the_published_identity_unseals_under_the_keys 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-f908... Finished 'test' profile [unoptimized] target(s) in 0.77s ──────────── Nextest run ID 70875a76-ea14-4db3-8faf-54a2cfb57c2b with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep a_published_identity_carrying_no_key_is_refused_by_role 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-9a44... Finished 'test' profile [unoptimized] target(s) in 0.54s ──────────── Nextest run ID 36a9ee19-8763-4be5-b17a-af098db01335 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep keys_never_regenerated_on_restart 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-bd95... Finished 'test' profile [unoptimized] target(s) in 0.70s ──────────── Nextest run ID 46085020-493d-4b66-8ea5-fbf5a8c7f301 with nextest profile: default Starting 2 tests across 2
cargo nextest run -p bep key_replacement_kills_outstanding_members 0 Applying loadouts: default Running task runtime-implement-test-5 in session task-runtime-implement-test-5-41b1... Finished 'test' profile [unoptimized] target(s) in 0.82s ──────────── Nextest run ID 1e6f5ad1-ea72-4703-98f6-09769d293423 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep refused_key_names_the_replacement_command 0 Applying loadouts: default Running task runtime-implement-test-6 in session task-runtime-implement-test-6-866a... Finished 'test' profile [unoptimized] target(s) in 0.75s ──────────── Nextest run ID 0ae8e780-1c32-4f64-9f44-57fd4ab52b69 with nextest profile: default Starting 1 test across 2

Verify min task run agent-verify passed.

Outside the task's layers::

  • crates/bep/src/seal.rs
  • justfile

Converted to a draft: something above needs a person before review.

Review threads the coder judged:

  • 👍 valid — crates/bep/src/keys.rs:124: Added seal::open_held, which opens through HeldKey::agree and shares the KDF and AEAD path with open (commit f549cda). The HeldKey::agree doc now points to it.
  • 👍 valid — crates/bep/src/main.rs:49: The real-keychain proof and the Linux backend are filed as Prove non-exportable key custody on a real keychain (BEP, Gatehouse §6.10) #2436 and listed under Residuals in the PR body. No code change this round.
  • 👍 valid — PRRT_kwDOPdfcxM6uIFVM: Keychain::generate now removes the stored key when KeychainKey::new fails, as the exportable branch does (commit f549cda). (not posted on the thread: not an open thread on the pull request)

The proxy's keys survive a restart and appear in no file. Replacing the sealing key refuses every member sealed to the old one. The proxy can now also open a sealed value through its held sealing key's agreement (seal::open_held), so the private half never leaves the store.

Notes from the agent: Revise round 6. The directive asked for a change in crates/bep/src/seal.rs, which is not in the task's layers. The task lists it under consumes. The change adds seal::open_held and an OpenError::Key variant. It moves the shared KDF and AEAD path of open into a private open_with. MemoryKey::open in the test store now opens through open_held and does not rebuild a software key. Tests: a value sealed to a MemoryStore key opens through the agreement. A held key that reports the right kid but agrees under another key fails the tag (NotOpenable). A key that the store refuses gives OpenError::Key(Refused). publish.rs now links issue 2437 in the module doc and on Identity::root. The Keychain::generate fix (remove the stored key when KeychainKey::new fails) is in the macOS-only module. This Linux box did not compile that module, so the macOS lane must compile it. I did not run just clippy-strict.

@norrietaylor norrietaylor left a comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Babysitter review of f8aae06 (T2: the proxy's keys in the host keychain, the published identity, bep --replace-key, and seal::open_held). Read as a COMMENT review so it starts no coder round.

What I checked. keys::load finds each role, probes a found key so a refusal shows at start, treats a lost key as lost whenever the store holds or refuses any other role (whatever the identity file says), generates only after every role is known usable, and rolls back a partial first start. A locked store is a Store error, never replacement advice. replace removes before it generates, so no value sealed to the old key opens after it returns, and a failed generation leaves the identity naming the role so the next start refuses rather than regenerates. On macOS the private attrs set kSecAttrIsPermanent and kSecAttrIsExtractable = false, generate removes a key the keychain made exportable, find and the use paths classify only errSecAuthFailed, errSecNoAccessForItem and errSecUserCanceled as refusals. publish writes the identity 0600 under a 0700 directory through a create_new staged file, and read refuses a file another user owns or can write and a directory others can change without the sticky bit. Identity::key checks the kid against the point. seal::open_held keeps the kid check before the agreement, maps a store refusal to OpenError::Key, and the mismatched-agreement test pins the tag failure as NotOpenable. The macOS backend is unproven on a real keychain, which is filed as #2436, and the root certificate is #2437. No blocking finding from me.

Findings, none blocking

  1. Identity::after_replacement (publish.rs): the comment "The next start treats this role as a first start" is wrong since round 4; keys::load refuses a lost role whenever the store holds any other key. The runtime raised this as LOW. One comment line; it rides the next push if one comes.

  2. replace_key in main.rs: when the identity file exists but Identity::read fails (writable, open directory, malformed), the key has already been replaced and the message tells the operator to run bep after fixing the file. That is the right order (the old key must go first), but the message should say the new key is already in use and only the published copy is stale, so nobody runs --replace-key a second time and kills a second generation of members.

  3. publish() creates the directory before check_dir runs, so a pre-existing world-writable directory gets a file written into it? No: create on an existing directory is a no-op and check_dir then refuses before the staged file is created. Fine as is; noting that I checked it.

Merge. With #2436 and #2437 filed and the body recording the seal.rs layer exception, I read the PASS_WITH_CONDITIONS ruling 118366526304 the way the epic owner read T1's: conditions met, residuals open at merge.


Generated by Claude Code

@norrietaylor norrietaylor changed the title T2 — Hold the proxy's keys non-exportable in the host keychain and publish their public halves feat(bep): hold the proxy keys in the host keychain and publish their identity Oct 20, 2026
@norrietaylor norrietaylor left a comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Babysitter approval of f8aae06 for the epic owner. Gate: minimal-arch PASS_WITH_CONDITIONS 118366526304 with its conditions met the way the epic owner read T1's (C1 and C2 are the filed residuals #2436 and #2437, which the ruling says may stay open at merge; C3, the body, is done), runtime review concluded with no blocking finding, every review thread resolved and re-verified, all lane aggregators and the CLA status green on the head, squash subject lints as Conventional Commits.


Generated by Claude Code

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.

T2 — Hold the proxy's keys non-exportable in the host keychain and publish their public halves

1 participant

0