Repository navigation
feat(attest): per-session require-v2 refuses a binding's v1 writes (aegis-2idcev) - #824
Conversation
6b30452 to
ec8dca1
Compare
e8cf153 to
7f9c275
Compare
ec8dca1 to
7cb6c37
Compare
7f9c275 to
6ad582d
Compare
6ad582d to
9219fa8
Compare
…egis-2idcev) A write-v1 signature names no store, so a write-granted key's request is accepted by every store that trusts it. An operator can now require quipu-write-v2 for ONE session binding, so each of its writes is bound to the store it was signed for: quipu attest require-v2|allow-v1 <SESSION> quipu attest register ... --require-v2 It mirrors allow_write gate by gate: an additive require_v2 column (default 0, pragma-guarded ALTER, in CREATE TABLE for new stores), a serde(skip) field so no share manifest can set or clear it, both binding reads, the respace map, the CLI catalogue, and `attest list`. The check sits next to the write-scope check in check_binding and applies to write payloads only. It is never store-wide: sd signs every crew write under v1 today, so a global refusal would refuse them all. Existing bindings read 0. The flag is read when a request is verified; it is not re-read at the writer-lock settlement (PendingAttestation carries no envelope version), so a flip while a v1 write is queued applies from the next request. Paired controls: an unenrolled v1 signer is accepted while an enrolled one is refused on v1 (library and full HTTP stack), the enrolled binding is accepted on v2, allow-v1 restores v1, a pre-column store migrates as v1-allowed, the flag never serializes, and the CLI round-trips. Sabotage, each red: check dropped; refusal made global (also reds seven existing v1 tests); migration default 1; ALTER skipped; flag carried by serde; store read ignoring the column. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF
9219fa8 to
5bd94bd
Compare
|
SOURCE APPROVE at 5bd94bd after a full ten-path review. The per-session policy is loaded from the protected binding before nonce consumption, applies only to signed writes, and preserves v1 for unenrolled sessions. Serialization omits the local policy flag. The legacy migration, explicit insert/read paths, reversible CLI verbs and respace classification are consistent. Six independent SQLite controls using the exact source SQL passed under the HOME guard (157 paths and five resources clean). Exact-head hosted logs independently confirm the four new verifier tests, the HTTP enrollment pair and the CLI round trip pass. Native review binding is currently REFUSED because the current main has pending tests and no Release correctness verdict. Content 2c1ddcdd is source-approved only; binding, merge readiness and runtime enrollment remain pending. Retry binding after the current main is green; no second full audit is needed. |
|
Current binding update: normal native review accepted CONTENT 2c1ddcdddb7c74db621932b04770151d90954234cb93b6615ec231a61a69a36d for exact head 5bd94bd / base 44d11a0. The earlier main-pending refusal is retained. Main 44d is now complete green and normal --diff-bound merge preflight passes. Canonical :10–:20 UTC no-merge hold is active; no merge executed. Full source review and six independent SQLite controls remain as recorded in comment 6393699812. |
…to the S3b stream branch Restacked on #816's final head 1e4947f first (clean), then main's delta since that head. One real conflict, src/asof_share.rs: this branch extracted the per-graph checks into resolve_graphs (shared with the replication stream), and #831 changed the same loop to read frozen graphs from their packs and refuse the graph-metadata graph. Resolution: - resolve_graphs refuses the graph-metadata graph (both cut and stream, per the s24.1.1 ruling) and attached packs, and no longer refuses frozen graphs itself; - the cut reads frozen graphs from their packs exactly as #831 does (pack re-verified inside the transaction, frozen_member); - the stream keeps this branch's reviewed behaviour and refuses frozen graphs explicitly (refuse_frozen), now tested together with the metadata refusal. The derived rest-api.md witness hash is recomputed from the merged file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
Stacked on #823 (base
malcolm/2idcev-receiver-controls), which is stacked on #820. Once #820 lands, the stack is retargeted tomainin order so full CI runs.Why
A
quipu-write-v1signature names no store, so a write-granted key's request is accepted by every store that trusts the key.tests/signed_writes_v2_server.rs::baseline_a_v1_write_accepted_by_one_store_is_accepted_by_anotherpins this as v1's documented limit.This PR lets an operator require
quipu-write-v2for one session binding. It is deliberately never store-wide: today'ssdsigns every write under v1, so a global refusal would refuse all of them.What
It mirrors
allow_writegate by gate:attestation_bindings.require_v2 INTEGER NOT NULL DEFAULT 0 CHECK IN (0,1), added by a pragma-guarded ALTER and present in CREATE TABLE. Existing bindings read 0. Older binaries name their columns explicitly and ignore it.SessionBinding.require_v2,#[serde(skip)], so a share manifest can neither set nor clear it.attestation_bindings()andbinding_oncheck_binding, for write payloads only, next to the write-scope check. A v1 envelope from a require-v2 binding is refused asinvalid. Share verification is unchanged.attest list(require_v2=appended at the end of each line), andcli-sharing.mdTermIdKind::None)Known limit (stated in the docs). The flag is read when a request is verified. It is not re-read at the writer-lock settlement, because
PendingAttestationcarries no envelope version. Flipping the flag while a v1 write is queued therefore applies from the next request. Revocation remains the queued barrier and is re-read at settlement.Controls
require_v2_refuses_only_the_enrolled_binding_on_v1invalid, message names write-v2); both are accepted on v2.require_v2_refuses_v1_only_for_the_enrolled_signer(server, full stack)handle→graph_create→ finalize. The refusal writes nothing and spends no nonce.allow-v1restores v1.the_store_sets_and_clears_it_per_session_and_the_verifier_reads_ita_binding_that_predates_the_column_migrates_as_v1_allowedrequire_v2=false, still verifies on v1, and accepts the setter.the_flag_never_travels_in_a_serialized_bindingrequire_v2_is_set_and_cleared_per_session_through_the_cliquipubinary round-trip. It first failed because the CLI catalogue rejected the new verbs; the catalogue is fixed here.Sabotage
Each sabotaged file was restored from a saved copy, and its sha256 was checked after the run.
serde(default))Gates
attest_cli,cli_help,cli_help_io,signed_writes_serverandsigned_writes_v2_serverall pass.Every run was under the HOME-escape guard (clean). Also passing:
-D warnings -A missing-docsforfullandshacl,owl;🤖 Generated with Claude Code
https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF