Repository navigation
Conversation
A quipu-write-v1 signature names no store, so a signed request is accepted by every store that trusts the key. sd now signs quipu-write-v2 by default: it reads the server's store id from GET /stats once, before anything is signed, pins it for the connection, and puts it in the signed message (the audience line last) and the envelope. The store then refuses a request signed for any other store. - The audience is never a caller choice. A /stats that fails or names no usable store_id refuses the connection; sd never falls back to v1. - `[quipu] signing_store_id` optionally pins the store: a server reporting another id is refused before anything is signed. - `[quipu] signing_version = "v1-legacy"` keeps the old format for a server older than write-v2. It cannot be combined with a pin. Both keys are user-level only, like the other signing_* keys. The bytes reproduce quipu's published tests/vectors/write-attestation-v2.json (copied unchanged), and the v1 vector still reproduces. BREAKING CHANGE: signed requests default to quipu-write-v2, so a quipu server older than write-v2 refuses them until `signing_version = "v1-legacy"` is set. `Signer::canonical_message`, `Signer::header` and `Signer::header_now` take the audience as a new last argument. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF
…cev) quipu-ai-v0.12.0 is the first quipu release that verifies quipu-write-v2 and reports store_id in GET /stats (checked: v0.11.1 has neither). Older servers refuse every signed request until signing_version = "v1-legacy". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF
…s (aegis-2idcev) The config.rs record claims storage routing resolves flags, environment, project then user config and refuses a conflict. The signing_version and signing_store_id keys leave that claim true; only the source hash moved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF
|
Repair at head The red:
I also ran the
Limit: these suites use no signer (no
|
Why
A
quipu-write-v1signature names no store, so a signed request is accepted by every store that trusts the key. quipu has acceptedquipu-write-v2(the signature binds the receiving store's id) since #388. sd still signed only v1.What
sd signs
quipu-write-v2by default:store_idfromGET /stats. That endpoint is an unauthenticated read in quipu. sd pins the id for the connection./identity, and signed reads such as/health) puts the id in the signed message, as theaudience=line LAST, and in the envelope, as theaudiencefield LAST.[quipu], user-level only)/statssigning_store_id = "urn:uuid:…"/statsreports another idsigning_version = "v1-legacy"/statsthat errors, names nostore_id, or names one with a control character, or over 256 bytes, refuses the connection with nothing signed.signing_*keys.Breaking:
signing_version = "v1-legacy"is set.Signer::{canonical_message, header, header_now}take the audience as a new last argument.[quipu]hasdeny_unknown_fields, so an older sd rejects a config that sets either new key. The default needs neither key.Evidence
Vectors:
tests/vectors/write-attestation-v2.jsonis copied unchanged (sha256 identical to the source).reproduces_quipus_published_v2_vector_byte_for_bytematches the canonical message, the signature, the envelope JSON and the header. The v1 vector test still passes.cryptographyEd25519 check rebuilt the v2 canonical message from the vector's rules, not by copying it. It reproduced all 9 derived fields and verified the signature; a mutated audience is refused.Against a real quipu-server, built from quipu #824, which adds per-session
require-v2. The session binding was set to REQUIRE v2, so a v1 write would be refused:v1-legacyinvalid, "session binding requires quipu-write-v2"allow-v1, thenv1-legacysigning_store_idsigning_store_idUnit tests (new):
an_unusable_stats_refuses_before_anything_is_signed: 500, 404, nostore_id, a control character, a pin mismatch. OnlyGET /statsis sent, and it carries no attestation.the_signing_version_defaults_to_v2_and_v1_needs_the_explicit_legacy_worda_store_id_that_could_break_the_message_is_refusedAdapted tests (intentional):
/statsfirst, and asserts it is unsigned;actor_principalmock answers/stats, and now asserts every signed update is v2 with the fixture audience;reproduce.pyanswers/stats(it still runs, rc 0).Sabotage, each restored from a saved copy with its sha256 checked:
/statsis unusableactor_principalGates:
cargo test: 407 passed / 0 failed, each binary under a HOME-escape guard (clean);--no-default-features: 160 / 0;cargo fmt --check;cargo clippy --all-targets -D warnings;clippy --no-default-features -D warnings.🤖 Generated with Claude Code
https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF