Skip to content

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.

Terminal window

Or from source:

Terminal window
git clone https://github.com/identities-ai/ratify-protocol
cd ratify-protocol/sdks/rust
cargo build --release

Module path: ratify_protocol. Crate name: ratify-protocol.

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: AuthorizedAgent

Note: 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.

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:delegate
let 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 grant
let 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.

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.

Terminal window
cd ratify-protocol/sdks/rust
cargo test

You 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.