Skip to content

feat(attest): per-session require-v2 refuses a binding's v1 writes (aegis-2idcev) - #824

Merged
scbrown merged 1 commit into
mainfrom
malcolm/2idcev-require-v2
Oct 25, 2026
Merged

scbrown merged 1 commit into
mainfrom
malcolm/2idcev-require-v2

Conversation

@scbrown

@scbrown scbrown commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #823 (base malcolm/2idcev-receiver-controls), which is stacked on #820. Once #820 lands, the stack is retargeted to main in order so full CI runs.

Why

A quipu-write-v1 signature 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_another pins this as v1's documented limit.

This PR lets an operator require quipu-write-v2 for one session binding. It is deliberately never store-wide: today's sd signs every write under v1, so a global refusal would refuse all of them.

What

quipu attest require-v2|allow-v1 <session>
quipu attest register ... [--require-v2]

It mirrors allow_write gate by gate:

surface change
schema 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.
type SessionBinding.require_v2, #[serde(skip)], so a share manifest can neither set nor clear it.
reads both attestation_bindings() and binding_on
check check_binding, for write payloads only, next to the write-scope check. A v1 envelope from a require-v2 binding is refused as invalid. Share verification is unchanged.
operator the CLI verbs, the catalogue entries, attest list (require_v2= appended at the end of each line), and cli-sharing.md
respace the column is mapped (TermIdKind::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 PendingAttestation carries 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

test what it shows
require_v2_refuses_only_the_enrolled_binding_on_v1 The pair: an unenrolled v1 signer is accepted; the enrolled one is refused on v1 (invalid, message names write-v2); both are accepted on v2.
require_v2_refuses_v1_only_for_the_enrolled_signer (server, full stack) The same pair through handle → graph_create → finalize. The refusal writes nothing and spends no nonce. allow-v1 restores v1.
the_store_sets_and_clears_it_per_session_and_the_verifier_reads_it Store setter, both reads, unknown session refused.
a_binding_that_predates_the_column_migrates_as_v1_allowed A pre-column schema with a write-granted row reads require_v2=false, still verifies on v1, and accepts the setter.
the_flag_never_travels_in_a_serialized_binding Not emitted; a forged field is ignored.
require_v2_is_set_and_cleared_per_session_through_the_cli The real quipu binary 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.

sabotage red
check dropped the lib pair, store, migration, and the HTTP pair
refusal made global (flag ignored, always on) the pair, store, migration, HTTP pair, plus 7 existing v1 tests
migration column default 1 migration
ALTER skipped migration, plus the existing allow_write migration test
flag carried by serde (serde(default)) serde
store read ignores the column store, migration, HTTP pair

Gates

  • default lib 2868 passed / 0 failed;
  • server unit 248 / 0;
  • attest_cli, cli_help, cli_help_io, signed_writes_server and signed_writes_v2_server all pass.

Every run was under the HOME-escape guard (clean). Also passing:

  • clippy -D warnings -A missing-docs for full and shacl,owl;
  • the wasm32 lib check;
  • fmt, file size and the repository pendant scripts.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF

@scbrown
scbrown force-pushed the malcolm/2idcev-receiver-controls branch from 6b30452 to ec8dca1 Compare October 25, 2026 01:44
@scbrown
scbrown force-pushed the malcolm/2idcev-require-v2 branch from e8cf153 to 7f9c275 Compare October 25, 2026 01:44
@scbrown
scbrown force-pushed the malcolm/2idcev-receiver-controls branch from ec8dca1 to 7cb6c37 Compare October 25, 2026 02:15
@scbrown
scbrown force-pushed the malcolm/2idcev-require-v2 branch from 7f9c275 to 6ad582d Compare October 25, 2026 02:15
Base automatically changed from malcolm/2idcev-receiver-controls to main October 25, 2026 02:47
@scbrown
scbrown force-pushed the malcolm/2idcev-require-v2 branch from 6ad582d to 9219fa8 Compare October 25, 2026 03:18
…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
@scbrown
scbrown force-pushed the malcolm/2idcev-require-v2 branch from 9219fa8 to 5bd94bd Compare October 25, 2026 04:22
@scbrown

scbrown commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

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.

@scbrown

scbrown commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

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.

@scbrown
scbrown merged commit ac5b7e1 into main Oct 25, 2026
24 checks passed
@scbrown
scbrown deleted the malcolm/2idcev-require-v2 branch October 25, 2026 05:20
@scbrown scbrown mentioned this pull request Oct 25, 2026
scbrown added a commit that referenced this pull request Oct 25, 2026
…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
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.

1 participant