Rust SDK
The Rust SDK is byte-for-byte interoperable with Go, TypeScript, Python, and C/C++. Rust 1.75+. Uses
ed25519-dalek and pqcrypto-mldsa for the underlying primitives.
Stability: every primitive on this page is stable in 1.0.0-alpha.8. There are no
preview / experimental APIs in the snippets below.
Install
Section titled “Install”Or from source:
git clone https://github.com/identities-ai/ratify-protocolcd ratify-protocol/sdks/rustcargo build --releaseModule path: ratify_protocol. Crate name: ratify-protocol.
Three minutes, end to end
Section titled “Three minutes, end to end”use ratify_protocol::{ DelegationCert, HybridSignature, ProofBundle, VerifyOptions, generate_agent, generate_challenge, generate_human_root, issue_delegation, sign_challenge, verify_bundle, PROTOCOL_VERSION, SCOPE_MEETING_ATTEND, SCOPE_MEETING_SPEAK,};use std::time::{SystemTime, UNIX_EPOCH};
fn main() { // 1. Alice generates her hybrid (Ed25519 + ML-DSA-65) root identity. let (alice, alice_priv) = generate_human_root();
// 2. Her AI agent generates its own hybrid keypair. let (agent, agent_priv) = generate_agent("Alice's Scheduler", "custom");
let now = SystemTime::now() .duration_since(UNIX_EPOCH).unwrap().as_secs() as i64;
// 3. Alice signs a delegation cert for the agent. let mut cert = DelegationCert { cert_id: "cert-001".into(), version: PROTOCOL_VERSION, issuer_id: alice.id.clone(), issuer_pub_key: alice.public_key.clone(), subject_id: agent.id.clone(), subject_pub_key: agent.public_key.clone(), scope: vec![SCOPE_MEETING_ATTEND.into(), SCOPE_MEETING_SPEAK.into()], constraints: Vec::new(), issued_at: now, expires_at: now + 7 * 24 * 3600, signature: HybridSignature { ed25519: Vec::new(), ml_dsa_65: Vec::new() }, }; issue_delegation(&mut cert, &alice_priv);
// 4. Verifier issues a challenge. Agent signs it. let challenge = generate_challenge(); let challenge_sig = sign_challenge(&challenge, now, &agent_priv);
// 5. Agent assembles a proof bundle. let bundle = ProofBundle { agent_id: agent.id.clone(), agent_pub_key: agent.public_key.clone(), delegations: vec![cert], challenge, challenge_at: now, challenge_sig, session_context: Vec::new(), stream_id: Vec::new(), stream_seq: 0, };
// 6. Verifier runs the verifier in a single call. let opts = VerifyOptions { required_scope: SCOPE_MEETING_ATTEND.into(), ..VerifyOptions::default() }; let result = verify_bundle(&bundle, &opts);
if result.valid { println!("✓ Authorized"); println!(" human_id: {}", result.human_id); println!(" agent_id: {}", result.agent_id); println!(" granted_scope: {:?}", result.granted_scope); println!(" identity_status: {:?}", result.identity_status); } else { println!("✗ Rejected: {:?} — {}", result.identity_status, result.error_reason); }}Real output (IDs are derived from the freshly generated keys, so they’ll differ on every run):
✓ Authorized human_id: d3fbb94368eb77d307e6b672735d81ad agent_id: 256952dbf6ea0b9eeb0562c1c83744e1 granted_scope: ["meeting:attend", "meeting:speak"] identity_status: AuthorizedAgentNote: in Rust, result.identity_status is the IdentityStatus enum (debug-printed here as
AuthorizedAgent); its as_str() method returns the wire-format string
("authorized_agent").
Verifier-side: branching on IdentityStatus
Section titled “Verifier-side: branching on IdentityStatus”verify_bundle always returns a VerifyResult. Inspect result.valid first; if it’s
false, the specific failure mode is in result.identity_status — an exhaustive enum so the
compiler enforces handling every case:
use ratify_protocol::IdentityStatus;
match result.identity_status { IdentityStatus::AuthorizedAgent => { // ✓ All checks passed. result.granted_scope is the intersection of // every cert's scope (effective scope across the chain). } IdentityStatus::Expired => { // ✗ At least one cert is past its expires_at OR not-yet-valid. } IdentityStatus::Revoked => { // ✗ A cert ID in the chain matched a revoked entry. } IdentityStatus::ScopeDenied => { // ✗ required_scope was not in the chain's effective scope, OR // an advanced PolicyProvider rejected the bundle. } IdentityStatus::ConstraintDenied => { // ✗ A first-class constraint (geo/time/amount/rate) evaluated // to "outside the allowed range". } IdentityStatus::ConstraintUnverifiable => { // ✗ A constraint required input the caller didn't provide. } IdentityStatus::ConstraintUnknown => { // ✗ A cert declared a constraint type this SDK build doesn't // recognize. Fail-closed by design. } IdentityStatus::DelegationNotAuthorized => { // ✗ An intermediate cert sub-delegated without identity:delegate. } IdentityStatus::VerifiedHuman => { // For verify-human flows (out of scope for this example). } IdentityStatus::Invalid => { // ✗ Catch-all for structural / cryptographic failures. // result.error_reason carries a stable machine-readable prefix. } IdentityStatus::Unauthorized => { // Reserved; not currently emitted by the verifier. }}The verifier is fail-closed by default — any error path returns valid: false.
Constraints (geo / time / amount / rate / speed)
Section titled “Constraints (geo / time / amount / rate / speed)”When a delegation declares constraints, supply the runtime context:
use ratify_protocol::VerifierContext;
let mut ctx = VerifierContext::default();ctx.current_lat = Some(37.7749);ctx.current_lon = Some(-122.4194);
let opts = VerifyOptions { required_scope: "drone:deliver".into(), context: ctx, ..VerifyOptions::default()};let result = verify_bundle(&bundle, &opts);VerifierContext fields are Option<T> — None means “not supplied”, and the canonical
context-hash (used by PolicyVerdict, SPEC §17.6) treats None and unset-Has* from the
Go SDK identically. Any geo_circle / geo_polygon / geo_bbox constraint will return
ConstraintUnverifiable if current_lat and current_lon aren’t both set.
The full constraint vocabulary (geo_circle, geo_polygon, geo_bbox, time_window,
max_speed_mps, max_amount, max_rate) and the VerifierContext field requirements are
documented in Constraints.
Sub-delegation
Section titled “Sub-delegation”Agent-to-agent delegation uses the same primitive. Alice grants Agent A meeting:attend
plus the identity:delegate privilege; A then issues a sub-delegation to Agent B.
Without identity:delegate on A’s grant from Alice, A’s sub-delegation will be rejected
with DelegationNotAuthorized.
use ratify_protocol::SCOPE_IDENTITY_DELEGATE;
// Alice → A: meeting:attend + identity:delegatelet mut alice_to_a = DelegationCert { cert_id: "cert-alice-to-a".into(), version: PROTOCOL_VERSION, issuer_id: alice.id.clone(), issuer_pub_key: alice.public_key.clone(), subject_id: agent_a.id.clone(), subject_pub_key: agent_a.public_key.clone(), scope: vec![SCOPE_MEETING_ATTEND.into(), SCOPE_IDENTITY_DELEGATE.into()], constraints: Vec::new(), issued_at: now, expires_at: now + 86400, signature: HybridSignature { ed25519: Vec::new(), ml_dsa_65: Vec::new() },};issue_delegation(&mut alice_to_a, &alice_priv);
// A → B: a subset of A's grantlet mut a_to_b = DelegationCert { cert_id: "cert-a-to-b".into(), version: PROTOCOL_VERSION, issuer_id: agent_a.id.clone(), issuer_pub_key: agent_a.public_key.clone(), subject_id: agent_b.id.clone(), subject_pub_key: agent_b.public_key.clone(), scope: vec![SCOPE_MEETING_ATTEND.into()], constraints: Vec::new(), issued_at: now, expires_at: now + 3600, signature: HybridSignature { ed25519: Vec::new(), ml_dsa_65: Vec::new() },};issue_delegation(&mut a_to_b, &agent_a_priv);
// Chain order: [leaf, ..., root]. A→B is the leaf, Alice→A is the root.let bundle = ProofBundle { agent_id: agent_b.id.clone(), agent_pub_key: agent_b.public_key.clone(), delegations: vec![a_to_b, alice_to_a], challenge, challenge_at: now, challenge_sig: agent_b_sig, session_context: Vec::new(), stream_id: Vec::new(), stream_seq: 0,};
let opts = VerifyOptions { required_scope: SCOPE_MEETING_ATTEND.into(), ..VerifyOptions::default()};let result = verify_bundle(&bundle, &opts);result.granted_scope is the lex-sorted intersection of every cert in the chain.
identity:delegate doesn’t propagate to B because A didn’t grant it onward — that’s the
intentional behavior.
Provider hooks (SPEC §17)
Section titled “Provider hooks (SPEC §17)”Each hook is a trait that you implement and place in VerifyOptions:
use ratify_protocol::{ RevocationProvider, PolicyProvider, AuditProvider, ConstraintEvaluator, AnchorResolver,};
struct MyRevocation { /* … */ }impl RevocationProvider for MyRevocation { fn is_revoked(&self, cert_id: &str) -> Result<bool, String> { /* … */ }}
let opts = VerifyOptions { required_scope: SCOPE_MEETING_ATTEND.into(), revocation: Some(Box::new(MyRevocation { /* … */ })), // §17.1 // policy: Some(Box::new(MyPolicy)), // §17.2 // audit: Some(Box::new(MyAudit)), // §17.3 // constraint_evaluators: Some(my_ext_registry), // §17.7 // anchor_resolver: Some(Box::new(MyAnchorResolver)), // §17.8 // policy_verdict: Some(cached_verdict), // §17.6 // policy_secret: Some(policy_secret), ..VerifyOptions::default()};Each hook has fail-closed semantics — a RevocationProvider that returns
Err(...) fails the bundle with IdentityStatus::Invalid +
error_reason: "revocation_error: ...", not a silent “unknown means allow.” See
Provider architecture for the full reference.
Running the conformance suite
Section titled “Running the conformance suite”cd ratify-protocol/sdks/rustcargo testYou should see all 59 canonical wire-format fixtures plus the 10 alpha.7 cross-SDK byte-equivalence vectors pass. If any fail, the bytes have drifted from the reference and the SDK is not interoperable — file a bug.
Where to next
Section titled “Where to next”- Protocol concepts: Delegate → Present → Verify — every primitive in depth
- Constraints — full constraint vocabulary +
VerifierContextreference - Provider architecture — the §17 hooks plus the alpha.7 levers
- Integration guides — surface-specific patterns