sudp
Rust implementation of the Custodian (
T) role of the Secret-Use Delegation Protocol.
The Custodian holds the secret-bearing sealed state Σ and dispatches
one authorized use at a time. It never persists reusable authority over
the secret: every redemption requires a fresh single-shot grant signed
by an Authorizer-side authenticator. The Requester (typically an LLM /
agent runtime) can be fully compromised without giving up s or any
reusable artifact derived from it.
Protocol overview, byte-anchored cross-language conformance, and a runnable three-role demo live at the repo root. Formal definition: arXiv:2604.24920.
Install
[]
= "~0.1"
MSRV: 1.85 (transitive base64ct 1.8+ requires edition 2024).
Minimal usage
use *;
// Standard primitive profile + WebAuthn as the authenticator.
let mut custodian: = new;
// Phase I — build Σ₀ from an initial M and one enrolled passkey.
let sealed = custodian.setup?;
// Phase II.1 — T issues a fresh r; A signs β = H(DS_bind ‖ r ‖ H(o)).
let r = custodian.issue_freshness;
// ... client sends the signed Grant back to T ...
// Phase II.3 — redeem.
let redeemed = custodian.redeem_grant?;
// Phase III.1 — use s_o inside T's boundary; R never sees it.
let response = custodian.execute_use?;
Runnable variants:
cargo run --example end_to_end— single process, mock authenticator.tests/e2e.rs— adversarial cases (tampering, replay, rotation lockout, revocation).examples/protocol-demo/at the repo root — full three-role flow over HTTP with the TypeScript Authorizer and Requester.
Per-record sealing (per-item vaults)
For deployments that store a vault as many independently-encrypted records
(one ciphertext per item) instead of a single SealedState blob — the model
that makes multi-device concurrent writes safe — use the per-record codec
directly:
use ;
let ctx = SealCtx ;
let sealed = ?; // suite ‖ nonce ‖ ct ‖ tag
let opened = ?; // rejects any ctx mismatch
The crate builds the AEAD associated data from SealCtx and binds it to the
ciphertext, so a record can't be opened under a different vault / id / version /
domain. The per-record AEAD key is HKDF-derived from k under a dedicated label
(key separation from any HMAC_K(name) id derivation). Everything above the
codec is the caller's: id derivation, version comparison, conflict resolution,
tombstones, GC, the record set. version is opaque — a server-assigned
sequence + CAS, a version-vector, an HLC, or LWW all sit on top unchanged.
Binding version detects a version/ciphertext mismatch, not rollback; keep a
monotonic per-id version store and enforce freshness there.
@sudp-protocol/authorizer mirrors this as sealRecord / unsealRecord,
byte-anchored by shared conformance vectors.
The Custodian<S, A, F> façade
Custodian<S, A, F>
// │ │ │
// │ │ └── FreshnessStore — `r`-token pool (in-memory, Redis, ...)
// │ └───── Authenticator — WebAuthn, HSM, mock-for-tests, ...
// └──────── PrimitiveSuite — Hash + Kdf + Aead + Wrap + Csprng
Phase methods:
| Phase | Methods |
|---|---|
| I — setup / enroll / revoke | setup, execute_enroll, execute_revoke |
| II — grant request + redemption | issue_freshness, build_conveyance, redeem_grant, redeem_batch |
| III — bounded use | execute_use, execute_export, execute_lifecycle |
Wire types: Operation, Grant<A>, RedeemedGrant<A>, SealedState,
ProtectedState, BatchOperations, BatchGrant<A>. Per-record codec:
SealCtx, seal_record, unseal_record, record_aad.
Feature flags
| Feature | Default | Pulls in |
|---|---|---|
std-primitives |
✓ | sha2, hkdf, chacha20poly1305, rand |
webauthn |
✓ | p256, ES256/P-256 assertion verifier |
json-canonical |
✓ | reserved; JCS canonical encoder is always on |
hpke |
✗ | hpke, rand_core 0.9; exposes HpkeDhKem<…> |
Bring your own primitives:
= { = "0.1", = false }
Customizing primitives
Three swap granularities, pick whichever fits.
1. Use the standard profile — StdPrimitives bundles SHA-256,
HKDF-SHA-256, XChaCha20-Poly1305 (AEAD + AEAD-as-wrap), and OsRng.
let custodian: = new;
2. Replace a single primitive — implement one trait, assemble a
PrimitiveSuite:
;
;
3. Bring your own everything — implement every trait (FIPS-validated
stack, post-quantum experiment, AES-KW without AEAD, ...). The protocol
logic in phases/ only sees S::Hash, S::Aead, etc. — no built-in
primitive is hardcoded.
The Authenticator trait is a separate axis
because it carries more associated types and is swapped much more often
than the crypto bundle (tests / HSMs / OS credential mediators). The
default webauthn feature ships ES256/P-256 with the PRF extension.
The freshness store is the third axis (F: FreshnessStore); default is
an in-memory pool, swap in a database-backed store as needed.
Out of scope
- HTTP / transport (TLS 1.3, cross-device handshake).
- Tool-call →
Operationcompilation (per-tool adapter). - Trusted rendering at
A(the crate emits canonical bytes; UI is the deployment's job). - Persistence of
SealedState(atomicity is a deployment invariant). - Rotation of the authority-bearing secret at
E(deployment policy).
License
Apache-2.0. See LICENSE.