Skip to content

feat(cli): codespar mandate verify — offline V3 dual-signature verification#114

Merged
fabianocruz merged 4 commits into
mainfrom
feat/mandate-verify-v3
Jul 5, 2026
Merged

feat(cli): codespar mandate verify — offline V3 dual-signature verification#114
fabianocruz merged 4 commits into
mainfrom
feat/mandate-verify-v3

Conversation

@fabianocruz

Copy link
Copy Markdown
Member

Third-party verification of V3 dual-signed mandates from the CLI, with zero new runtime deps (node:crypto only):

  • packages/cli/src/mandate-codec.ts: token decode, canonical signing-string reconstruction (V3 14-field; also handles V2 12-field), Ed25519 verify with a raw 32-byte pubkey (RFC 8410 SPKI wrap). Byte-locked against the enterprise canonical fixture (copied into tests) so any drift fails loud.
  • packages/cli/src/did.ts: did:web resolver — standard URL mapping first, api.codespar.dev encoded-DID fallback; extracts Ed25519 keys from JsonWebKey2020 documents.
  • codespar mandate verify <token>: offline mode (--agent-pubkey/--issuer-pubkey, no network, no API key) and network mode (resolves pubkeys via did:web, still no API key). Exit code tracks signature verification: verified = at least one Ed25519 signature verified and none failed; in network mode an unresolvable key is a failure. --json supported. expires_at shown with an [expired] flag, informational only.

Tests: 45 passed (22 new) — byte-lock, tamper detection, wrong-key, bad-kid, decode failures; CLI driven end-to-end for real exit codes. Typecheck clean.

🤖 Generated with Claude Code

fabianocruz and others added 4 commits July 5, 2026 09:44
Port the V3 canonical signing-string format from the private mandate
package into the public CLI, dependency-free (node:crypto + stdlib only):

- decodeToken: split the base64url JSON presentation envelope
  (signature / agent_sig / issuer_sig / kid) from the signed fields.
- reconstructSigningString: the 14-field V3 canonical string (V2's 12 +
  principal_kyc_ref + agent_kid), byte-locked against the copied
  canonical.v3.fixture.json freeze.
- verifyEd25519: SPKI-wrap a raw 32-byte key (RFC 8410) and verify with
  node:crypto; returns false, never throws, on malformed input.
- did.ts: did:web resolution (standard URL first, api.codespar.dev
  encoded fallback) extracting Ed25519 keys from JsonWebKey2020 docs.

Tests (offline only): byte-lock, agent+issuer verify pass, one-byte
tamper fails, wrong key fails, did:web URL mapping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the offline/network V3 verifier command under the mandate group:

- Offline mode (--agent-pubkey / --issuer-pubkey hex): verify the named
  signatures against supplied raw Ed25519 keys, zero network calls.
- Network mode (default): resolve agent + issuer public keys via did:web
  (issuer DID derived from the agent DID host) and verify.
- Prints per-signature status (verified / failed / skipped), the decoded
  governed fields (purposes, caps, agent_did, kid, expiry), and redacts
  principal_kyc_ref to a boolean. --json for machine output.
- Non-zero exit code on any verification failure. Requires no API key
  (offline verification is fully local).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bring the V3 mandate codec to the @codespar/sdk public surface as a new
./mandate subpath export, keeping node:crypto out of the main (edge-safe)
graph:

- decodeMandateToken: split the base64url presentation envelope from the
  signed fields.
- reconstructSigningString: the 14-field V3 canonical string (byte-locked
  against the shared canonical.v3.fixture.json freeze).
- verifyEd25519: SPKI-wrap a raw 32-byte key (RFC 8410), verify via
  node:crypto; false-never-throws on malformed input.
- verifyMandateToken: high-level offline verify from hex/byte pubkeys →
  { verified, mandate, agent/issuer SignatureCheck, agentDid, kid }.

node:crypto is a Node builtin, not an npm dependency — the zero-runtime-dep
guarantee holds. Fixture is byte-identical to the enterprise freeze and the
CLI copy (cross-impl parity anchor). 16 tests; subpath import driven E2E.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… crypto)

Port the V3 mandate codec to the Python SDK as a submodule (mirrors the TS
./mandate subpath — imported explicitly, kept out of the base import path):

- decode_mandate_token / reconstruct_signing_string: pure stdlib, zero new
  deps. The 14-field canonical string is byte-locked against the same shared
  canonical.v3.fixture.json the enterprise codec, TS SDK, and CLI pin.
- verify_ed25519 / verify_mandate_token: Ed25519 offline verify (hex or bytes
  keys) → { verified, mandate, agent/issuer SignatureCheck, agent_did, kid }.
  Ed25519 isn't in the stdlib, so it lazy-imports 'cryptography' and raises a
  clear install hint if absent — the base package keeps its single runtime
  dependency (httpx). Opt in via the new [verify] extra.

Tests byte-lock decode + reconstruction (always run); the verify cases are
guarded on the optional extra (installed in the dev extra, so they run in CI).

Verified locally against real cryptography 49.0.0 (agent+issuer verify, tamper
fails, wrong/short key fails, partial-key skip). Note: this env is Python 3.9
and the package requires 3.10+ (match statements), so the pytest suite itself
runs in CI; behavior here was exercised via standalone module load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@fabianocruz

Copy link
Copy Markdown
Member Author

Scope extended to the full SDK surface (same branch, +2 commits):

  • @codespar/sdk: new ./mandate subpath export — verifyMandateToken, decodeMandateToken, reconstructSigningString, verifyEd25519. Subpath (not main index) keeps node:crypto out of the edge-safe main graph, mirroring ./testing. tsc clean, 114 tests passed (+16), real subpath import driven through the exports map on the built dist.
  • codespar (Python): new codespar.mandate submodule — decode + canonical reconstruction pure-stdlib; Ed25519 verify behind a lazy cryptography import with a [verify] optional extra (base package keeps its single httpx dep). Byte-locked against the same fixture; exercised against cryptography 49.0.0 including tamper/wrong-key negatives. Local env is Python 3.9, so ruff/mypy/pytest gate in CI (3.10+) — treat CI as the final gate on the Python leg.
  • Cross-impl parity: the same canonical.v3.fixture.json is byte-identical in enterprise (source), CLI, TS SDK, and Python — any codec drift fails all four byte-locks loudly.

Publish plan (post-merge, OIDC trusted publishing only): @codespar/sdk 0.10.11→0.11.0, @codespar/cli 0.5.5→0.6.0, codespar (PyPI) 0.10.2→0.11.0 via gh release create python-v0.11.0. Note: npm trusted-publisher config for sdk/cli needs confirming before the npm legs.

@fabianocruz fabianocruz merged commit 148bcb4 into main Jul 5, 2026
10 checks passed
fabianocruz added a commit that referenced this pull request Jul 5, 2026
…date verify (#115)

Version bumps + changelogs for the #114 surface (already merged):
`@codespar/sdk/mandate` subpath, `codespar mandate verify`,
`codespar.mandate` Python submodule with the `[verify]` extra.

Release plan after merge:
1. Tag `v0.11.0` on the merge commit → `publish.yml` (npm OIDC trusted
publishing) ships sdk 0.11.0 + cli 0.6.0; every other package
soft-passes at its already-published version.
2. `gh release create python-v0.11.0` → `publish-python.yml` (PyPI
trusted publishing) ships `codespar` 0.11.0.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
fabianocruz added a commit that referenced this pull request Jul 5, 2026
…PI 0.11.0 (#116)

The python-v0.11.0 release run failed at the ruff gate: `__all__` in the
new `codespar.mandate` module wasn't isort-sorted (the authoring env was
Python 3.9, so ruff couldn't run locally — CI is the gate, as flagged in
#114). Sort-only change. After merge: re-run `publish-python.yml` via
workflow_dispatch to ship `codespar` 0.11.0.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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