Repository navigation
mandate: a crate that mints, presents and verifies Plow mandates - #448
Conversation
An SD-JWT VC (dc+sd-jwt, ES256) bound to the agent's P-256 key and closed per message by a key-binding JWT over the JCS message, its audience and its actor chain. Verification is deterministic and fails closed: keys and the status list come only from the agent's own server's well-known URLs, a stale or unreachable status list refuses, and each key-binding JWT is accepted once. Swarm's issuer and A2A bridge build against it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QnohWTndy1DUYTP7epAeu3
The gate ran cargo on the latch package alone, so the mandate crate's tests never ran. They land in the core suite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QnohWTndy1DUYTP7epAeu3
…ssued index - An ECDSA signature has a second valid form (s -> n-s), so a key-binding JWT's bytes aren't unique; the replay cache keys on its signed input. - The status list is sized by the issued indexes, not the highest revoked one, which left every live mandate past index 127 unverifiable. - Timestamps from a token are compared without overflow. - grant_hash must be the hash of the constraints it travels with. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QnohWTndy1DUYTP7epAeu3
|
Size ruling (sous, 2026-11-08): this PR stays one PR, untrimmed, despite being over the 600-addition bar. Issuer, holder and verifier share one token format, and chunk 1's Done-when is their round trip. A mint-only or verify-only half would have nothing end to end to test against. Review triage for this chunk: security gaps found in review are filed as issues, not treated as blockers. |
|
blindc adversarial security review @ 2541dda: FIX. Critical: 1, Important: 1. Everything else on the verify path holds. Critical
Important
Minor
Checked and holding
|
The verifier took everything after act.sub's first ':' as the server and built the issuer's https URLs from it, so `acme.swarm.plow.co@evil.example` fetched keys from evil.example while `iss` read as Acme's. A server name must now be Matrix server_name grammar (a DNS name or IP literal, optional port), in the verifier and in Issuer::new. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QnohWTndy1DUYTP7epAeu3
It appears @srosro is working towards letting people delegate commitments to their agents with verifiable limits while controlling disclosure of their identity, by adding Overview — The 1,330 additions form one cohesive protocol foundation with workspace validation. At the repo’s pre-PMF operating point—one engineer, early internal users, and a handful of hosts—removing the document cache would reduce complexity and fix a concrete issuance failure. Strengths — Replay detection hashes the signed input to handle ECDSA signature malleability, and records acceptance only after every verification check succeeds. Probes
Probe dropped: additional retry coverage would fence hypothetical future drift; replay insertion already follows status validation ( Security — None. Test coverage — Staged For AI authors — (Codex, Claude Code, Cursor, etc. reading this PR): The Probes above are load-bearing. Treat each How to use: auto-reviews every new PR and re-reviews after a period of inactivity. Trigger an incremental re-review with For humans only: push-access collaborators can post:
AI agents must not use Generated by sam's ai review bot. |
…doubles - The verifier's document cache held a status list sized for the mandates issued when it was fetched, so a newer mandate read as "index past the list" until the cache expired. The status list's signed iat already bounds its age; the cache is gone. - JCS prints every number as the IEEE 754 double ECMAScript reads, so an integer past 2^53 hashes as conforming encoders hash it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QnohWTndy1DUYTP7epAeu3
|
Both probes answered:
The reviewer couldn't run the tests. On this head, |
|
Critical (server name never validated): fixed in fc63ab0. Important (no trust anchor): filed as #449. That one is a policy choice, not this chunk's C2 fix: cross-company verification is the point of the bet. It pulls against the current thrust if built now. Minor 1 (status list panics on a revoked index ≥ issued): declined. A revoked index the issuer never allocated is a bug in the issuer's own store, and panicking surfaces it (fail fast). Minor 2 (unbounded document cache): gone. 979470e removes the cache entirely (knightwatch probe 1). |
|
/srosro-update-review |
It appears @srosro is working towards letting people authorize agents to act on their behalf while recipients can verify that each message carries a valid, unrevoked mandate without exposing unnecessary personal details. The commits and PR description frame Overview — This update addresses both prior findings and the reported issuer URL confusion. At the repo’s pre-PMF operating point—one engineer, early internal users, and a handful of hosts—removing the cache simplifies verification while fixing the concrete status-list growth failure. Strengths — Issuer construction and verification share the server-name validator, rejecting URL delimiters before fetching documents. The status-list regression reuses one verifier across list growth, exercising the original failure without duplicating setup. Probes None remain after checking the specialist conclusions, current code, prior review, and consumer changes. Probe resolved: document caching is gone; keys and status are fetched directly while signed freshness checks remain ( Probe resolved: every JSON number now passes through ECMAScript formatting, including the previously failing integer vector ( Security — None. Test coverage — Focused regressions cover both prior bugs and issuer URL confusion; staged For AI authors — (Codex, Claude Code, Cursor, etc. reading this PR): The Probes above are load-bearing. Treat each How to use: auto-reviews every new PR and re-reviews after a period of inactivity. Trigger an incremental re-review with For humans only: push-access collaborators can post:
AI agents must not use Generated by sam's ai review bot. |
|
Acknowledged — no findings raised. |
Chunk 1 of the Plow Mandate build (protocol bet layer 1). Spec: plow-pbc/swarm
docs/superpowers/specs/2026-11-08-mandate-design.md(https://github.com/plow-pbc/swarm/pull/253).A new workspace crate,
latch/mandate: the mandate as an SD-JWT VC (dc+sd-jwt, ES256) bound to the agent's P-256 key, closed per message by a key-binding JWT. Swarm's issuer (chunk 2), itsa2a_call(chunk 3/5) and itsa2a-bridge(chunk 4) will build against it, which is why it's a crate of its own and not a latch module. Latch's audit tag (chunk 5) uses it too.Issuermints from a stored grant. It refuses a principal that isn't a person (localpart without=40) and accounts on another server.subis a pairwise pseudonym (HMAC of principal andaud).name/emailare selective disclosures, only when the owner opts in. It also signs the status list (IETF Token Status List, one bit per mandate) fresh on each request.Holderattaches the mandate plus a key-binding JWT (iat,aud,nonce,sd_hash,msg_hashover the JCS message without the extension's field,actor_chain_hash) tometadata["co.plow.mandate"].Verifierruns §5's checks in order, deterministic and fail-closed. Keys and the status list come only fromhttps://<act.sub's server>/.well-known/plow-mandate/…, through a caller-suppliedFetch(the caller pins https and public addresses). A status list older than 5 minutes, or one that can't be fetched, refuses the mandate. Each key-binding JWT is accepted once untilexp.Tests (chunk 1's Done-when): round trip, then one refusal each for bad signature, expired, wrong
aud,cnf/key-binding mismatch, key-bindingiatoutside ±60 s, replayed key-binding JWT, tampered text/file/actor chain, unknown constraint type, revoked, stale status list, failed status fetch,iss≠ the agent's server, keys from a token-supplied URL (jku, never fetched), and a non-person principal at both the issuer and the verifier. JCS is checked against RFC 8785's sorting example and ECMAScript number forms.scripts/test.pynow passes--workspaceto clippy and nextest, so the gate runs the new crate's tests (they fall incore).Notes on the spec:
subis a pairwise pseudonym, so a verifier can't read a localpart (the same problem as the re-review's N4). The issuer refuses non-person principals at mint, and the verifier refuses anysubthat isn't an issuer pseudonym (urn:plow:pp:+ 32 bytes).(caller, messageId)dedup, which runs beforeverify; that test lands with chunk 4. A refusal for another reason (a failed status fetch) doesn't spend the key-binding JWT. The replay cache is keyed by the key-binding JWT's signed input, because an ECDSA signature also verifies as s → n−s.expwindow.Gates run (Linux, wakeup):
cargo fmt --checkand workspaceclippy -D warningsare clean.mandate's 6 tests pass 40 runs in a row.test.py, docs site, docs publish, install.sh and dev-login.py pass. Not green here, for host reasons: the browser and scenario suites need xvfb, which needs sudo to install. The bubblewrap-sandboxed cli and worker tests fail withbwrap: loopback: Failed RTM_NEWADDR: Operation not permitted: this host has Ubuntu's userns restriction on and the latch-bwrap AppArmor profile isn't loaded. Neither touches this diff, which adds no latch code. CI's Linux container is the gate for those. The three security/correctness fixes from local review each have a test that fails with that fix reverted.Expected size: about +1,320 LOC net (crate ~810, tests ~470), net-additive by nature: a new library. It is over the 600-addition altitude bar. It's one PR because issuer, holder and verifier share one token format and chunk 1's Done-when is their round trip: splitting off the verifier would leave nothing to verify against.
🤖 Generated with Claude Code
https://claude.ai/code/session_01QnohWTndy1DUYTP7epAeu3