# Ratify Protocol — full documentation > Concatenated full-text dump of every page on docs.identities.ai, ordered by priority for LLM ingestion. Generated automatically at build time from the source content; if you're an LLM or a researcher reading this, the structured navigation index is at /llms.txt and the canonical site is at https://docs.identities.ai. --- # Ratify Protocol > Delegated authority proofs for AI agents. Open protocol, five SDKs, byte-for-byte interop. Source: https://docs.identities.ai/ ## Where to start Open SDKs in Go, TypeScript, Python, Rust, and C/C++. Free under Apache-2.0. Byte-for-byte interoperable across all five. [Pick a language →](/sdk/go/) The mental model is three verbs: Delegate, Present, Verify. Same primitive for human → agent and agent → agent. [Read the deep dive →](/concepts/delegate-present-verify/) API gateways and MCP tools, voice and conversational agents, meetings, and physical systems. Same wire format everywhere. [See the integration guides →](/guides/api-gateway/) The managed control plane — hosted revocation, audit archive, policy enforcement at scale. For enterprises. [Learn what Verify adds →](/verify/overview/) ## Three verbs, one primitive ``` DELEGATE PRESENT VERIFY ──────── ─────── ────── Principal signs a Presenter (agent) Any third party DelegationCert carries the cert runs the verifier. naming the subject, and signs a fresh Both Ed25519 AND the scopes, and the challenge on every ML-DSA-65 must expiration. interaction. verify. Yes/no in milliseconds. No trust Human → Agent OR Proves "this key is relationship with Agent → Agent. live right now." presenter required. ``` The same primitive works in both directions. A human delegating to their AI assistant and one AI agent sub-delegating to another use the **exact same verifier algorithm** and the **same cryptographic guarantees**. ## Where Ratify sits in your stack Keep your IAM, OAuth, MCP, A2A, and policy engines — Ratify adds the missing proof; it replaces none of them. ``` IAM authenticates Alice → MCP moves the tool call → Ratify proves delegated authority → your API ``` Bearer tokens worked when a human was present, a known client called a known API, and the session stayed inside one app boundary. Agents act later, call many tools, cross vendors, and hand work to other agents — so the receiving side needs a portable, pre-action proof of **who** delegated authority, **what** scope was granted, and **whether it is still valid**. OAuth authenticates an account. MCP moves a tool call. Policy engines interpret rules. Ratify proves the agent had delegated authority before the action. ## Quantum-safe by design Every signature is a hybrid pair: **Ed25519 + ML-DSA-65** (NIST FIPS 204). Both must verify. Bundles signed today remain unforgeable even when a cryptographically-relevant quantum computer exists. ## Two products, one protocol - The **Ratify Protocol** is open: spec under CC-BY-4.0, SDKs under Apache-2.0, reference verifier free for everyone. [GitHub →](https://github.com/identities-ai/ratify-protocol) - **Ratify Verify** is the managed operational layer for agent authorization decisions: a metered verification API, hosted revocation checks, delegation controls, and a hash-chained audit trail. The managed service is currently in alpha. [What Verify adds →](/verify/overview/) You can use the open protocol forever without ever touching Verify. Verify exists for teams that want managed authorization infrastructure without operating it themselves. --- **Maintained by [Identities AI, Inc.](https://identities.ai)** Ratify Protocol™ and identities.ai™ are trademarks of Identities AI, Inc. U.S. patent application pending. --- # Protocol vs. Verify > How the open Ratify Protocol, the reference SDKs, and the commercial Ratify Verify product fit together. Source: https://docs.identities.ai/start/protocol-vs-verify/ Ratify is two products, one protocol. The split is intentional and the boundary is sharp. ## The short version | You need… | You use… | License | Pricing | |---|---|---|---| | To create, present, or verify proof bundles in code | The **open protocol + SDKs** | Apache-2.0 (SDKs) + CC-BY-4.0 (spec) | Free, forever | | Hosted infrastructure around proofs: revocation lists, audit archive, key custody, delegation controls, integration adapters | **Ratify Verify** (managed) | Commercial | Per-verification (Operational); annual contract (Trust) | | Self-hosted Verify in your VPC / on-prem | **Verify Sovereign** | Commercial license + customer-controlled keys | Annual license | ## Pick a path ``` ┌────────────────────────────────────────┐ │ What are you building? │ └─────────────────┬──────────────────────┘ │ ┌────────────────────────┼────────────────────────┐ ▼ ▼ ▼ ┌──────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │ An AI agent that │ │ A platform that │ │ An enterprise that │ │ needs to PROVE │ │ HOSTS AI agents for │ │ wants to AUTHORIZE │ │ what it can do. │ │ many customers. │ │ AI usage at scale. │ └────────┬─────────┘ └──────────┬──────────┘ └──────────┬──────────┘ │ │ │ ▼ ▼ ▼ ┌──────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │ Use the SDK. │ │ Use the SDK + │ │ Use Ratify Verify. │ │ Build proof │ │ register your │ │ Enterprise IT │ │ bundles. │ │ platform with │ │ controls policy + │ │ │ │ Verify so your │ │ audit. Agent │ │ Open source. │ │ users' agents are │ │ platforms connect │ │ Apache-2.0. │ │ verifiable wherever │ │ in. Policy and │ │ │ │ they act. │ │ audit built in. │ └──────────────────┘ └─────────────────────┘ └─────────────────────┘ ``` ## What the open protocol gives you - The wire format. Signed `DelegationCert` + `ProofBundle` JSON shapes. - Hybrid Ed25519 + ML-DSA-65 signing. Quantum-safe in v1. - The verifier algorithm: fail-closed, deterministic, milliseconds to run (sub-millisecond in the Go reference). - 79 canonical conformance fixtures. Any implementation passing them is byte-for-byte interoperable. - Reference SDKs in Go, TypeScript, Python, Rust, and C/C++. - A working `ratify-cli` for local experimentation. You can build a complete agent authorization system using nothing but the open protocol. Many will. The protocol owes nothing to Identities AI as a company; the spec is CC-BY-4.0. ## What Ratify Verify adds - **Hosted revocation lists** with fail-closed cache invalidation. - **Append-only audit history** — hash-chained for organization workspaces in the current alpha; decision details are not yet covered by the chain. KMS-signed checkpoints and WORM archival are implemented but not yet runtime-verified in the alpha. - **Org-scoped key custody** with envelope encryption (AES-256-GCM under Cloud KMS) and self-custody upgrade paths. - **Agentic API integration**: call the managed Verify API from MCP servers, A2A services, REST gateways, or application middleware before executing a protected action. - **Delegation controls and Meetings policy configuration**, with verifier-local policy tooling on the roadmap. - **Surface integrations**: Conversational AI is the next managed integration target. A Meetings control plane and Zoom adapter implementation are under validation. Physical AI remains a later managed integration; the open protocol and SDKs already support its authorization constraints. - **Multi-tenant onboarding**: invite users, claim a domain, all the boring SaaS plumbing. - **Compliance mappings** (roadmap): audit fields mapped to SOX, FINRA, NYDFS, HIPAA, and the EU AI Act. Verify is the operational layer that turns protocol primitives into a managed authorization service with hosted verification, revocation checks, audit, and delegation controls. ## When you use the SDK without Verify All of the following are fine, and supported, with no commercial relationship: - Run a single-user Ratify identity from a laptop or phone. - Self-host a verifier endpoint and check proofs in-process. - Build a research demo, a hackathon project, an open-source side project. - Implement a new-language SDK that passes all 79 fixtures. The protocol does not phone home. There is no telemetry. There is no "validation server" you have to ping. The SDK ships with the verifier algorithm built in. ## When you graduate to Verify Most teams hit one of these triggers: 1. **You need managed verification for consequential API and tool actions.** An agent is about to invoke an MCP tool, send an A2A request, or hit a REST operation that matters, and you want the authorization decision metered, attributed, and recorded with one call from your middleware. 2. **You need revocation at scale.** A leaked agent key has to be revoked across every verifier you rely on, quickly. Self-hosting revocation lists works for a single instance; it breaks down across regions and customers. 3. **You need retained verification history.** Hash-chained logs with signed checkpoints and write-once storage are non-trivial to build correctly. Verify's alpha includes an append-only audit history, hash-chained for organization workspaces; production retention and compliance evidence capabilities (including audit fields mapped to SOX/FINRA/NYDFS/HIPAA/EU AI Act) are still being validated or on the roadmap. 4. **You want managed surface integrations.** Your customers' teams want to onboard, set policy, see audit, and move on without running infrastructure. Conversational AI is the next managed integration target; a Meetings control plane and Zoom adapter are under validation. ## Source of truth - **Specification:** [SPEC.md in ratify-protocol](https://github.com/identities-ai/ratify-protocol/blob/main/SPEC.md). The normative protocol document. - **Reference SDKs:** [github.com/identities-ai/ratify-protocol](https://github.com/identities-ai/ratify-protocol). Apache-2.0. - **Conformance fixtures:** [testvectors/v1/](https://github.com/identities-ai/ratify-protocol/tree/main/testvectors/v1) in the same repo. - **Verify product:** Sign up via [identities.ai](https://identities.ai). Currently in private alpha. --- # Quickstart > Install a Ratify SDK in your language of choice and verify the conformance suite in under 60 seconds. Source: https://docs.identities.ai/start/quickstart/ The 60-second test: install the SDK in your language of choice, run the conformance suite, see the 79 canonical fixtures pass. **If they pass on your machine, you've proven byte-for-byte interop with every other Ratify implementation.** ```bash # Install as a module in your project go get github.com/identities-ai/ratify-protocol@v1.0.0-alpha.20 # Or clone and run the full conformance suite git clone https://github.com/identities-ai/ratify-protocol cd ratify-protocol go test ./... # → ok github.com/identities-ai/ratify-protocol 0.5s ``` [Full Go quickstart →](/sdk/go/) ```bash npm install @identities-ai/ratify-protocol@1.0.0-alpha.20 ``` Or run the conformance suite yourself: ```bash git clone https://github.com/identities-ai/ratify-protocol cd ratify-protocol/sdks/typescript npm install npm test # → 416 passed (79 conformance fixtures + cross-SDK vectors + wire codecs + challenge stores + providers) ``` [Full TypeScript quickstart →](/sdk/typescript/) ```bash pip install ratify-protocol==1.0.0a20 ``` Or run the conformance suite yourself: ```bash git clone https://github.com/identities-ai/ratify-protocol cd ratify-protocol/sdks/python pip install -e '.[dev]' pytest -q # → 464 passed (79 conformance fixtures + unit and integration tests) ``` [Full Python quickstart →](/sdk/python/) ```bash cargo add ratify-protocol@1.0.0-alpha.20 ``` Or run the conformance suite yourself: ```bash git clone https://github.com/identities-ai/ratify-protocol cd ratify-protocol/sdks/rust cargo test # → test result: ok. 1 passed (loads all 79 fixtures) ``` [Full Rust quickstart →](/sdk/rust/) Download the pre-built library for your platform from the [GitHub Releases page](https://github.com/identities-ai/ratify-protocol/releases) — no Rust toolchain required. The `ratify.h` header is also committed at [`sdks/c/include/ratify.h`](https://github.com/identities-ai/ratify-protocol/blob/main/sdks/c/include/ratify.h). Or build from source (requires Rust 1.70+): ```bash git clone https://github.com/identities-ai/ratify-protocol cd ratify-protocol/sdks/c cargo build --release # → target/release/libratify_c.a + libratify_c.so + include/ratify.h ``` Run the conformance suite: ```bash cargo test --test conformance # → 79/79 conformance fixtures pass cargo test --test api cargo test --test advanced # → 77 ABI unit tests pass across api + advanced ``` [Full C/C++ SDK guide →](/sdk/c/) ## What "the conformance suite" means Every Ratify SDK ships a test that loads `testvectors/v1/*.json` — 79 fixtures generated deterministically by the Go reference implementation — and verifies each one. The fixtures cover: - Valid delegations of various scopes, durations, and chain depths - Tampered delegations (modified scope, modified expiry, modified subject) - Expired certs (clock-skew scenarios) - Out-of-scope requests (requested scope not granted by chain) - Revoked certs (against signed revocation lists) - Hybrid signature edge cases (Ed25519 valid + ML-DSA-65 invalid, both invalid, etc.) - Sub-delegation chains (Alice → Agent-A → Agent-B) - Constraint enforcement (geo, time, version) If all 79 pass in your SDK, your SDK produces and verifies the same bytes as every other Ratify SDK on the planet. That is the interoperability contract. ## Run the end-to-end demo The conformance suite proves *the bytes are correct*. The demos prove *the protocol does what it claims*. Each runs one positive end-to-end flow — told in five steps, from delegation to verified — plus four rejection scenarios (tampered / out-of-scope / expired / revoked), and prints what happened and why. ```bash cd ratify-protocol go run ./demos/go ``` ```bash cd sdks/python && pip install -e . && cd ../.. python demos/python/demo.py ``` ```bash cd sdks/typescript && npm install && npm run build && cd ../.. cd demos/typescript && npm install && npm run demo ``` ```bash cargo run --manifest-path demos/rust/Cargo.toml ``` ```bash cd ratify-protocol/sdks/c cargo build --release cc examples/verify_bundle.c -I include \ -L target/release -lratify_c -lpthread -ldl \ -o verify_bundle && ./verify_bundle ``` The narrative demos are a [single repository directory](https://github.com/identities-ai/ratify-protocol/tree/main/demos) for Go, TypeScript, Python, and Rust. The C/C++ path uses the ABI example in `sdks/c/examples/`. Reading one demo and watching it run is the fastest way to understand what the protocol does. ## Next steps - **Want to ship an integration?** Start with the language-specific guide ([Go](/sdk/go/) / [TypeScript](/sdk/typescript/) / [Python](/sdk/python/) / [Rust](/sdk/rust/) / [C\/C++](/sdk/c/)). - **Building for embedded or Physical AI?** The [C/C++ SDK](/sdk/c/) ships `libratify_c.a` for RTOS and embedded Linux targets. - **Want to understand the protocol?** [Concepts: Delegate, Present, Verify](/concepts/delegate-present-verify/). - **Protecting an API, MCP tool, or A2A action?** Start with the [Agentic API guide](/guides/api-gateway/). - **Exploring meeting authorization?** See the [Meetings guide](/guides/meetings/). - **Need managed revocation and audit?** [Ratify Verify overview](/verify/overview/). --- # Delegate, Present, Verify > The three verbs that compose the entire protocol, illustrated with byte-level detail. Source: https://docs.identities.ai/concepts/delegate-present-verify/ The Ratify Protocol is **three verbs.** That's it. Every adapter, every SDK, every integration — they're all built out of some sequence of these three. ``` DELEGATE PRESENT VERIFY ──────── ─────── ────── Principal signs a Presenter (agent) Any third party DelegationCert carries the cert runs the verifier. naming the subject, and signs a fresh Both Ed25519 AND the scopes, and the challenge on every ML-DSA-65 must expiration. interaction. verify. Yes/no in milliseconds. No trust Human → Agent OR Proves "this key is relationship with Agent → Agent. live right now." presenter required. ``` The symmetry matters. A human delegating to an AI agent and one AI agent sub-delegating to another use the **exact same primitive**, the **same verifier algorithm**, and the **same cryptographic guarantees**. ## 1. Delegate The principal — a human or another agent — signs a `DelegationCert`. The cert binds the principal's identity to the subject's identity and specifies what the subject can do. ### The bytes A `DelegationCert` is the following JSON shape, serialized with **canonical JSON** (keys sorted, no insignificant whitespace, deterministic number formatting): ```json { "cert_id": "0a3b...c9d2", "version": 1, "issuer_id": "92cb0a15572d7a71", "issuer_pub_key": { "ed25519": "base64-encoded-32-bytes", "ml_dsa_65": "base64-encoded-1952-bytes" }, "subject_id": "b4a4c71795d676b6", "subject_pub_key": { "ed25519": "...", "ml_dsa_65": "..." }, "scope": ["meeting:attend", "meeting:speak"], "constraints": [], "issued_at": 1799996400, "expires_at": 1800082800, "signature": { "ed25519": "base64-encoded-64-bytes", "ml_dsa_65": "base64-encoded-3309-bytes" } } ``` `expires_at` is a required integer with no null representation. A cert meant to last "until revoked" carries `NO_EXPIRY_SENTINEL = 4070908799` (2099-12-31 23:59:59 UTC) as its `expires_at` — display and policy code must treat that value as "no expiry (until revoked)", never as a literal 2099 expiry. Each SDK ships an `IsNoExpiry` / `is_no_expiry` helper. Verification is unchanged; [revocation](/concepts/revocation/) is the sole termination mechanism for such certs. ### How the signature is computed ``` delegationSignBytes = canonical_json(cert with signature field omitted) signature.ed25519 = Ed25519.Sign(delegationSignBytes, issuer.private.ed25519) signature.ml_dsa_65 = ML-DSA-65.Sign(delegationSignBytes, issuer.private.ml_dsa_65) ``` The canonical JSON is RFC 8785, with byte fields base64-standard-encoded. Both signatures are computed over the same canonical bytes and both must verify at the receiving end. The `delegationSignBytes` algorithm is defined precisely in [SPEC.md §7.1](https://github.com/identities-ai/ratify-protocol/blob/main/SPEC.md) and the conformance fixtures pin it byte-identically across all five SDKs. ### Why a hybrid signature ``` Ed25519 alone Hybrid (Ed25519 + ML-DSA-65) ───────────── ──────────────────────────── Today ✓ Forgery-resistant ✓ Forgery-resistant After Q-day ✗ BROKEN — quantum-capable ✓ Still resistant — ML-DSA-65 (large-scale adversary can forge any is lattice-based, not quantum Ed25519 signature ever number-theoretic. Quantum computer) produced, retroactively offers no known speedup. ("harvest now, decrypt later") ML-DSA-65 alone is sufficient for Q-day defense. So why use Ed25519 too? Defense in depth. If a flaw is found in ML-DSA-65 (newer algorithm, less battle-tested), Ed25519 still holds the line. ``` The hybrid posture is what FIPS 204, CNSA 2.0, and BSI guidance all recommend for the post-quantum transition. ## 2. Present When the agent wants to do something, it builds a `ProofBundle`: the chain of delegations it holds plus a fresh signature over a verifier-supplied challenge. ### The bytes ```json { "agent_id": "b4a4c71795d676b6", "agent_pub_key": { "ed25519": "...", "ml_dsa_65": "..." }, "delegations": [ /* [leaf, ..., root] — leaf-first per SPEC §6.5 */ ], "challenge": "Zx8t4vQrM2...", "challenge_at": 1800000000, "challenge_sig": { "ed25519": "...", "ml_dsa_65": "..." }, "session_context": "", // optional 32 bytes; SPEC §15.1 "stream_id": "", // optional 32 bytes; v1.1 "stream_seq": 0 // optional; ≥1 when stream_id is set } ``` ### How the challenge signature is computed The challenge signable is **raw concatenated bytes**, not canonical JSON: ``` sign_data = challenge // typically 32 random bytes || big-endian uint64(challenge_at) // 8 bytes || [optional] 32-byte session_context || [optional] 32-byte stream_id || big-endian int64(stream_seq) challenge_sig.ed25519 = Ed25519.Sign(sign_data, agent.private.ed25519) challenge_sig.ml_dsa_65 = ML-DSA-65.Sign(sign_data, agent.private.ml_dsa_65) ``` The agent's identity isn't in the signable — it's established by the chain, and the verifier runs hybrid verify against `bundle.agent_pub_key`. The full byte-layout, sizes, and the three SDK entry points (`SignChallenge`, `SignChallengeWithSessionContext`, `SignChallengeWithStream`) live on the [Challenges](/concepts/challenges/) page. ### Why a fresh challenge Without freshness, an attacker who steals a bundle once could replay it forever. The challenge mechanism defeats this: ``` 1. Verifier generates 32 random bytes (challenge). 2. Verifier records the timestamp (challenge_at). 3. Verifier sends both to the presenter. 4. Presenter signs (challenge, challenge_at) with the agent's hybrid private key. 5. Verifier rejects if challenge_at is older than the protocol's freshness window (CHALLENGE_WINDOW_SECONDS = 300, a protocol-level constant — see [Challenges](/concepts/challenges/)). ``` The reference verifier checks only that the embedded `challenge_at` timestamp is recent; it does not track which challenges it issued. That bounds replay to the freshness window rather than eliminating it — a verifier that must treat presentations as single-use has to track issued challenges itself. See [Challenges](/concepts/challenges/) for the trade-offs. ## 3. Verify Any third party with the principal's public key can verify the entire bundle. **Six deterministic checks. Milliseconds (sub-millisecond in the Go reference). Yes or no.** ``` ┌───────────────────────────────────-───────┐ bundle ─────────▶│ Verify(bundle, options) │ │ │ │ 1. Structural checks │ │ depth ∈ [1, MaxDelegationChainDepth] │ │ session_context/stream_id are 32B │ │ │ │ 2. Chain check │ │ For each link i in chain: │ │ cert[i].issuer_id == │ │ cert[i+1].subject_id │ │ Last cert's subject_id == │ │ bundle.agent_id │ │ │ │ 3. Per-cert checks │ │ For each delegation: │ │ - both hybrid sigs verify │ │ - issued_at ≤ now < expires_at │ │ - revocation provider says false │ │ - all constraints pass │ │ - intermediates carry │ │ identity:delegate │ │ │ │ 4. Liveness │ │ 0 ≤ now − challenge_at ≤ 300 s │ │ hybrid verify of challenge_sig over │ │ challenge || ts || session_ctx || │ │ stream_id || stream_seq │ │ │ │ 5. Scope │ │ effective = ⋂ link.scope (lex sort) │ │ required_scope ∈ effective │ │ │ │ 6. Policy (if provided) │ │ PolicyVerdict or PolicyProvider OK │ └────────────────┬───────────────────────-──┘ │ ▼ ┌──────────┴───────────┐ ▼ ▼ ┌───────────────┐ ┌──────────────────────────┐ │ Valid=true │ │ Valid=false, with one of:│ │ identity_ │ │ │ │ status: │ │ expired │ │ authorized_ │ │ revoked │ │ agent │ │ scope_denied │ │ granted_scope │ │ constraint_denied │ │ human_id │ │ constraint_unverifiable │ │ agent_id │ │ constraint_unknown │ │ │ │ delegation_not_ │ │ │ │ authorized │ │ │ │ invalid │ └───────────────┘ └──────────────────────────┘ ``` **Fail-closed.** Any check failing → `Valid=false` plus a specific `identity_status` → caller rejects the request. There is no "valid with warnings" path, no "partially valid" state. `invalid` is the catch-all for structural / cryptographic failures and always carries a machine-parsable `error_reason` prefix (e.g. `stale_challenge: ...`, `bad_challenge_sig: ...`, `revocation_error: ...`). ## Multi-hop chains Agent-to-agent sub-delegation uses the exact same primitive — the chain just gets longer. ``` Alice (human) │ │ signs DelegationCert { │ issuer: Alice │ subject: Agent-A │ scope: [meeting:*, identity:delegate] ← identity:delegate │ } is REQUIRED for A │ to sub-delegate. ▼ Agent-A │ │ signs DelegationCert { │ issuer: Agent-A │ subject: Agent-B │ scope: [meeting:attend, meeting:record] │ (identity:delegate intentionally NOT passed on) │ } ▼ Agent-B │ │ builds ProofBundle { │ agent_id: Agent-B │ delegations: [A→B, Alice→A] ← leaf-first per §6.5 │ challenge_sig: signed by Agent-B's private key │ } ▼ Verifier checks: ├ Both delegations' hybrid sigs valid (Ed25519 + ML-DSA-65) ├ A→B.issuer_id == Alice→A.subject_id (chain well-formed) ├ Alice→A scope contains identity:delegate │ (else identity_status: delegation_not_authorized) ├ All certs within issued_at/expires_at ├ effective_scope = scope[Alice→A] ∩ scope[A→B], lex-sorted │ = scope[meeting:*-expansion + identity:delegate] ∩ │ {meeting:attend, meeting:record} │ = {meeting:attend, meeting:record} └ required_scope ∈ effective_scope → authorized_agent ``` The verifier runs the same algorithm for chain depth 1 and chain depth `MaxDelegationChainDepth` — same code path, just loops more times. Runtime grows linearly with chain depth. Effective scope is the **lex-sorted intersection** across the chain. An agent cannot grant more rights than it was given. The structural invariant that makes sub-delegation safe. ## Source The verifier algorithm is normative — defined in [SPEC.md §8](https://github.com/identities-ai/ratify-protocol/blob/main/SPEC.md). Every SDK implements the same algorithm; the 79 conformance fixtures verify byte-identical results. ## Where to next - [Scopes](/concepts/scopes/) — the canonical 54-scope vocabulary plus the `custom:` extension pattern - [Constraints](/concepts/constraints/) — geo / time / version gating - [Challenges & freshness](/concepts/challenges/) — replay protection deep dive - [Revocation](/concepts/revocation/) — signed revocation lists - [Hybrid post-quantum crypto](/concepts/hybrid-pqc/) — why two signatures, not one --- # Provider architecture & the build-vs-buy boundary > How the Ratify Protocol stays open while Ratify Verify holds the operational moat, through the four hook points and four optional levers. Source: https://docs.identities.ai/concepts/provider-architecture/ The Ratify Protocol is open: wire format, hybrid Ed25519 + ML-DSA-65 signatures, the verifier state machine, the canonical 54-scope vocabulary, the seven first-class constraint types. Every conformant SDK implements those byte-for-byte — the 79 canonical test vectors prove it. **Bundles verified by any SDK against any provider stack are byte-identical to bundles verified with no providers at all.** That is the conformance contract. But three of the verifier's responsibilities are inherently operational, and a single static spec cannot pin them down without locking out the rest of the ecosystem: 1. **Revocation freshness.** A CRL file polled hourly is fine for a low-throughput verifier; a real-time payment endpoint needs sub-second push propagation. 2. **Policy evaluation.** Quotas, geo-tagged kill switches, runtime overrides, per-tenant rules. These are stateful and verifier-local; they cannot live in a signed cert. 3. **Audit retention.** A developer's local log file is enough for staging; SOC2 / ISO 27001 / HIPAA compliance requires a signed, hash-chained, append-only ledger. Alpha.9 introduces the SDK-side architecture that makes the boundary explicit. The protocol stops at the verifier's deterministic core; everything operational sits behind pluggable provider interfaces. The open-source default is a working, no-op implementation. The commercial implementation, Ratify Verify, supplies the stateful, operational counterpart. ## The diagram ``` [ open ] ProofBundle wire format ────────── same bytes everywhere [ open ] hybrid sig + chain walk + scope ─── same algorithm everywhere [ open ] cert-bound Constraints ─────────── same evaluation everywhere ─────────────────────────────────────────── deterministic verifier core [ hook ] RevocationProvider ↔ local file / hosted revocation lists [ hook ] PolicyProvider ↔ none / Rego/OPA + quota [ hook ] AuditProvider ↔ stdout / managed append-only audit store [ opt ] ConstraintEvaluator ↔ none / extension type registry [ opt ] PolicyVerdict ↔ none / HMAC-cached allow/deny [ opt ] AnchorResolver ↔ none / SSO-bound identity lookup [ opt ] VerificationReceipt ↔ none / signed audit chain ``` A bundle moves freely across all five SDKs. Where verifiers differ is in operational surface — latency, compliance posture, integration ergonomics — not in cryptography. ## The three core providers (§17.1–§17.3) ### RevocationProvider — `is_revoked(cert_id) -> (bool, error)` Determines whether a certificate is currently revoked. Returns a tuple so the SDK can distinguish "live," "revoked," and "unknown" — a lookup failure is fail-closed (`revocation_error: ...`), not silently treated as "not revoked." | Implementation | Where it lives | Operational properties | |---|---|---| | Local file CRL | Open-source SDK | Polled hourly; staleness bounded by poll interval. | | In-memory bloom filter | Open-source SDK | O(1) at call time; fits 1M revoked IDs in ~12 MB. | | **Ratify Verify push-sync** (roadmap) | Commercial | Push stream for low-staleness revocation distribution. | The interface lives in the open-source SDK. The managed implementation is what customers pay for. ### PolicyProvider — `evaluate_policy(bundle, context) -> (bool, error)` Verifier-local, stateful policy evaluation. Runs **after** all cryptographic, temporal, revocation, constraint, and scope-intersection checks pass — so a bundle that fails earlier never reaches policy. - `(true, None)` → allow; verification returns success. - `(false, None)` → deny; verification returns `identity_status="scope_denied"`. - `(_, error)` → fail-closed; verification returns `identity_status="invalid"`, `error_reason="policy_error: ..."`. The distinction from constraints (§5.7.2) matters: **constraints are signed by the principal and travel with the bundle** (every verifier sees the same bytes); **policy is verifier-local and runtime-mutable** (different verifiers can run different policies on the same bundle). Both signals are required, neither replaces the other. ### AuditProvider — `log_verification(result, bundle)` Invoked on **every** `verify_bundle` call, success AND failure. Provider errors are intentionally swallowed by the verifier — auditing is observation, not control; an audit-store outage MUST NOT flip a `Valid=true` result to `Valid=false`. SDKs surface provider exceptions through a separate diagnostic channel. ## The four optional levers (§17.5–§17.8) These are crypto primitives and pluggable surfaces that sit on top of the core verifier. They are **OPTIONAL**: nothing changes in the wire format whether you use them or not. The SDKs ship them so commercial verifiers and OSS deployments share a vocabulary. ### Lever 1 — `VerificationReceipt` (§17.5) A verifier-signed, hash-chained attestation that a specific `ProofBundle` was verified at a specific moment with a specific outcome. The cryptographic complement of `AuditProvider`: - An `AuditProvider` chooses **what to do** with verification events. A buggy or malicious one can drop, backdate, or refuse entries. - A chain of `VerificationReceipt`s makes the event itself **unforgeable**. Each receipt's `prev_hash` is the SHA-256 of the previous receipt's canonical signable bytes; missing or backdated entries are detectable. SDK API (Go names; see SPEC §17.9 for cross-language naming): ```go BundleHash(bundle) -> 32-byte SHA-256 IssueVerificationReceipt(bundle, result, verifierID, verifierPub, verifierPriv, prevHash, ts) -> *VerificationReceipt VerifyVerificationReceipt(receipt) -> nil | error ReceiptHash(receipt) -> 32-byte SHA-256 (next prev_hash) ``` **This is the strongest audit moat possible.** A clone audit provider can't retroactively prove "we verified bundle X at time Y" — only a chain of receipts signed by the verifier's key can. ### Lever 2 — `PolicyVerdict` (§17.6) The policy equivalent of `SessionToken`. A short-lived, HMAC-bound cached allow/deny: issued once by a commercial policy backend, accepted locally by the verifier for the rest of `valid_until` without re-calling the backend, which removes most policy-server round-trips on streaming workloads. **Context binding.** `context_hash` is the SHA-256 of the canonical-JSON serialization of the policy-relevant subset of `VerifierContext` (location, speed, transaction amount, currency). A verdict cached for one context (e.g. `current_lat=37, current_lon=-122`) does NOT apply to a different context (e.g. London). The verifier recomputes the context hash on every call and compares — preventing a verdict for one context from leaking into another. **Fast-path semantics.** When `VerifyOptions.PolicyVerdict` and `VerifyOptions.PolicySecret` are both set, the verifier consults the verdict BEFORE the live `Policy` provider: - Cached allow → live policy is **not called**; return success. - Cached deny → live policy is **not called**; return `scope_denied`. - Verdict unusable (expired / wrong MAC / scope mismatch) → fall through to live `Policy`. A stale verdict MUST NOT cause a verification failure on its own. ### Lever 3 — `ConstraintEvaluator` registry (§17.7) The built-in constraint types in §5.7.2 (`geo_circle`, `geo_polygon`, `geo_bbox`, `time_window`, `max_speed_mps`, `max_amount`, `max_rate`) are the universal vocabulary every conformant SDK implements byte-identically. Real deployments need additional types (`max_concurrent_sessions`, `max_daily_spend`, `region_allowlist`, …) that don't belong in the universal spec. Extension constraints can carry a `params` object under the restricted value model in SPEC §5.7.1. The type and parameters are part of the certificate's signed canonical bytes. The `ConstraintEvaluator` interface is the pluggable layer: 1. Built-in evaluators handle universal types (always, by the SDK directly). 2. For unknown types, the per-Verify registry is consulted. 3. If no entry matches, the verifier fails closed with `identity_status="constraint_unknown"`. **Naming convention:** - New extension types use a reverse-domain prefix controlled by their defining organization. - Ratify integration profiles use `com.ratifyprotocol..`. - Ratify Verify managed types use `com.ratifyprotocol.verify.`. - A party that controls `example.com` uses `com.example.`. The verifier treats the name as an exact opaque string and fails closed when no evaluator is registered. The namespace prevents collisions. It does not require a network lookup or a Ratify service at verification time. ### Lever 4 — `AnchorResolver` (§17.8) The `Anchor` type (§5.4) is an optional binding between a HumanRoot and an external identity system: Okta SSO assertion, government-ID attestation, verified email, etc. v1 carried `Anchor` only at HumanRoot mint time. v1.1 adds `AnchorResolver`: a verifier-local lookup from `human_id` to the registered `Anchor`, run on every successful verification. The result is **anchor-bound audit**: - A `VerificationReceipt` proves "this bundle was verified at this time." - An anchor-bound `VerificationReceipt` proves "this bundle was verified at this time, AND the human root behind it was bound to an SSO-asserted identity at Okta as of `Anchor.VerifiedAt`." That's the chain compliance auditors want to see. **Audit interaction.** When both `AnchorResolver` and `AuditProvider` are configured, the resolver runs BEFORE the audit hook — so the `VerifyResult` that audit providers see already has `Anchor` populated. Resolver errors are non-fatal: an identity-directory outage degrades the audit trail; it does not block a properly-signed, cryptographically-valid bundle. ## What you can build yourself (OSS-only path) Honest checklist of what the open-source protocol and SDKs cover, with nothing else configured: - ✅ Generate `HumanRoot` and `AgentIdentity` keypairs (hybrid Ed25519 + ML-DSA-65) - ✅ Issue and verify `DelegationCert`s with first-class constraints - ✅ Build, present, and verify `ProofBundle`s with session and stream binding - ✅ Issue and verify `RevocationList`s and `RevocationPush`es - ✅ Issue and verify `KeyRotationStatement`s - ✅ Issue and verify `WitnessEntry`s (the protocol's append-only log primitive) - ✅ Issue and verify `TransactionReceipt`s for multi-party transactions - ✅ Local revocation via the `RevocationProvider` interface - ✅ Local policy enforcement via the `PolicyProvider` interface - ✅ Local audit logging via the `AuditProvider` interface - ✅ Signed `VerificationReceipt` chains (the protocol provides the primitive; you build the archive) - ✅ HMAC `PolicyVerdict` caching (the protocol provides the primitive; you run the backend) - ✅ Extension constraint registry (you write the evaluators) - ✅ Anchor resolver against your own identity directory **The decision is in the protocol.** A `PolicyProvider` returning `false` rejects the bundle — Ratify decides whether presented authority covers the requested operation, and your organization enforces that decision at its own boundary. What Ratify Verify sells is not the decision; it is the **management surface** that makes acting on decisions operable at scale. Verify currently provides managed verification, hosted revocation checks, delegation controls, and append-only audit history. Richer policy tooling, push distribution, SSO-bound anchors, and customer-deployed packaging remain roadmap or design-partner work. ## Compliance checklist A pragmatic mapping of common compliance regimes to what OSS provides vs. what Ratify Verify supplies on top: | Regime | OSS provides | Verify supplies | |---|---|---| | **SOC 2 Type 2** | `VerificationReceipt` primitive; `AuditProvider` interface | Hash-chained ledger, retention SLAs, exportable evidence, SAML/SSO | | **ISO 27001** | Hybrid PQC signatures; revocation; key rotation | Documented key custody, signed compliance attestations | | **HIPAA** | All cryptographic primitives | BAA, US-only data residency, signed audit archive | | **GDPR / data residency** | Anchor primitive; opaque references (no PII on the wire) | Regional deployments, on-prem / VPC option | | **Air-gapped / FedRAMP-style** | Full SDK works offline | Ratify Air-Gap: on-prem control plane, no phone-home | | **PCI DSS** | `max_amount` constraint; `TransactionReceipt`s | Per-transaction audit chain, attested key custody | ## Surface adapters (out of scope for this repository) The integration code that turns a `ProofBundle` into a "Zoom auth gate," "Twilio SIP attestation," "AWS API Gateway authorizer," "Slack request validator" — the **surface adapters** — lives in separate repositories (`ratify/zoom-sdk`, `ratify/voice-sdk`, …). Those are the home of proprietary "last-mile" integration code and are not covered by the protocol specification. The protocol's contract stops at the `ProofBundle` wire format and the verifier algorithm. How a third-party platform's signaling layer is intercepted, how middleware is wired into a specific framework, how an incumbent product's auth model is mapped onto Ratify scopes — is integration work, not protocol work. Ratify Verify ships those adapters as commercial product. Nothing about the spec prevents a third party from writing their own. ## Deprecation notice `VerifyOptions.IsRevoked` (the legacy `func(certID) bool` closure) is **deprecated in v1.0.0-alpha.10** and scheduled for removal in **v1.0.0-beta.1**. The closure cannot distinguish "not revoked" from "I don't know," so it cannot fail closed on lookup failures — `Revocation` (§17.1) returns `(bool, error)` and fails closed correctly. When both fields are set on `VerifyOptions`, `Revocation` wins. The closure remains functional through every `v1.0.0-*` release. Each SDK marks the field with its idiomatic deprecation mechanism: Go doc comment with `Deprecated:` line, TypeScript `@deprecated` JSDoc, Python runtime `DeprecationWarning` on use, Rust `#[deprecated]` attribute. ## Pointers - Normative spec: [SPEC §17](https://github.com/identities-ai/ratify-protocol/blob/main/SPEC.md#17-sdk-architecture-provider-interfaces) - Changelog: [v1.0.0-alpha.20 release notes](https://github.com/identities-ai/ratify-protocol/blob/main/CHANGELOG.md) - Pricing for the managed surface: [Ratify Verify pricing](/verify/pricing) - Architecture deep-dive: [docs/EXPLAINED.md](https://github.com/identities-ai/ratify-protocol/blob/main/docs/EXPLAINED.md) --- # Scopes > The canonical 54-scope vocabulary, the 14 wildcards, the sensitive-scope rule, and the `custom:` extension pattern. Source: https://docs.identities.ai/concepts/scopes/ A **scope** is a string that names a specific action an agent is authorized to take. Every `DelegationCert.scope` field is a list of scope strings, and the verifier checks that the action being attempted is covered by the chain's *effective scope* (the lex-sorted intersection of every cert in the chain). The full canonical vocabulary at `v1.0.0-alpha.20`: ``` ┌──────────────────────────────────────────────────────────────┐ │ Ratify v1 scope vocabulary │ │ │ │ 54 canonical scopes • 14 wildcards • custom: extension │ └──────────────────────────────────────────────────────────────┘ ``` **Stability:** every scope listed below is stable in v1. New scopes can be added in minor versions without breaking existing certs; nothing on this page is going to be renamed or removed. ## Why a canonical vocabulary Without one, every integrator invents their own scope strings — `read`, `read:files`, `files.read`, `Files.Read`, `FILE_READ` — all meaning the same thing and none of them cross-compatible. That's the OAuth scope graveyard. Ratify fixes it by having the protocol define the set: every SDK exports the same constants, every verifier recognizes the same vocabulary, every issuer signs against the same strings. The full list lives in [`scope.go`](https://github.com/identities-ai/ratify-protocol/blob/main/scope.go) (Go), [`scope.ts`](https://github.com/identities-ai/ratify-protocol/blob/main/sdks/typescript/src/scope.ts) (TypeScript), [`scope.py`](https://github.com/identities-ai/ratify-protocol/blob/main/sdks/python/src/ratify_protocol/scope.py) (Python), [`scope.rs`](https://github.com/identities-ai/ratify-protocol/blob/main/sdks/rust/src/scope.rs) (Rust), and [`ratify.h`](https://github.com/identities-ai/ratify-protocol/blob/main/sdks/c/include/ratify.h) (C/C++). All five are byte-for-byte aligned via the conformance suite. ## The 17 domains The 54 canonical scopes group into 17 first-level domains. Sensitive scopes are marked with a 🔒 — they are NEVER introduced by wildcard expansion (§9.1) and must always be granted explicitly. ### Meetings (6 scopes) | Scope | Purpose | |---|---| | `meeting:attend` | Join a meeting as a listener | | `meeting:speak` | Speak / transmit audio in a meeting | | `meeting:video` | Transmit video in a meeting | | `meeting:chat` | Post messages in meeting chat | | `meeting:share_screen` | Share screen in a meeting | | `meeting:record` 🔒 | Record a meeting | ### Communications (8 scopes) | Scope | Purpose | |---|---| | `comms:message:read` | Read direct / group messages | | `comms:message:send` | Send messages | | `comms:message:delete` 🔒 | Delete sent messages | | `comms:email:read` | Read email | | `comms:email:send` | Send email | | `comms:email:delete` 🔒 | Delete email | | `comms:calendar:read` | Read calendar events | | `comms:calendar:write` | Create / modify calendar events | ### Files (2 scopes) | Scope | Purpose | |---|---| | `files:read` | Read files | | `files:write` 🔒 | Create / modify files | ### Identity (2 scopes) | Scope | Purpose | |---|---| | `identity:prove` | Present proof of identity (used for authn flows) | | `identity:delegate` 🔒 | Sub-delegate authority to another agent. **Required on the parent cert for any sub-delegation to be accepted.** | ### Presence (1 scope) | Scope | Purpose | |---|---| | `presence:represent` 🔒 | Attend and interact as a direct representative of the principal — other parties may be interacting with this agent as if it were the principal. Covers non-likeness representatives and full likeness agents. | Three things to know about `presence:represent`: 1. It does **not** imply `identity:prove` — Ratify has no scope-implication table, so issuers grant both explicitly when both are needed. 2. There is deliberately **no `presence:*` wildcard** — sensitive scopes never ride wildcards, and representation must always be granted explicitly. 3. Verifiers that accept a bundle carrying this scope are expected to surface the representation relationship to the other participants in the interaction. That disclosure is platform policy, not a protocol constraint. ### Transactions (2 scopes) | Scope | Purpose | |---|---| | `transact:purchase` | Buy a good or service | | `transact:sell` | Sell a good or service | ### Payments (3 scopes) | Scope | Purpose | |---|---| | `payments:send` | Initiate an outbound payment | | `payments:receive` | Accept an inbound payment | | `payments:authorize` 🔒 | Authorize movement of funds — separated from `send` so a delegation can be "may receive but never debit" | ### Contracts (2 scopes) | Scope | Purpose | |---|---| | `contract:read` | Read contract terms | | `contract:sign` 🔒 | Enter into a binding agreement | ### Data (5 scopes) | Scope | Purpose | |---|---| | `data:read` | Read data records | | `data:write` 🔒 | Create or modify data records | | `data:delete` 🔒 | Delete data records | | `data:export` 🔒 | Bulk export — data-exfiltration concern, separated from `read` | | `data:share` | Share specific records with a third party | ### Execute (2 scopes) | Scope | Purpose | |---|---| | `execute:tool` | Invoke a tool / function call (MCP-style) | | `execute:code` 🔒 | Execute arbitrary code on the principal's behalf | ### Generate (2 scopes) | Scope | Purpose | |---|---| | `generate:content` | Generate content (text, image, etc.) | | `generate:deepfake` 🔒 | Generate content intended to impersonate someone — separated so platforms can refuse this scope universally | ### Physical (4 scopes) | Scope | Purpose | |---|---| | `physical:enter` | Enter a physical zone (badge access, geofence cross) | | `physical:exit` | Exit a physical zone | | `physical:actuate` 🔒 | Activate a physical actuator — valve, motor, switch, robot end-effector | | `physical:manipulate` 🔒 | Manipulate physical objects (grasping, moving, assembling) | ### Robots (3 scopes) | Scope | Purpose | |---|---| | `robot:operate` | General robot operation — sensor readings, state queries | | `robot:move` | Autonomous locomotion (the robot may move in space) | | `robot:interact` | Interact with humans or other robots (gesture, speech, handoff) | Robot scopes are not individually marked sensitive, but high-stakes robot operations (picking up a knife, opening a valve) should be composed with `physical:manipulate` 🔒 or `physical:actuate` 🔒 so the human consent is explicit. ### Drones (3 scopes) | Scope | Purpose | |---|---| | `drone:fly` 🔒 | Fly a drone — sensitive because of airspace + safety risks | | `drone:deliver` | Deliver a package via drone | | `drone:capture` | Capture imagery / sensor data from a drone | ### Vehicles (3 scopes) | Scope | Purpose | |---|---| | `vehicle:operate` 🔒 | Operate a vehicle (drive, ride, command) | | `vehicle:transport` | Use a vehicle as a passenger / payload carrier | | `vehicle:charge` | Initiate vehicle charging or refueling | ### Infrastructure (3 scopes) | Scope | Purpose | |---|---| | `infrastructure:monitor` | Read infrastructure metrics (temperature, flow, voltage) | | `infrastructure:control` 🔒 | Control infrastructure systems (HVAC, power, water) | | `infrastructure:access` 🔒 | Gain administrative access to infrastructure | ### Actuators (3 scopes) | Scope | Purpose | |---|---| | `actuate:valve` 🔒 | Open / close a physical valve | | `actuate:motor` 🔒 | Energize / de-energize a motor | | `actuate:switch` 🔒 | Flip an electrical switch | Every actuator scope is sensitive by design — physical-world actions get explicit delegation. ## Sensitive scopes — what the marker means There are **22 sensitive scopes** in v1 (every 🔒 above). The protocol enforces three rules around them: 1. **No wildcard introduction.** A wildcard like `comms:*` expands to its non-sensitive members only. Granting `comms:*` does NOT include `comms:message:delete` 🔒 or `comms:email:delete` 🔒 — those must be listed explicitly in the cert's scope. 2. **No `identity:delegate` shortcut.** Even with `identity:delegate` 🔒 on the parent cert, the child cert can only sub-delegate scopes the parent itself held. The sub-delegation gate (`delegation_not_authorized`) keeps the chain bounded. 3. **Verifier UX recommendation.** Verifiers SHOULD highlight sensitive scopes in any user-facing prompt ("Agent wants to delete your messages, are you sure?") even when the delegation is valid. The authoritative list is [`sensitiveScopes`](https://github.com/identities-ai/ratify-protocol/blob/main/scope.go) in the Go reference. All five SDKs expose `is_sensitive(scope)` / `IsSensitive(scope)` / `isSensitive(scope)` — in C, use `ratify_scope_is_sensitive(scope)`. ## Wildcards For ergonomics, Ratify defines 14 wildcards that expand into a set of non-sensitive scopes from the same domain: | Wildcard | Expands to (non-sensitive members only) | |---|---| | `meeting:*` | `meeting:attend`, `meeting:speak`, `meeting:video`, `meeting:chat`, `meeting:share_screen` (excludes `meeting:record` 🔒) | | `comms:message:*` | `comms:message:read`, `comms:message:send` (excludes `comms:message:delete` 🔒) | | `comms:email:*` | `comms:email:read`, `comms:email:send` (excludes `comms:email:delete` 🔒) | | `comms:*` | All of `comms:message:*`, `comms:email:*`, and `comms:calendar:read` + `comms:calendar:write` | | `transact:*` | `transact:purchase`, `transact:sell` | | `payments:*` | `payments:send`, `payments:receive` (excludes `payments:authorize` 🔒) | | `data:*` | `data:read`, `data:share` (excludes `data:write` 🔒, `data:delete` 🔒, `data:export` 🔒) | | `execute:*` | `execute:tool` (excludes `execute:code` 🔒) | | `generate:*` | `generate:content` (excludes `generate:deepfake` 🔒) | | `physical:*` | `physical:enter`, `physical:exit` (excludes `physical:actuate` 🔒, `physical:manipulate` 🔒) | | `robot:*` | `robot:operate`, `robot:move`, `robot:interact` | | `drone:*` | `drone:deliver`, `drone:capture` (excludes `drone:fly` 🔒) | | `vehicle:*` | `vehicle:transport`, `vehicle:charge` (excludes `vehicle:operate` 🔒) | | `infrastructure:*` | `infrastructure:monitor` (excludes `infrastructure:control` 🔒, `infrastructure:access` 🔒) | `files:*`, `identity:*`, `presence:*`, `contract:*`, and `actuate:*` are **not defined as wildcards** — every scope in those domains is either sensitive or singleton, so wildcards would either expand to nothing useful or introduce a sensitive scope (which the protocol forbids). When a wildcard appears in a `DelegationCert.scope` list, the SDK expands it during verification — the cert's signable bytes still contain the wildcard form, but the *effective scope* used for intersection / required-scope checks contains the expanded set. ## Custom scopes If you need an application-specific scope outside the canonical set, prefix it with `custom:`: ``` custom:acme:invoice:approve custom:my-platform:moderate-content custom:internal:archive:purge ``` Custom scopes: - Are accepted by `validate_scopes()` / `ValidateScopes()` without lookup against the canonical list. - Pass through `expand_scopes()` / `ExpandScopes()` unchanged (they don't participate in wildcard expansion). - Are treated as **non-sensitive by default** unless the application opts in via out-of-band policy. - Do not have cross-application meaning — `custom:acme:invoice:approve` issued by Acme Corp is meaningless to a verifier outside Acme's ecosystem. The intent is to let teams ship Ratify-secured features that aren't yet part of the canonical vocabulary, without forking the protocol. If a custom scope sees broad adoption, file a proposal to add it to the canonical set in a minor version. ## Effective scope across a chain When a chain has more than one cert, the **effective scope** is the lex-sorted intersection of every cert's expanded scope list: ``` Alice → Agent A: ["meeting:attend", "meeting:speak", "identity:delegate"] Agent A → Agent B: ["meeting:attend"] Effective scope of [A→B, Alice→A]: ["meeting:attend"] ``` Two implications: 1. **A child can never have more than the parent.** The intersection is monotonically shrinking. 2. **`identity:delegate` doesn't propagate downward.** Even though Alice granted A `identity:delegate`, B's effective scope doesn't include it — A didn't grant it onward. B cannot mint further sub-delegations. This is the foundation of Ratify's least-privilege story: every link in the chain can only narrow, never broaden, what the previous link granted. ## Programmatic access All five SDKs export the canonical scope strings as constants. Use them so the compiler / linter catches typos before they reach the verifier: ```go // Go import "github.com/identities-ai/ratify-protocol" ratify.ScopeMeetingAttend // "meeting:attend" ratify.ScopeIdentityDelegate // "identity:delegate" ratify.IsSensitive(s) // bool ratify.ExpandScopes(scopes) // []string with wildcards expanded ratify.IntersectScopes(a, b, c) // []string lex-sorted intersection ``` ```typescript // TypeScript import { SCOPE_MEETING_ATTEND, SCOPE_IDENTITY_DELEGATE, isSensitive, expandScopes, intersectScopes, } from "@identities-ai/ratify-protocol"; ``` ```python # Python from ratify_protocol import ( SCOPE_MEETING_ATTEND, SCOPE_IDENTITY_DELEGATE, is_sensitive, expand_scopes, intersect_scopes, ) ``` ```rust // Rust use ratify_protocol::{ SCOPE_MEETING_ATTEND, SCOPE_IDENTITY_DELEGATE, is_sensitive, expand_scopes, intersect_scopes, }; ``` ## Where to next - [Constraints](/concepts/constraints/) — geo / time / amount / rate gates that ride on top of scopes - [Delegate → Present → Verify](/concepts/delegate-present-verify/) — how scopes flow through the protocol - [Provider architecture](/concepts/provider-architecture/) — the §17 hooks for policy / revocation / audit --- # Constraints > First-class bounds on when, where, and how much an agent may exercise a scope. Source: https://docs.identities.ai/concepts/constraints/ Scopes name **what** an agent is allowed to do. A **constraint** rides alongside a scope and names **when, where, or how much** — geofence, time-of-day, speed cap, amount cap, rate cap. The verifier evaluates every constraint against the runtime `VerifierContext` you supply. **Fail-closed by design:** - If the constraint's value is **outside the allowed range** → `constraint_denied`. - If the runtime context required to decide is **not supplied** → `constraint_unverifiable`. - If the constraint's `type` is **not recognized** by this SDK build → `constraint_unknown`. In other words: the verifier never falls back to "allow because I'm not sure." ## The v1 constraint vocabulary Seven first-class types are built into every SDK. They are byte-for-byte identical across Go, TypeScript, Python, and Rust — the canonical wire-format fixtures cover all seven. | `type` | What it bounds | Required `VerifierContext` | |---|---|---| | `geo_circle` | Position inside a haversine-radius circle | `current_lat`, `current_lon` | | `geo_polygon` | Position inside a polygon (≥3 vertices) | `current_lat`, `current_lon` | | `geo_bbox` | Position inside a lat/lon (+ optional alt) box | `current_lat`, `current_lon` (+ `current_alt_m` if set) | | `time_window` | Local wall-clock time in `[start, end]` | (none — verifier clock is enough) | | `max_speed_mps` | Current velocity ≤ `max_mps` | `current_speed_mps` | | `max_amount` | Requested amount ≤ `max_amount` in `currency` | `requested_amount`, `requested_currency` | | `max_rate` | ≤ `count` invocations per rolling `window_s` | rate-counter callback | Unknown types are rejected (`constraint_unknown`). New types require either a SPEC bump or an extension `ConstraintEvaluator` (SPEC §17.7) — see [Provider architecture](/concepts/provider-architecture/). ## Geofence: `geo_circle` ```json { "type": "geo_circle", "lat": 37.7749, "lon": -122.4194, "radius_m": 500 } ``` Haversine distance on WGS-84. Valid only when the agent is within `radius_m` meters of (`lat`, `lon`). ```go ctx := ratify.VerifierContext{ HasLocation: true, CurrentLat: 37.7751, // ~30 m from center CurrentLon: -122.4190, } result := ratify.Verify(bundle, ratify.VerifyOptions{ RequiredScope: "drone:deliver", Context: ctx, }) ``` If `HasLocation == false` the result is `constraint_unverifiable` — not denied, **unverifiable**. ## Geofence: `geo_polygon` ```json { "type": "geo_polygon", "points": [ [37.7755, -122.4200], [37.7755, -122.4180], [37.7745, -122.4180], [37.7745, -122.4200] ] } ``` `points` is a list of `[lat, lon]` pairs. At least 3 vertices required; winding order is irrelevant. Inclusion is tested with ray-casting in equirectangular projection. **v1 limitation (fail-closed):** polygons whose longitude span exceeds 180° (i.e., that cross the anti-meridian) are rejected with `constraint_denied` rather than silently returning a wrong answer. For very large or anti-meridian-crossing regions, model the boundary with a sequence of sub-polygons or use a `ConstraintEvaluator` extension (geodesic semantics are planned for v2). ## Geofence: `geo_bbox` ```json { "type": "geo_bbox", "min_lat": 37.7700, "max_lat": 37.7800, "min_lon": -122.4250, "max_lon": -122.4150, "min_alt_m": 0, "max_alt_m": 120 } ``` Rectangular bounding box. Altitude bounds are only enforced if **either** `min_alt_m` or `max_alt_m` is non-zero — set both to zero to ignore altitude entirely. `geo_bbox` is the only geo type with **anti-meridian semantics built in**: if `min_lon > max_lon`, the box is interpreted as wrapping the 180° meridian. For example, `{"min_lon": 170, "max_lon": -170}` means "from 170°E eastward across the date line to 170°W." ## Time of day: `time_window` ```json { "type": "time_window", "tz": "America/Los_Angeles", "start": "06:00", "end": "22:00" } ``` Local wall-clock window in the IANA-named timezone `tz`. Inclusive at both ends. **No `VerifierContext` input is required** — the verifier uses its own clock. Wrapping is supported: `start: "22:00", end: "06:00"` means "10 PM through 6 AM the next day." ```python # Cert says "weekdays 6am–10pm Pacific"; the verifier's own clock decides result = verify_bundle( bundle, VerifyOptions(required_scope="physical:move"), ) ``` Times are validated as `HH:MM` 24-hour clock. Malformed values fail closed. ## Velocity cap: `max_speed_mps` ```json { "type": "max_speed_mps", "max_mps": 5.0 } ``` The agent's current velocity must be ≤ `max_mps` meters per second (SI units — no mph hidden conversions). ```rust let mut ctx = VerifierContext::default(); ctx.current_speed_mps = Some(3.2); // ~7 mph let result = verify_bundle(&bundle, &VerifyOptions { required_scope: "robot:operate".into(), context: ctx, ..VerifyOptions::default() }); ``` Without `current_speed_mps`, the result is `constraint_unverifiable`. ## Transaction amount: `max_amount` ```json { "type": "max_amount", "max_amount": 500.0, "currency": "USD" } ``` The requested amount must be ≤ `max_amount` and the currency must match **exactly** (ISO 4217 codes; case-sensitive on the wire). A USD constraint will deny a EUR request with `constraint_denied: currency mismatch`. ```ts const result = await verifyBundle(bundle, { required_scope: "payment:execute", context: { requested_amount: 75.0, requested_currency: "USD", }, }); ``` The protocol does no FX conversion. If you grant `max_amount: 500 USD` and the agent presents a EUR payment, the cert is denied — even if 75 EUR < 500 USD by today's rate. This is intentional: FX feeds are out of scope for an authorization protocol. ## Rate cap: `max_rate` ```json { "type": "max_rate", "count": 10, "window_s": 3600 } ``` At most `count` exercises of this specific cert (matched by `cert_id`) within a rolling `window_s` seconds. Both must be positive integers — malformed values fail closed. `max_rate` requires you to supply a **callback** that answers "how many times has this cert been used in the last N seconds?": ```go ctx := ratify.VerifierContext{ InvocationsInWindow: func(certID string, windowS int64) int { return myRateLimiter.Count(certID, windowS) }, } ``` This is the one constraint that needs persistent state across verifications — the rest are pure functions of `(constraint, context, now)`. Most implementations back the counter with Redis or a database keyed on `(cert_id, window_bucket)`. Without the callback, the result is `constraint_unverifiable`. ## `VerifierContext` reference The full set of fields the eight built-in constraints can read: | Field | Type | Read by | |---|---|---| | `current_lat`, `current_lon` | `float64` | `geo_circle`, `geo_polygon`, `geo_bbox` | | `current_alt_m` | `float64` | `geo_bbox` (when altitude bounds set) | | `current_speed_mps` | `float64` | `max_speed_mps` | | `requested_amount` | `float64` | `max_amount` | | `requested_currency` | `string` (ISO 4217) | `max_amount` | | `invocations_in_window(cert_id, window_s) -> int` | callback | `max_rate` | | `requested_resource_id` | `string` | `resource_path` | | `requested_path` | `string` | `resource_path` when `path_prefix` is present | In Go and Rust, presence is signaled by boolean flags (`HasLocation`, `HasSpeed`, `HasAmount`, `HasResource`); in TypeScript and Python, by leaving the field `undefined` / `None`. The canonical context hash (used inside `PolicyVerdict`, SPEC §17.6) treats "flag unset" and "field is None" identically across SDKs — so a verdict signed by a Go verifier is consumable by a Rust verifier on the exact same context. ## Chained constraints are additive A sub-delegation chain compounds constraints — every cert's constraints must pass. ``` Alice → Agent-A constraint: geo_bbox (warehouse footprint) Agent-A → Agent-B constraint: time_window (06:00–08:00 PT) Agent-B's bundle is valid iff: ✓ inside the warehouse (Alice's constraint) ✓ at 6 AM–8 AM PT (Agent-A's constraint) ``` Constraints never weaken down the chain. If you want a wider envelope downstream, the upstream cert has to grant it explicitly. ## What the verifier returns | Outcome | `identity_status` | `error_reason` prefix | |---|---|---| | All constraints satisfied | `authorized_agent` | (empty) | | A value is out of range | `constraint_denied` | `constraint[i] (geo_circle): outside allowed radius: 612.3m > 500.0m` | | A required input is missing | `constraint_unverifiable` | `constraint[i] (max_speed_mps): no current speed in context` | | The `type` is unknown to this SDK | `constraint_unknown` | `constraint[i] (foo): constraint_unknown: unknown constraint type "foo"` | `error_reason` always carries the cert chain index and the constraint type, so logs are machine-parsable. ## Extension constraint types (SPEC §17.7) If you need a constraint vocabulary the v1 set doesn't cover, define a reverse-domain name under a domain you control and register a `ConstraintEvaluator` in `VerifyOptions.ConstraintEvaluators`: ```go opts := ratify.VerifyOptions{ RequiredScope: "internal:do_thing", ConstraintEvaluators: map[string]ratify.ConstraintEvaluator{ "com.example.org_membership": myOrgMembershipEvaluator, }, } ``` The eight built-in types always win. Registering a `geo_circle` evaluator is a no-op. Unknown types are routed to your registry; if no entry matches, the verifier returns `constraint_unknown` (fail-closed). An extension constraint can carry a `params` object containing null, booleans, strings, safe integers, arrays, and objects under the restricted model in SPEC §5.7.1. Both the type and parameters are covered by the principal's signature. Every downstream verifier must register the same exact type name before it can evaluate the constraint. See [Provider architecture](/concepts/provider-architecture/) for the full evaluator interface and lifecycle. ## Where to next - [Scopes](/concepts/scopes/) — the verbs that constraints further restrict - [Delegate → Present → Verify](/concepts/delegate-present-verify/) — where constraints sit in the verification pipeline - [Provider architecture](/concepts/provider-architecture/) — `ConstraintEvaluator` and the other §17 hooks - [Physical AI guide](/guides/physical-ai/) — geofences and speed caps in production --- # Challenges & freshness > The challenge-response mechanism that turns a static cert into a live-key proof. Source: https://docs.identities.ai/concepts/challenges/ A `DelegationCert` proves *Alice authorized this agent*. But on its own, that cert is just a file — anyone who steals it once could replay it forever. The **challenge-response** is what turns "I have a cert" into "I have a cert *and I am the live holder of the agent's private key right now*." ## The mechanism ``` ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ Agent │ │ Verifier │ │ │ │ │ │ │ │ │ │ has: │ │ │ │ │ │ - cert │ │ │ │ │ │ - own │ │ │ │ │ │ priv │ │ │ │ │ │ keys │ │ │ │ │ └──────┬──────┘ └──────┬──────┘ │ │ │ │ │ │ │ 1. "Hi, I want to do X." │ │ │ │ ──────────────────────────────────────────────▶│ │ │ │ │ │ │ │ 2. challenge = 32 random bytes │ │ │ │ challenge_at = now() │ │ │ │ ◀──────────────────────────────────────────────│ │ │ │ │ │ │ │ 3. Sign (challenge || challenge_at || …) │ │ │ │ with agent's hybrid (Ed25519 + ML-DSA-65) │ │ │ │ private key │ │ │ │ │ │ │ │ 4. ProofBundle { │ │ │ │ delegations: [...], │ │ │ │ challenge, │ │ │ │ challenge_at, │ │ │ │ challenge_sig │ │ │ │ } │ │ │ │ ──────────────────────────────────────────────▶│ │ │ │ │ │ │ │ 5. Verify: │ │ │ │ - chain sigs OK? │ │ │ │ - challenge_sig │ │ │ │ valid against │ │ │ │ agent pub key? │ │ │ │ - challenge_at │ │ │ │ within 5 minutes?│ │ │ │ │ │ │ │ ◀──── 6. ✓ Authorized OR ✗ Rejected ───────────│ │ │ │ │ │ │ └──────┴──────┘ └──────┴──────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` ## What gets signed (raw bytes, not JSON) The challenge signable is **raw binary concatenation** — not canonical JSON. The challenge is already opaque random bytes, so JSON-wrapping would add weight without adding security (`crypto.go` → `challengeSignBytes`): ``` sign_data = challenge // typically 32 random bytes || big-endian uint64(challenge_at) // 8 bytes || [optional] 32-byte session_context // §15.1 SessionContext binding || [optional] 32-byte stream_id || [optional] big-endian int64(stream_seq) // 8 bytes (only when stream_id is set) challenge_sig.ed25519 = Ed25519.Sign(sign_data, agent.priv.ed25519) challenge_sig.ml_dsa_65 = ML-DSA-65.Sign(sign_data, agent.priv.ml_dsa_65) ``` Three signable sizes are unambiguously distinct on the wire: | Bindings | Signable length (for a 32-byte challenge) | |---|---| | Base (challenge + ts) | 40 bytes | | `session_context` only | 72 bytes | | `stream_id` + `stream_seq` only | 80 bytes | | Both bindings | 112 bytes | The verifier reconstructs the same byte sequence from `bundle.Challenge`, `bundle.ChallengeAt`, `bundle.SessionContext`, `bundle.StreamID`, `bundle.StreamSeq` and runs the hybrid verify against `bundle.AgentPubKey`. There is no `agent_id` in the signable — the binding to identity comes from the hybrid verify against `agent_pub_key`, whose ID is established by the chain. ### Three sign-paths in the SDKs Each reference SDK exposes three entry points so callers can pick the binding level they need without ever building the byte string themselves: | Function | Binds to | |---|---| | `SignChallenge(challenge, ts, priv)` | challenge + timestamp only (the common case) | | `SignChallengeWithSessionContext(challenge, ts, sessionContext, priv)` | + verifier/session (§15.1) | | `SignChallengeWithStream(challenge, ts, sessionContext, streamID, streamSeq, priv)` | + ordered stream (v1.1) | The Python, TypeScript, Rust, and C/C++ SDKs expose the same challenge-signing semantics through their idiomatic APIs. The fixture corpus checks that all five produce byte-identical signables. ## Freshness window The verifier rejects bundles whose `challenge_at` is older than `ChallengeWindowSeconds` (a **constant** in the protocol, currently `300` — 5 minutes). It also rejects negative ages — a challenge timestamped in the future. ```go // from verify.go challengeAge := now.Unix() - bundle.ChallengeAt if challengeAge < 0 || challengeAge > ChallengeWindowSeconds { return invalid("stale_challenge", fmt.Sprintf( "challenge is %d seconds old (max %d)", challengeAge, ChallengeWindowSeconds)) } ``` On failure: - `identity_status: invalid` - `error_reason: "stale_challenge: challenge is 312 seconds old (max 300)"` The window is a **protocol-level constant**, not a per-`Verify` option. If you need a tighter window for a high-stakes surface, apply the check yourself before calling Verify — the spec does not currently expose `freshness_max_seconds` on `VerifyOptions`. If you'd benefit from configurability per surface, file an issue; that's a v1.1 candidate. ### Clock skew There is **no built-in clock-skew tolerance**. `challengeAge < 0` means *any* timestamp in the future fails. Both ends are expected to be NTP-synchronized. If you need to tolerate a small forward skew, override `VerifyOptions.Now` with a slightly-back-dated clock — the mechanism is intentionally explicit so misaligned clocks fail loudly instead of silently. ## Why this defeats replay Without freshness: ``` Day 1, 09:00: Eve records a bundle Alice's agent sent to a verifier. Day 8, 09:00: Eve replays the same bundle to the same verifier. ✗ The cert is still within its 7-day expiry — looks valid. ✗ The challenge_sig still verifies — still authored by the agent. ✗ Verifier accepts. ATTACK SUCCESSFUL. ``` With freshness: ``` Day 1, 09:00: Eve records a bundle (challenge_at = day-1 09:00). Day 8, 09:00: Eve replays the same bundle. ✓ Cert still valid. ✓ challenge_sig still verifies. ✗ challenge_at is 7 days old → stale_challenge. → identity_status: invalid, error_reason: stale_challenge ... ``` The freshness check is a single subtraction. It's the cheapest possible defense, and it completely eliminates the long-window replay class of attack. ## SessionContext: defeating cross-verifier forwarding (SPEC §15.1) The base signable (challenge + timestamp) doesn't bind a bundle to *which* verifier the agent intended to talk to. A malicious proxy that knows the agent will hit verifier X can forward the agent's bundle to verifier Y instead — both have the same `(challenge, challenge_at)` the proxy passed through. `SessionContext` closes the loop. The verifier sets a per-session 32-byte hash; the agent signs `challenge || ts || session_context`; the verifier compares `bundle.SessionContext == opts.SessionContext` byte-for-byte and re-verifies the signature. A bundle signed for verifier X is now unforgeable for verifier Y. ```go opts := ratify.VerifyOptions{ RequiredScope: "payment:execute", SessionContext: mySessionHash, // 32 bytes } result := ratify.Verify(&bundle, opts) ``` Length is enforced: anything other than 0 or 32 bytes is `invalid_session_context`. ## StreamID + StreamSeq: defeating reorder/replay within a stream For v1.1 stream-bound flows (a sequence of bundles sharing one logical session), the agent binds `stream_id` (32 bytes) and `stream_seq` (≥1, monotonic) into the same signable. The verifier's `StreamContext.LastSeenSeq` tracks the highest seq seen so far; a bundle with `stream_seq <= LastSeenSeq` is rejected (proxy can't replay or reorder within the stream). Both bindings are **optional** — the base signable remains the v1.0 path and conformant verifiers handle both without flags. ## What about per-challenge nonce tracking? You could store every issued challenge and check that no challenge is presented twice. **The protocol does not currently require this**, because it would force every verifier to maintain durable state, which: - Breaks offline verifiers (drones, vehicles, edge-inference appliances) - Forces clustered verifiers to share challenge-tracking state - Adds DB load on every Verify call Be clear-eyed about what that trade-off means: the freshness check **bounds** replay to the challenge window (300 seconds); it does not make presentations single-use. A captured bundle can be re-presented to the same verifier within that window unless the verifier tracks the challenges it issued and consumes each one exactly once. `session_context` narrows what a captured bundle is good for, but it does not prove a challenge is still outstanding. If your deployment needs single-use presentations — task handoffs, payments, anything where accepting the same authorization twice is itself the harm — track issued challenges with one-time consumption on the verifying side, or dedupe by request ID at the application layer for idempotency-shaped cases. ## Source `crypto.go` → `challengeSignBytes` (canonical byte layout), `SignChallenge` family (SDK entry points), `verify.go` → freshness window and `ChallengeWindowSeconds` constant. ## Where to next - [Delegate → Present → Verify](/concepts/delegate-present-verify/) — the full verifier algorithm - [Revocation](/concepts/revocation/) — defending against compromised keys (different threat) - [Hybrid post-quantum crypto](/concepts/hybrid-pqc/) — why the challenge sig is hybrid --- # Revocation > Signed revocation lists and the RevocationProvider hook for stopping a cert before it naturally expires. Source: https://docs.identities.ai/concepts/revocation/ A `DelegationCert` carries `issued_at` and `expires_at`. The natural way for it to stop being valid is to wait until `now > expires_at`. But sometimes you need it to stop being valid *right now* — the agent's key was leaked, the principal changed their mind, the relationship ended. That's what revocation is for. ``` ┌─────────────────────────────────────────────┐ │ Principal (Alice) │ │ │ │ Decides to revoke a cert she issued │ │ (e.g. agent's key was leaked). │ └──────────────────┬──────────────────────────┘ │ │ 1. Signs a RevocationList: │ issuer_id: │ updated_at: now │ revoked_certs: ["cert-abc", ...] │ signature: hybrid (Ed25519+ML-DSA-65) ▼ ┌─────────────────────────────────────────────┐ │ Distribution │ │ │ │ - Self-hosted by Alice, OR │ │ - Hosted by a registry, OR │ │ - Pushed in real time via RevocationPush │ └──────────────────┬──────────────────────────┘ │ │ consulted by the verifier's │ RevocationProvider on every Verify ▼ ┌─────────────────────────────────────────────┐ │ Verifier │ │ │ │ For each cert in bundle.delegations: │ │ revoked, err := provider.IsRevoked(id) │ │ if err → identity_status: invalid │ │ (revocation_error) │ │ if revoked → identity_status: revoked │ └─────────────────────────────────────────────┘ ``` ## The `RevocationList` wire shape `RevocationList` is the canonical, hybrid-signed payload an issuer publishes (`types.go` → `RevocationList`): ```json { "issuer_id": "92cb0a15572d7a71", "updated_at": 1800000000, "revoked_certs": [ "cert-abc-001", "cert-xyz-042" ], "signature": { "ed25519": "...", "ml_dsa_65": "..." } } ``` That's it — four fields. The list is **signed by the same root key that signed the certs it's revoking**, so consumers can authenticate it without a CA. There's no `version`, `expires_at`, or per-entry metadata on the wire; freshness is managed at the distribution layer via TTL on the fetch, not by an embedded expiry. There is also a v1.1 push variant (`RevocationPush`) — a signed delta carrying `{issuer_id, seq_no, entries, pushed_at, signature}` — for verifiers that maintain a long- lived subscription and need sub-second propagation. Edge / serverless verifiers stick with the pull model. ## The `RevocationProvider` hook (SPEC §17.1) The verifier never fetches a `RevocationList` itself. Revocation is a **provider hook** you plug into `VerifyOptions` — that way the same SDK can talk to a self-hosted endpoint, a managed registry, a Redis cache, or all three in front of each other. The interface is one method: ```go // Go type RevocationProvider interface { IsRevoked(certID string) (bool, error) } ``` ```ts // TypeScript interface RevocationProvider { isRevoked(certID: string): Promise; // throw on lookup failure } ``` ```python # Python class RevocationProvider(Protocol): def is_revoked(self, cert_id: str) -> bool: ... # raise on lookup failure ``` ```rust // Rust pub trait RevocationProvider { fn is_revoked(&self, cert_id: &str) -> Result; } ``` Wire it up by setting `VerifyOptions.Revocation` (Go field name; equivalents in TS/Py/Rust): ```go opts := ratify.VerifyOptions{ RequiredScope: "meeting:attend", Revocation: myProvider, } result := ratify.Verify(&bundle, opts) ``` ## Fail-closed by design The verifier is **fail-closed**: a provider that returns an error fails the cert as `identity_status: invalid` with `error_reason: "revocation_error: ..."`. There is no fallback to "allow because we can't tell" — a verifier that doesn't know revocation state MUST NOT report a cert as valid. The three outcomes: | Provider returns | `identity_status` | `error_reason` | |---|---|---| | `(false, nil)` | continues to next check | — | | `(true, nil)` | `revoked` | `cert-xyz revoked` | | `(_, err)` | `invalid` | `revocation_error: ` | Provider implementations decide their own freshness policy: a `RevocationList` cached in Redis with a 60-second TTL behaves differently from one served directly by Cloudflare KV with edge-replicated push. The `RevocationProvider` interface is intentionally narrow so that policy is *yours*, not the SDK's. ### Legacy `IsRevoked` closure For v1.0 backward compatibility, `VerifyOptions` still accepts a plain closure `IsRevoked: func(certID string) bool`. It is **deprecated** because there's no way to surface lookup failures — the closure must collapse "I don't know" into either `false` (allow) or `true` (deny). It will be removed in `v1.0.0-beta.1`. Use `RevocationProvider` in new code. When both are set, `RevocationProvider` wins. ## ForceRevocationCheck `VerifyOptions.ForceRevocationCheck = true` is the high-stakes path: it signals your provider implementation that it should bypass any local cache and re-fetch the freshest state. The verifier itself doesn't perform the fetch — that's your provider's job — but it does guarantee one safety check: if `ForceRevocationCheck` is true and neither `RevocationProvider` nor `IsRevoked` is set, the bundle is failed with `force_revocation_no_callback`. The caller asked for fresh revocation state but provided no way to check it. ## Sub-delegation: every cert in the chain is checked When the chain is Alice → Agent-A → Agent-B, every cert in `bundle.delegations` is passed through `IsRevoked` independently: ``` For each cert in [A→B, Alice→A]: revoked, err := provider.IsRevoked(cert.cert_id) ... ``` Alice can revoke Alice→A — which transitively kills everything Agent-A sub-delegated, because the chain check fails on the first link. Agent-A can revoke A→B without affecting Alice's relationship with Agent-A. The protocol does **not** support a "revoke everything from Alice" call. Revocation is always per-cert. If you need a global revoke, your provider returns `(true, nil)` for every `cert_id` issued by Alice's root. ## Recommended freshness budgets Suggested TTLs by surface (the protocol doesn't mandate these — they're operational guidance): | Surface | Cache TTL | Max staleness before fail-closed | |---|---|---| | Meeting verification | 60 s | 300 s | | Voice gateway | 30 s | 120 s | | API gateway | 60 s | 300 s | | Physical AI / offline | 5 min | mission-duration cached | Tighter TTL = revocations propagate faster, more network calls. Looser TTL = better availability during partial outages, slower propagation. Pick a max-staleness your provider enforces by returning an error past that threshold — the verifier will then fail closed. ## Revocation vs. short expiry | | Revocation | Short `expires_at` | |---|---|---| | Mechanism | `RevocationProvider.IsRevoked` per Verify | Plain timestamp check, no I/O | | Propagation speed | Seconds (TTL-bounded) | Bounded by cert lifetime | | Verifier cost | Network call (cacheable) | None | | Operational burden | Maintain a list endpoint or registry | None | | Best for | Compromised keys, terminated relationships | Routine refresh, blast-radius shrinkage | Common pattern: **short expiry + revocation together**. Issue certs with 1-hour expiry by default, refresh on a heartbeat, only revoke when something goes wrong. Worst case is a 1-hour blast radius even if revocation hosting is completely down. ## Source `RevocationList` and `RevocationPush` wire shapes: `types.go`. `RevocationProvider` interface and the verifier integration: `verify.go`. SPEC.md §11 (revocation) and §17.1 (provider hook) are normative. ## Where to next - [Provider architecture](/concepts/provider-architecture/) — all §17 hooks (revocation, policy, audit, …) - [Key custody](/concepts/key-custody/) — what to do when a private key is actually leaked - [Verify overview](/verify/overview/) — managed revocation hosting if you don't want to run it yourself --- # Key custody > Three custody modes for principal keys — self-custody, custodial, and self-custody upgrade — with their threat models. Source: https://docs.identities.ai/concepts/key-custody/ The protocol is silent on **where** a principal's hybrid private key lives. It just specifies the signing operations the key must support. This page describes the three custody modes the SDK and Verify support, the tradeoffs, and when to use each. ``` ┌─────────────────────────────────┐ │ Principal's hybrid private │ │ key │ │ │ │ ▸ Ed25519 component │ │ ▸ ML-DSA-65 component │ └────────────┬────────────────────┘ │ ┌─────────────────────┼─────────────────────┐ ▼ ▼ ▼ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────┐ │ Self-custody │ │ Custodial │ │ Self-custody upgrade │ │ │ │ │ │ │ │ Key lives on │ │ Key lives in │ │ Started custodial. │ │ user's device. │ │ Verify backend, │ │ Generated a new key │ │ Never sent over │ │ envelope-encr- │ │ on device. Signed a │ │ the wire. │ │ ypted by Cloud │ │ rotation statement │ │ │ │ KMS. │ │ by BOTH old +new. │ │ Strongest. │ │ │ │ Continuous identity │ │ │ │ Convenient for │ │ with strictly │ │ │ │ most teams. │ │ stronger custody │ │ │ │ │ │ going forward. │ └──────────────────┘ └──────────────────┘ └──────────────────────┘ ``` ## Self-custody The user generates their hybrid keypair locally — on their device, in their browser, on their phone, on their server — and the private key never leaves that environment. ```ts // User generates keypair locally const { root, privateKey } = await generateHumanRoot(); // Only the public root.id and root.public_key are ever shared await publishToRegistry(root.id, root.public_key); // Delegations are signed locally with the private key const cert = { /* fields */ }; await issueDelegation(cert, privateKey); // privateKey never leaves this environment ``` ### Threat model | Threat | Mitigation | |---|---| | Server-side key extraction | Not applicable — the key isn't on a server | | Insider at Identities AI accesses keys | Not applicable — Identities AI never has the key | | Compromised laptop/device | Real threat. Use OS keychain / Secure Enclave / TPM. | | Lost device with no backup | Real threat. The user must back up the key (encrypted at rest) or accept that losing the device means losing the identity. | Self-custody is the strongest mode by definition: there is no third party to compromise. But it puts operational burden on the user (backups, device sync, recovery). For technical users this is acceptable; for typical enterprise end-users it's a UX challenge. ## Custodial The Verify backend generates the keypair server-side and stores it under **envelope encryption**: ``` ┌──────────────────────────────────────────────┐ │ Ratify Verify backend │ │ │ │ 1. Generate hybrid keypair in-memory │ │ 2. Generate a fresh DEK (data enc key) │ │ 3. Encrypt private key bytes with DEK │ │ using AES-256-GCM │ │ 4. Encrypt DEK with Cloud KMS KEK │ │ (key encryption key) │ │ 5. Store {encrypted_priv, encrypted_DEK} │ │ in DB │ │ 6. Wipe in-memory plaintext │ │ │ └────────────┬─────────────────────────────────┘ │ │ To sign a delegation: │ - Fetch encrypted record │ - Decrypt DEK via Cloud KMS API │ - Decrypt private key with DEK │ - Sign in-memory │ - Wipe plaintext ▼ ┌──────────────────────────────────────────────┐ │ At rest, Verify only has: │ │ - encrypted private key (AES-GCM) │ │ - encrypted DEK (Cloud KMS-wrapped) │ │ │ │ Cannot sign without both DB access AND │ │ IAM permission on the Cloud KMS KEK. │ └──────────────────────────────────────────────┘ ``` ### Threat model | Threat | Mitigation | |---|---| | Database leak alone | Encrypted keys are useless without the Cloud KMS KEK | | Cloud KMS compromise alone | DEKs are useless without the DB | | Identities AI insider | Requires both DB access AND KMS access. Audit-logged. Need-to-know. | | Verify service compromise | Real threat. Verify must be hardened. SOC 2 / ISO 27001 in progress. | | Quantum adversary in the future | Same as self-custody — hybrid signatures defend against this | Custodial is the right default for enterprise SaaS use cases: users sign in, the system handles keys, no one loses their identity to a dead laptop. The tradeoff is trust: you trust Identities AI's operational controls, audit posture, and incident response. ## Self-custody upgrade A user who started in custodial mode can **migrate to self-custody** at any time without losing their identity. The mechanism is a `KeyRotationStatement` signed by **both** the old (custodial) key and the new (device) key. ``` ┌──────────────────────────────────────────┐ │ User's account in Verify: │ │ - custodial key (in KMS) — old_root │ │ - delegations issued by old_root │ └────────────────┬─────────────────────────┘ │ │ User decides to migrate │ ▼ ┌──────────────────────────────────────────┐ │ User generates fresh hybrid keypair │ │ on their device → new_root │ └────────────────┬─────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ KeyRotationStatement: │ │ old_id: old_root.id │ │ old_pub_key: old_root.public_key │ │ new_id: new_root.id │ │ new_pub_key: new_root.public_key │ │ rotated_at: now │ │ reason: "self_custody_upgrade" │ │ signature_old: signed by old_root │ │ signature_new: signed by new_root │ └────────────────┬─────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ Anyone with old_root.public_key OR │ │ new_root.public_key can verify the │ │ continuity: same person, new key. │ │ │ │ Existing delegations issued by old_root│ │ stay valid until expiry. New ones must │ │ be issued by new_root. │ └──────────────────────────────────────────┘ ``` ```ts // User generates a NEW keypair on their device const { root: newRoot, privateKey: newPrivateKey } = await generateHumanRoot(); // Rotation statement signed by BOTH old (custodial) and new (device) keys const stmt = { version: 1, old_id: oldRoot.id, old_pub_key: oldRoot.public_key, new_id: newRoot.id, new_pub_key: newRoot.public_key, rotated_at: Math.floor(Date.now() / 1000), reason: "self_custody_upgrade", signature_old: { ed25519: new Uint8Array(), ml_dsa_65: new Uint8Array() }, signature_new: { ed25519: new Uint8Array(), ml_dsa_65: new Uint8Array() }, }; await issueKeyRotationStatement(stmt, oldCustodialPrivateKey, newPrivateKey); // Verify accepts new_root signatures going forward. // Old_root signatures still verify against old delegations until those expire. ``` The double-signature is what proves continuity. Without the old key's signature, anyone could claim to be the rotation target. Without the new key's signature, anyone could replay the statement. Both are required. ## Picking a mode | You're building… | Recommended mode | |---|---| | A research prototype | Self-custody | | A consumer agent (browser/mobile) | Custodial, with optional self-custody upgrade later | | Enterprise SaaS where users sign in via SSO | Custodial — that's what enterprise IT expects | | A regulated deployment (SOX, HIPAA, FINRA) | Custodial initially, with a documented self-custody upgrade path for users who request it | | An embedded device (drone, vehicle) | Self-custody using the device's secure element (TPM, ARM TrustZone, Secure Enclave) | | An OSS project distributing identities | Self-custody, no Verify needed | ## Where to next - [Revocation](/concepts/revocation/) — what to do if a key is compromised regardless of custody mode - [Ratify Verify overview](/verify/overview/) — what custodial Verify operationally looks like - [Hybrid post-quantum crypto](/concepts/hybrid-pqc/) — the underlying signature primitives --- # Hybrid post-quantum crypto > Why every Ratify signature is two signatures, what each algorithm contributes, and what threats hybrid mode actually defends against. Source: https://docs.identities.ai/concepts/hybrid-pqc/ Every Ratify signature is a **pair**: one Ed25519, one ML-DSA-65. **Both must verify.** This page explains why, what each algorithm is doing, and what threats this defends against. ``` HybridSignature ─────────────── │ ┌────────┼────────┐ ▼ ▼ ┌─────────────┐ ┌──────────────────┐ │ Ed25519 │ │ ML-DSA-65 │ │ signature │ │ signature │ │ │ │ │ │ RFC 8032 │ │ NIST FIPS 204 │ │ Elliptic │ │ Lattice-based │ │ curve │ │ Module-LWE │ │ │ │ │ │ 64 bytes │ │ ~3309 bytes │ │ fast │ │ ~10x slower │ │ │ │ ~50x larger │ └─────────────┘ └──────────────────┘ │ │ └─────── BOTH ────┘ must verify ``` ## The signing function ``` sign_bytes = canonical_json(object, omit: ["signature"]) signature.ed25519 = Ed25519.Sign(sign_bytes, private_key.ed25519) signature.ml_dsa_65 = ML-DSA-65.Sign(sign_bytes, private_key.ml_dsa_65) ``` Both signatures are over the **same canonical bytes**. The two component keys are unrelated — they share no math, no curve, no seed. The hybrid keypair is the cartesian product of two independent keypairs. ## The verifier function ``` Valid(σ, msg, pub) := Ed25519.Verify(msg, σ.ed25519, pub.ed25519) ∧ ML-DSA-65.Verify(msg, σ.ml_dsa_65, pub.ml_dsa_65) ``` Logical AND. Failure in either means the whole signature is rejected. This is the structural property — the hybrid defends against algorithm failure in either component because failure in one cannot mask the other. ## Threat model ``` Today Post-quantum future ───── ─────────────────── Forge a sig with quantum computer no key access? ✗ Practically ✗ ML-DSA-65 has no infeasible known quantum attack; shorter Ed25519 is broken but doesn't help — the ML-DSA-65 sig still fails Recover priv key quantum computer from sig alone? ✗ Practically ✗ Same — ML-DSA-65 is infeasible conjectured Q-secure Forge old archived quantum computer sigs (HND attack)? — N/A ✗ ML-DSA-65 holds. Ed25519 alone would be retroactively forgeable; hybrid mode prevents this. Algorithm flaw Both algorithms used ↓ discovered in in production with Even if a flaw appears in one of them? decade+ of crypt- ML-DSA-65 (newer, less analysis (Ed25519) battle-tested), Ed25519 and FIPS standard- still verifies. ization (ML-DSA-65, FIPS 204 final August 2024). ``` The structural argument: **as long as at least one of (Ed25519, ML-DSA-65) is secure, the hybrid signature is secure.** You need both to be broken to forge a bundle. That's the defense in depth. ## "Harvest now, decrypt later" (HND) The specific attack the hybrid mode is designed to prevent: 1. An adversary records every Ratify proof bundle that crosses the public internet today, in 2026. They store these archives at low cost. 2. Years later, a cryptographically-relevant quantum computer (CRQC) becomes available to the adversary. 3. The CRQC can attack Ed25519 (via Shor's algorithm on the elliptic curve discrete log problem). The adversary can now compute the Ed25519 private key from any 2026 public key they recorded. 4. With a forged Ed25519 private key, they can produce Ed25519 signatures over arbitrary 2026-dated payloads. **In a non-hybrid system, this means they could retroactively claim any agent was authorized to do anything in 2026.** The hybrid defense: even with the Ed25519 private key, the adversary cannot produce a valid hybrid signature because they don't have the ML-DSA-65 private key. ML-DSA-65 is lattice-based (Module-LWE) and has no known quantum attack. The forged Ed25519 component verifies in isolation, but the verifier requires both — so the forgery is rejected. ## Why specifically Ed25519 + ML-DSA-65? | Choice | Why | |---|---| | **Ed25519** (not RSA, not ECDSA) | Faster, smaller signatures, no nonce-reuse pitfall, widely deployed, mature libraries everywhere, RFC 8032 standardized | | **ML-DSA-65** (not Falcon, not Dilithium) | ML-DSA-65 is the FIPS 204 standardized form of Dilithium-3, finalized Aug 2024. Wider tooling support than Falcon. Conservative security level (≈ AES-192). | | **Hybrid pair** (not pure PQC) | PQC algorithms are new. ML-DSA-65 has years of cryptanalysis but less than Ed25519. Hybrid mode means a flaw in ML-DSA-65 doesn't sink us. | This combination is what CNSA 2.0 (US NSA's commercial cryptography guidance) and BSI (German federal cyber agency) both recommend for the post-quantum transition period. ## Size tradeoff Hybrid signatures are larger: ``` Ed25519 only: 64 bytes per signature ML-DSA-65 only: 3309 bytes per signature Hybrid: 3373 bytes per signature (~50× larger than Ed25519 alone) ``` For a ProofBundle with one delegation, the wire size is roughly: ``` Cert metadata + scopes + IDs: ~400 bytes (varies by scope count) Issuer public key (hybrid): ~2000 bytes Subject public key (hybrid): ~2000 bytes Delegation signature (hybrid): ~3400 bytes Challenge + challenge_sig: ~3500 bytes Total wire size: ~11–12 KB per bundle ``` For a sub-delegated chain at depth 3: ~25–30 KB. The bundles are bigger than OAuth bearer tokens (sub-1KB) but fit easily inside any HTTP request and are negligible compared to a video frame, an LLM completion, or a meeting handshake. ## Performance tradeoff ``` Operation Ed25519 ML-DSA-65 Hybrid ───────────────────── ─────── ───────── ─────── Keygen ~50 μs ~120 μs ~170 μs Sign ~50 μs ~400 μs ~450 μs Verify ~150 μs ~250 μs ~400 μs ``` (Measured on a modern x86 CPU. Embedded targets are slower; ML-DSA-65 in particular benefits a lot from vectorization.) **Verification is the hot path** in real workloads. Roughly 400 μs per bundle, which means a single CPU core can process ~2500 bundles per second. For Verify's edge verifier (Cloud Run / edge worker) this is well below saturation. ## Source The exact algorithms, key encodings, and signature encodings are normative: [SPEC.md §5](https://github.com/identities-ai/ratify-protocol/blob/main/SPEC.md). Every SDK uses an audited implementation of each primitive: - Go: `crypto/ed25519` (standard library) + `pqcrypto/mldsa` (Cloudflare CIRCL) - TypeScript: `@noble/ed25519` + `@noble/post-quantum` - Python: `cryptography` (PyCA) + `pqcrypto` - Rust: `ed25519-dalek` + `pqcrypto-mldsa` ## Where to next - [Key custody](/concepts/key-custody/) — where the hybrid private key actually lives - [Conformance suite](/reference/conformance/) — how byte-identical hybrid signatures are verified across SDKs - [Delegate, Present, Verify](/concepts/delegate-present-verify/) — how the hybrid signature fits into the protocol --- # SDK packages > Where each SDK is published, current versions, and how to verify byte-for-byte interop. Source: https://docs.identities.ai/sdk/packages/ The Ratify Protocol ships reference SDKs in five languages. **All five are byte-for-byte interoperable** — they produce identical canonical JSON, identical hybrid signatures, and pass the same 79-fixture conformance suite. ## Current published versions | Language | Registry | Package | Version | Install | |---|---|---|---|---| | **Go** | [pkg.go.dev](https://pkg.go.dev/github.com/identities-ai/ratify-protocol) | `github.com/identities-ai/ratify-protocol` | v1.0.0-alpha.20 | `go get github.com/identities-ai/ratify-protocol@v1.0.0-alpha.20` | | **TypeScript** | [npm](https://www.npmjs.com/package/@identities-ai/ratify-protocol) | `@identities-ai/ratify-protocol` | 1.0.0-alpha.20 | `npm install @identities-ai/ratify-protocol@1.0.0-alpha.20` | | **Python** | [PyPI](https://pypi.org/project/ratify-protocol/) | `ratify-protocol` | 1.0.0a20 | `pip install ratify-protocol==1.0.0a20` | | **Rust** | [crates.io](https://crates.io/crates/ratify-protocol) | `ratify-protocol` | 1.0.0-alpha.20 | `cargo add ratify-protocol@1.0.0-alpha.20` | | **C/C++** | [GitHub Releases](https://github.com/identities-ai/ratify-protocol/releases) | `libratify_c.a` / `libratify_c.so` | 1.0.0-alpha.20 | Download pre-built archive or build from source — see [C/C++ SDK](/sdk/c/) | All five versions are released **atomically** — every tagged release of the protocol contains the same version of every SDK. A consumer using v1.0.0-alpha.20 in Go and v1.0.0-alpha.20 in Python is guaranteed to produce and verify byte-identical bundles. ## What's new in v1.0.0-alpha.20 Three releases published since alpha.17. No canonical bytes, fixture contents, wire format or verifier behavior change in any of them, the protocol version remains 1, and all 79 fixtures are byte-identical to alpha.17. **C SDK: context-bound proof bundles (alpha.20).** `ratify_proof_bundle_create_with_context` builds a ProofBundle carrying a 32-byte session context, so a C caller can now produce a context-bound proof rather than only verify one. Go, Rust, Python and TypeScript already had an equivalent entry point. Both the challenge and the session context must be exactly 32 bytes. Existing symbols are unchanged, so the addition is ABI-compatible for callers already linked against alpha.19. **TypeScript: base64 decoder backtracking (alpha.19).** The fallback decoder stripped trailing padding with an expression that backtracked polynomially, measured at 3.4 seconds for 80 000 characters. It is reachable only where `Buffer` is undefined, which means browsers, Deno and edge runtimes. The strip now scans backwards and is linear. **Rust and C: minimum SDK surface completed (alpha.18).** Both gained the operations the published surface documents but did not expose, and the two C conformance sign-byte assertions the suite was documented to make are now actually made. **Python: time-window denial without a system time-zone database (alpha.18).** Constraint evaluation denied `time_window` on hosts carrying no tz database, which affected Windows and slim containers on every earlier release. `tzdata` is now an unconditional dependency. A separate `ratify-protocol-native` distribution provides deterministic seed-based key generation; `ratify-protocol` itself remains pure Python. ## What was new in v1.0.0-alpha.17 A packaging and documentation release. No SDK behavior, canonical bytes or fixture contents change, and all 79 fixtures are byte-identical to alpha.16. **Python SDK dependency bound.** `pqcrypto` 1.0.0, published 2026-08-15, changed ML-DSA-65 behavior, and hybrid signatures do not verify against it. The Python package declared `pqcrypto>=0.3.4` with no upper bound, so any installation made from that date produced a verifier that rejected valid signatures, including the canonical fixtures. The dependency is now bounded to `>=0.3.4,<1.0`. No other SDK depends on `pqcrypto`. Anyone who installed `1.0.0a17` on or after 2026-08-15 should upgrade, or pin `pqcrypto<1.0` alongside it. **Extension type namespaces (SPEC §17.7).** Newly defined extension constraint types use a reverse-domain prefix controlled by the defining organization; Ratify profiles use `com.ratifyprotocol..`. Existing signed names, verifier behavior, SDK APIs and canonical fixture bytes are unchanged. ## What was new in v1.0.0-alpha.16 The resource-bound authority release. It is additive: existing signed objects remain byte-stable, and the conformance suite grows from 63 to 79 fixtures for the new constraint and bounds behavior. **Resource-bound authority (`resource_path` constraint, SPEC §5.7.2, §5.7.3).** A delegation can now name *where* a scope applies, not just *what* it permits. A `resource_path` constraint binds authority to an opaque `resource_id` (compared by exact byte equality, never dereferenced or normalized) and an optional `path_prefix` under segment-boundary matching. Chain evaluation stays conjunctive, so effective authority can narrow but never widen. **Resource-identifier profiles (SPEC §5.7.4).** The shared recipes that make an opaque `resource_id` interoperable, with a Git profile v1 and known-answer plus negative vectors. Profiles are versioned outside the core spec and invisible to parties that do not adopt them; byte-equality remains the only comparison. **Extension-constraint parameters (SPEC §5.7.1, §17.7).** Parameterized extension constraints are now representable in signed certificates under a restricted, cross-language-deterministic value model. Type-only extension constraints serialize exactly as before, so existing signed certs stay byte-stable. **Input bounds (SPEC §5.1).** `MAX_PROOF_BUNDLE_BYTES` (128 KiB, applied to the received wire representation and enforced before parsing), `MAX_JSON_NESTING_DEPTH`, and per-certificate scope and constraint count and length limits. Violations route to the existing `invalid` status. **Deeper chains.** `MAX_DELEGATION_CHAIN_DEPTH` rises from 3 to 8 for multi-hop agent topologies. The ceiling is a wire-determinism and denial-of-service bound, not a cryptographic limit; the new input bounds cap the work that depth alone does not. **Cross-SDK hashing correctness.** `bundle_hash` in the Python and TypeScript SDKs now canonicalizes delegation constraints by kind, so a `resource_path`-bound bundle produces the same digest across every SDK. A constrained cross-SDK vector guards against regression. ## What was new in v1.0.0-alpha.15 The integration-readiness release — no wire-format changes; all 63 fixtures are byte-identical to alpha.13. **Public wire codecs (TypeScript, Python).** `encodeProofBundle`/`decodeProofBundle` and friends for certs, bundles, and session tokens — strict decoding (duplicate keys, out-of-domain integers, invalid UTF-8, and unknown fields are rejected), with cross-SDK round-trip parity tests. No more hand-written transport glue. **Single-use challenges (SPEC §10, normative).** A `ChallengeStore` in every SDK makes verifier-issued challenges single-use: validated before signature work, atomically consumed after the challenge signature verifies. A captured bundle no longer replays inside the freshness window at an issuing verifier. **Streamed verification with options (SPEC §5.13).** The session-token fast path now enforces a required scope, single-use challenges, and session/stream bindings through a dedicated options object in all five SDKs. **Operation and session context (SPEC §6.4.9) + Middleware Custody Profile (SPEC §15.2.1).** Canonical, domain-separated constructions that bind a presentation to the verifier, workspace, agent, session, invocation, and the specific operation — and a named conformance profile for platforms whose middleware signs on behalf of hosted agents. **Vocabulary parity + honest performance docs.** `vocabulary()`/`scopeWildcards()` in every SDK, and a per-SDK benchmark matrix and wire-size table replace one-size-fits-all latency claims. ## What was new in v1.0.0-alpha.14 A metadata and tooling release — no wire-format changes; all 63 fixtures are byte-identical to alpha.13. The canonical project description ("delegated-authority proofs for human-agent and agent-agent interactions") now ships in every registry's metadata and README, the npm install pin is corrected (it had lagged at alpha.10 on the npm page), and both Rust crates are clippy-clean with lint gating in CI. ## What was new in v1.0.0-alpha.13 **Registry read binding (SPEC §13.1).** The optional lookup contract behind registry-mode key discovery: `GET /v1/registry/principals/{human_id}` returns the principal's current root key, the full key-rotation chain, and the optional anchor — so any registry (the managed Verify service or a third party) is interchangeable. TLS-mandatory, fail-closed resolver semantics, and explicit first-trust framing: rotation proves continuity *after* first trust; it never creates it. **Reference resolver.** `cmd/ratify-verifier` gains `--registry ` (registry trust: operator + TLS) and `--registry-pins` + `--registry-require-pinned` (pin-plus-registry: only first-trusted principals and their rotation successors). ## What was new in v1.0.0-alpha.12 **No-expiry sentinel.** A `DelegationCert` whose `expires_at` equals `NO_EXPIRY_SENTINEL = 4070908799` (2099-12-31 23:59:59 UTC) means "no expiry (until revoked)". Display and policy code must branch on the sentinel; every SDK ships a helper (`IsNoExpiry()` in Go, `isNoExpiry()` in TypeScript, `is_no_expiry()` in Python and Rust, `ratify_expires_at_is_no_expiry()` in C). Verification is unchanged; revocation is the sole termination mechanism for such certs. **`presence:represent` scope (sensitive).** The canonical vocabulary grows to 54 scopes: an agent may attend and interact as a direct representative of the principal. No `presence:*` wildcard; does not imply `identity:prove`. See [Scopes](/concepts/scopes/). **Verifier scope-vocabulary validation.** A cert granting a scope that is not canonical, not a wildcard, and not a `custom:` extension is now rejected with the new identity status `invalid_scope`, before any effective-scope arithmetic. **Conformance suite grows to 63 canonical fixtures** — four new fixtures pin the additions above; all pre-existing fixtures are byte-identical to the previous release. ## What was new in v1.0.0-alpha.10 **C/C++ SDK — full conformance-suite parity.** All canonical fixture kinds now pass through the C ABI: verify, scope, revocation, revocation_push, key_rotation, session_token, transaction_receipt, and witness_entry. 13 new exported functions added to the C API (`ratify_*_sign_bytes_hex`, `ratify_verify_streamed_turn`, `ratify_transaction_receipt_verify_full`). Pre-built libraries for common targets now published as GitHub Release assets — no Rust toolchain required to use the C SDK. ## What was new in v1.0.0-alpha.9 **SDK README overhaul** — consistent structure and cross-language framing across all five SDKs. npm Trusted Publisher (OIDC) configured; `@identities-ai/ratify-protocol` now available directly from npm. ## What was new in v1.0.0-alpha.8 **C/C++ SDK** — `libratify_c.a` (static) and `libratify_c.so` (shared) with a cbindgen-generated `ratify.h` header (includes C++ `extern "C"` guards). Supports embedded RTOS targets (FreeRTOS, Zephyr) via custom entropy. **Provider hooks (SPEC §17)** — the verifier's deterministic core is bracketed by pluggable hooks: - **`RevocationProvider` / `PolicyProvider` / `AuditProvider`** (§17.1–§17.3) - **`VerificationReceipt`** (§17.5) — verifier-signed, hash-chained audit attestation - **`PolicyVerdict`** (§17.6) — HMAC-bound cached policy decision - **`ConstraintEvaluator` registry** (§17.7) — pluggable extension constraint evaluators - **`AnchorResolver`** (§17.8) — identity-bound receipt anchoring - **`IsRevoked` closure** deprecated (§17.1 `RevocationProvider` is the replacement; removal scheduled for v1.0.0-beta.1) See [Provider architecture](/concepts/provider-architecture) for the full picture. ## Verifying you've installed the real thing Each registry exposes the source of every published version. To prove the package you installed matches the repo: ```bash # Python — PyPI lets you download the wheel directly pip download ratify-protocol==1.0.0a20 --no-deps -d /tmp # Compare the wheel's sha256 against the GitHub Release's sha256 # Rust — crates.io publishes via the same git tag cargo install --version 1.0.0-alpha.20 --git https://github.com/identities-ai/ratify-protocol # Then run cargo test in the install dir → all 79 fixtures pass # Go — the Go module proxy serves directly from the GitHub tag GOPROXY=https://proxy.golang.org go get github.com/identities-ai/ratify-protocol@v1.0.0-alpha.20 # pkg.go.dev shows the source view linked back to the GitHub repo # C/C++ — build from source or download pre-built libraries from GitHub Releases git clone https://github.com/identities-ai/ratify-protocol cd ratify-protocol/sdks/c cargo build --release # → target/release/libratify_c.a + libratify_c.so + include/ratify.h ``` The conformance fixtures are the practical interop check: any SDK passing all 79 produces the same canonical bytes as every other SDK at that version. ## Publishing process All five SDKs are published from a single CI pipeline triggered by a `v*` tag: ``` git tag v1.0.0-alpha.20 git push origin v1.0.0-alpha.20 │ ▼ ┌─────────────────────────────────────────────┐ │ .github/workflows/release.yml fires │ │ │ │ 1. gate-tests: │ │ - Go tests + race detector │ │ - Test vectors regenerate byte-ident. │ │ - Release-sync (versions aligned) │ │ - TS conformance (79 fixtures) │ │ - Python conformance (79 fixtures) │ │ - Rust conformance (79 fixtures) │ │ - C conformance (79 fixtures) │ │ - Tag ↔ SDK version coherence │ │ │ │ 2. If all pass: │ │ - publish-pypi (Trusted Publisher OIDC) │ │ - publish-crates (token) │ │ - publish-npm (token, when enabled) │ │ - publish-go (pkg.go.dev auto-warm) │ │ - github-release (notes + testvectors │ │ + pre-built C libraries) │ └─────────────────────────────────────────────┘ ``` Any conformance failure blocks the entire release. There is no "publish Python but skip the broken Rust" path — versions stay aligned by design. See the protocol repo's [docs/RELEASES.md](https://github.com/identities-ai/ratify-protocol/blob/main/docs/RELEASES.md) for the full release process and recovery procedures. ## Pre-release versions During the alpha series, **fixture bytes may change between versions.** Each alpha release documents the diff in the GitHub Release notes. Consumers pinning to `alpha.6` and `alpha.7` should expect a small migration. After `1.0.0` stable ships, fixture bytes are **frozen for the entire v1 lifetime**. Minor and patch releases add new optional fields and new scopes without changing existing byte representations. ## Where to next - Per-language SDK quickstarts: [Go](/sdk/go/) · [TypeScript](/sdk/typescript/) · [Python](/sdk/python/) · [Rust](/sdk/rust/) · [C/C++](/sdk/c/) - [Versioning](/reference/versioning/) — full semver scheme + the alpha → stable ladder - [Conformance suite](/reference/conformance/) — what the 79 fixtures check and how to add a new SDK --- # Go SDK > Use the Go reference SDK to issue delegations, build proof bundles, and verify them. Source: https://docs.identities.ai/sdk/go/ The Go SDK is the reference implementation of the Ratify Protocol. Every code path you'll see here is the same path that emits the [conformance fixtures](/reference/conformance/) every other SDK (TypeScript, Python, Rust, and C/C++) is validated against. **Stability:** every primitive on this page is stable in `v1.0.0-alpha.20`. There are no preview / experimental APIs in the snippets below. ## Install ```bash go get github.com/identities-ai/ratify-protocol@v1.0.0-alpha.20 ``` Module: `github.com/identities-ai/ratify-protocol`. Go 1.22+. ## Three minutes, end to end This is the entire protocol in one file: Alice creates her root identity, signs a delegation to her agent, the agent builds a proof bundle, and a verifier runs the verifier algorithm. No HTTP, no servers — just function calls. ```go package main import ( "fmt" "time" "github.com/identities-ai/ratify-protocol" ) func main() { // 1. Alice generates her hybrid (Ed25519 + ML-DSA-65) root identity. alice, alicePriv, err := ratify.GenerateHumanRootKeypair() if err != nil { panic(err) } // 2. Her AI agent generates its own hybrid keypair. agent, agentPriv, err := ratify.GenerateAgentKeypair("Alice's Scheduler", "custom") if err != nil { panic(err) } // 3. Alice signs a delegation cert for the agent. now := time.Now().Unix() cert := &ratify.DelegationCert{ CertID: "cert-001", Version: ratify.ProtocolVersion, IssuerID: alice.ID, IssuerPubKey: alice.PublicKey, SubjectID: agent.ID, SubjectPubKey: agent.PublicKey, Scope: []string{ratify.ScopeMeetingAttend, ratify.ScopeMeetingSpeak}, IssuedAt: now, ExpiresAt: now + 7*24*3600, } if err := ratify.IssueDelegation(cert, alicePriv); err != nil { panic(err) } // 4. Verifier issues a challenge. Agent signs it. challenge, err := ratify.GenerateChallenge() if err != nil { panic(err) } challengeAt := time.Now().Unix() challengeSig, err := ratify.SignChallenge(challenge, challengeAt, agentPriv) if err != nil { panic(err) } // 5. Agent assembles a proof bundle. Note: Delegations is a slice of // values, not pointers — the SDK takes []DelegationCert. bundle := &ratify.ProofBundle{ AgentID: agent.ID, AgentPubKey: agent.PublicKey, Delegations: []ratify.DelegationCert{*cert}, Challenge: challenge, ChallengeAt: challengeAt, ChallengeSig: challengeSig, } // 6. Verifier runs the verifier in a single call. result := ratify.Verify(bundle, ratify.VerifyOptions{ RequiredScope: ratify.ScopeMeetingAttend, }) if result.Valid { fmt.Printf("✓ Authorized\n") fmt.Printf(" human_id: %s\n", result.HumanID) fmt.Printf(" agent_id: %s\n", result.AgentID) fmt.Printf(" granted_scope: %v\n", result.GrantedScope) fmt.Printf(" identity_status: %s\n", result.IdentityStatus) } else { fmt.Printf("✗ Rejected: %s — %s\n", result.IdentityStatus, result.ErrorReason) } } ``` Run it with `go run main.go`. Output (`human_id` and `agent_id` are derived from the freshly generated keys, so they'll differ on every run): ``` ✓ Authorized human_id: 2254340cfd7bd2058c2813bb381a98e7 agent_id: 6fc06ca1155498bff675a6048da797af granted_scope: [meeting:attend meeting:speak] identity_status: authorized_agent ``` ## JSON transport — sending a bundle over the wire A proof bundle is sent as JSON. The issuer serializes it; the verifier deserializes and passes it straight to `Verify`. The SDK's canonical JSON round-trip is byte-identical across all languages — a bundle produced by the TypeScript SDK verifies in Go (and vice versa). ```go import "encoding/json" // Agent side: serialize for transport (HTTP header, message queue, etc.) bundleBytes, err := json.Marshal(bundle) if err != nil { panic(err) } // Send bundleBytes over your transport channel... // Verifier side: deserialize and verify var received ratify.ProofBundle if err := json.Unmarshal(received_bytes, &received); err != nil { // Reject — malformed JSON, not a valid bundle http.Error(w, "invalid proof bundle", http.StatusBadRequest) return } result := ratify.Verify(&received, ratify.VerifyOptions{ RequiredScope: ratify.ScopeExecuteTool, }) if !result.Valid { http.Error(w, "unauthorized: "+result.IdentityStatus, http.StatusForbidden) return } // Proceed — agent is authorized ``` Standard HTTP convention: agents PUT the bundle JSON in the `X-Ratify-Proof` request header (base64-encoded) or as a JSON body field. See the [Agentic API guide](/guides/api-gateway/) for the full middleware pattern. ## The full sequence, illustrated ``` ┌─────────────┐ ┌─────────────┐ │ Alice │ 1. GenerateHumanRootKeypair │ Verifier │ │ │ 2. IssueDelegation(cert) │ │ │ private │ │ │ │ key on │ │ │ │ her dev │ │ │ └──────┬──────┘ └─────▲───────┘ │ │ │ cert (signed) │ 6. Verify(bundle) ▼ │ → result.Valid ┌─────────────┐ ┌─────┴───────┐ │ AI Agent │ 3. GenerateAgentKeypair │ │ │ │ 4. SignChallenge(nonce) │ │ │ cert + │ 5. ProofBundle{...} │ │ │ own keys │ ────────────────────────────────▶│ │ └─────────────┘ └─────────────┘ Alice's private key never leaves her device. Agent never sees Alice's private key. Verifier never sees any private key — only public keys + signatures. ``` ## Verifier-side: branching on `IdentityStatus` `Verify` always returns a `VerifyResult`. Inspect `result.Valid` first; if it's false, the specific failure mode is in `result.IdentityStatus`. The constants live in the `ratify` package as `IdentityStatus*`: ```go result := ratify.Verify(bundle, ratify.VerifyOptions{ RequiredScope: ratify.ScopeMeetingAttend, }) switch result.IdentityStatus { case ratify.IdentityStatusAuthorizedAgent: // ✓ All checks passed. result.GrantedScope is the intersection of every // cert's scope (effective scope across the chain). case ratify.IdentityStatusExpired: // ✗ At least one cert in the chain is past its expires_at, OR not-yet-valid // (now < issued_at). case ratify.IdentityStatusRevoked: // ✗ A cert ID in the chain matched a revoked entry returned by your // Revocation provider (or legacy IsRevoked closure). case ratify.IdentityStatusScopeDenied: // ✗ RequiredScope was not in the chain's effective (intersected) scope. // Also returned when an advanced PolicyProvider rejects the bundle. case ratify.IdentityStatusConstraintDenied: // ✗ A first-class constraint (geo/time/amount/rate) on some cert // evaluated to "outside the allowed range". case ratify.IdentityStatusConstraintUnverifiable: // ✗ A constraint required input the caller didn't provide // (e.g. cert has geo_circle but VerifierContext.HasLocation == false). case ratify.IdentityStatusConstraintUnknown: // ✗ A cert declared a constraint Type this SDK build doesn't recognize. // Fail-closed by design — future-cert defense. case ratify.IdentityStatusDelegationNotAuthorized: // ✗ An intermediate cert in the chain sub-delegated, but its parent // never granted identity:delegate. The sub-delegation gate. case ratify.IdentityStatusInvalid: // ✗ Catch-all for structural / cryptographic failures: bad signature, // broken chain linkage, malformed key, wrong version, etc. // result.ErrorReason carries the specific code (e.g. "bad_signature", // "broken_chain", "stale_challenge", "key_mismatch"). } ``` The full list of constants lives in [verify.go](https://github.com/identities-ai/ratify-protocol/blob/main/verify.go) — and the `IdentityStatusInvalid` failure modes have stable, machine-readable reason codes inside `ErrorReason` (e.g. `stale_challenge: challenge is 31 seconds old (max 300)`), so your audit pipeline can route on those without parsing prose. The verifier is **fail-closed by default**. Any error path returns `Valid: false` — you cannot accidentally accept a malformed bundle. ## Constraints (geo / time / amount / rate / speed) If a delegation has constraints, supply the runtime context in `VerifyOptions.Context`: ```go result := ratify.Verify(bundle, ratify.VerifyOptions{ RequiredScope: ratify.ScopeDroneDeliver, Context: ratify.VerifierContext{ HasLocation: true, CurrentLat: 37.7749, CurrentLon: -122.4194, }, }) ``` The `Has*` flags on `VerifierContext` are authoritative: when false, the corresponding numeric fields are zeroed in the canonical context hash (this is what keeps `PolicyVerdict` caching portable across SDKs). Setting `CurrentLat = 37` without `HasLocation = true` is equivalent to "no location supplied." Real output for the three branches of a `geo_circle` constraint: ``` Inside circle: valid=true status=authorized_agent 50 km away: valid=false status=constraint_denied reason=constraint_denied: cert 0: constraint[0] (geo_circle): outside allowed radius: 67574.0m > 5000.0m No location supplied: valid=false status=constraint_unverifiable reason=constraint_unverifiable: cert 0: constraint[0] (geo_circle): constraint_unverifiable: no current location in context ``` The full constraint vocabulary (`geo_circle`, `geo_polygon`, `geo_bbox`, `time_window`, `max_speed_mps`, `max_amount`, `max_rate`) and their `VerifierContext` field requirements are documented in [Constraints](/concepts/constraints/). ## 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. The sub-delegation gate is intentional — without `identity:delegate` on A's grant from Alice, A cannot mint chains that B can use. ```go // Alice → A: meeting:attend + identity:delegate aliceToA := &ratify.DelegationCert{ CertID: "cert-alice-to-a", Version: ratify.ProtocolVersion, IssuerID: alice.ID, IssuerPubKey: alice.PublicKey, SubjectID: agentA.ID, SubjectPubKey: agentA.PublicKey, Scope: []string{ratify.ScopeMeetingAttend, ratify.ScopeIdentityDelegate}, IssuedAt: now, ExpiresAt: now + 86400, } _ = ratify.IssueDelegation(aliceToA, alicePriv) // A → B: must be a subset of A's grant aToB := &ratify.DelegationCert{ CertID: "cert-a-to-b", Version: ratify.ProtocolVersion, IssuerID: agentA.ID, IssuerPubKey: agentA.PublicKey, SubjectID: agentB.ID, SubjectPubKey: agentB.PublicKey, Scope: []string{ratify.ScopeMeetingAttend}, IssuedAt: now, ExpiresAt: now + 3600, } _ = ratify.IssueDelegation(aToB, agentAPriv) // Bundle: chain order is [leaf, ..., root]. A→B is the leaf, Alice→A is the root. bundle := &ratify.ProofBundle{ AgentID: agentB.ID, AgentPubKey: agentB.PublicKey, Delegations: []ratify.DelegationCert{*aToB, *aliceToA}, Challenge: challenge, ChallengeAt: challengeAt, ChallengeSig: agentBSig, } result := ratify.Verify(bundle, ratify.VerifyOptions{ RequiredScope: ratify.ScopeMeetingAttend, }) ``` Real output: ``` Valid: true status: authorized_agent human_id: b11b8196e2cf7bc0dd6853065eddd1f8 (Alice) agent_id: c814242c6ad075698554ce93e0e391e3 (Agent B) granted_scope: [meeting:attend] ``` Notice the effective scope is `[meeting:attend]` only — `identity:delegate` doesn't propagate because A didn't grant it onward. The effective scope of any chain is the lex-sorted intersection across every cert. ## Provider hooks (SPEC §17) Beyond the deterministic verifier core, `VerifyOptions` exposes pluggable hooks for revocation, policy, audit, anchor resolution, and extension constraint evaluators. They are all stable; full reference at [Provider architecture](/concepts/provider-architecture/). ```go result := ratify.Verify(bundle, ratify.VerifyOptions{ RequiredScope: ratify.ScopeMeetingAttend, Revocation: myRevocationProvider, // §17.1 Policy: myPolicyProvider, // §17.2 Audit: myAuditProvider, // §17.3 ConstraintEvaluators: myExtensionRegistry, // §17.7 AnchorResolver: myAnchorResolver, // §17.8 Context: ctx, }) ``` Each hook has fail-closed semantics — a `RevocationProvider` that returns an error fails the bundle with `IdentityStatusInvalid` + `error_reason: revocation_error: ...`, not a silent "unknown means allow." Audit hook errors are deliberately swallowed (auditing must not alter the verdict). ## Running the conformance suite ```bash git clone https://github.com/identities-ai/ratify-protocol cd ratify-protocol go test ./... ``` If you see `ok github.com/identities-ai/ratify-protocol` followed by the per-fixture pass log, all 79 canonical wire-format fixtures plus the 10 cross-SDK byte-equivalence vectors passed. Your local build is byte-for-byte interoperable with every other Ratify SDK at the same version. ## CLI The repo ships a `ratify` CLI for human use — create identities, sign delegations, verify bundles, list the canonical scopes. ```bash go build -o /tmp/ratify ./cmd/ratify /tmp/ratify --help ``` Common workflows: ```bash # Create a HumanRoot identity (writes to ~/.ratify/) ratify init # Sign a delegation cert. The agent pubkey JSON must be exported by the # `ratify agent-init` testing command or generated programmatically. ratify delegate \ --agent-pubkey-file agent-pubkey.json \ --scope "meeting:attend,meeting:speak" \ --days 7 \ --out delegation.json # Verify a bundle (note: --bundle is required; no positional argument) ratify verify --bundle bundle.json # Print the canonical 54-scope vocabulary ratify scopes # Show this device's identity state ratify status ``` `ratify agent-init` and `ratify challenge` are agent-side helpers for end-to-end testing without writing Go code; see `ratify --help` for the full surface. ## Where to next - [Protocol concepts: Delegate → Present → Verify](/concepts/delegate-present-verify/) — every primitive in depth - [Constraints](/concepts/constraints/) — full constraint vocabulary + `VerifierContext` reference - [Revocation](/concepts/revocation/) — signed revocation lists, push revocation, and the §17.1 `RevocationProvider` hook - [Provider architecture](/concepts/provider-architecture/) — the §17 hooks plus the provider levers (`VerificationReceipt`, `PolicyVerdict`) - [Integration guides](/guides/api-gateway/) — surface-specific patterns (Agentic API / meetings / voice) --- # TypeScript SDK > Use the TypeScript reference SDK to issue delegations, build proof bundles, and verify them. Source: https://docs.identities.ai/sdk/typescript/ The TypeScript SDK is byte-for-byte interoperable with Go, Python, Rust, and C/C++. It runs in Node 20+ and in modern browsers — the cryptographic primitives use audited pure-JS implementations from the Noble suite (`@noble/ed25519`, `@noble/hashes`) plus `@noble/post-quantum` for ML-DSA-65. No native dependencies, no WebAssembly required. **Stability:** every primitive on this page is stable in `1.0.0-alpha.20`. There are no preview / experimental APIs in the snippets below. ## Install ```bash npm install @identities-ai/ratify-protocol@1.0.0-alpha.20 ``` ## Three minutes, end to end ```ts import { PROTOCOL_VERSION, SCOPE_MEETING_ATTEND, SCOPE_MEETING_SPEAK, generateAgent, generateChallenge, generateHumanRoot, issueDelegation, signChallenge, verifyBundle, type DelegationCert, type ProofBundle, } from "@identities-ai/ratify-protocol"; // 1. Alice generates her hybrid (Ed25519 + ML-DSA-65) root identity. const { root: alice, privateKey: alicePriv } = await generateHumanRoot(); // 2. Her AI agent generates its own hybrid keypair. const { agent, privateKey: agentPriv } = await generateAgent( "Alice's Scheduler", "custom", ); // 3. Alice signs a delegation cert for the agent. const now = Math.floor(Date.now() / 1000); const cert: DelegationCert = { cert_id: "cert-001", version: PROTOCOL_VERSION, issuer_id: alice.id, issuer_pub_key: alice.public_key, subject_id: agent.id, subject_pub_key: agent.public_key, scope: [SCOPE_MEETING_ATTEND, SCOPE_MEETING_SPEAK], issued_at: now, expires_at: now + 7 * 24 * 3600, signature: { ed25519: new Uint8Array(0), ml_dsa_65: new Uint8Array(0) }, }; await issueDelegation(cert, alicePriv); // 4. Verifier issues a challenge. Agent signs it. const challenge = generateChallenge(); const challengeAt = Math.floor(Date.now() / 1000); const challengeSig = await signChallenge(challenge, challengeAt, agentPriv); // 5. Agent assembles a proof bundle. const bundle: ProofBundle = { agent_id: agent.id, agent_pub_key: agent.public_key, delegations: [cert], challenge, challenge_at: challengeAt, challenge_sig: challengeSig, }; // 6. Verifier runs the verifier. const result = await verifyBundle(bundle, { required_scope: SCOPE_MEETING_ATTEND, }); if (result.valid) { console.log("✓ Authorized"); console.log(" human_id: ", result.human_id); console.log(" agent_id: ", result.agent_id); console.log(" granted_scope: ", result.granted_scope); console.log(" identity_status:", result.identity_status); } else { console.log("✗ 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: 996dc9a99007e7239b20d08bc845337b agent_id: c7f98cc2bc4fcb492147b08aa887a470 granted_scope: [ 'meeting:attend', 'meeting:speak' ] identity_status: authorized_agent ``` `verifyBundle` is `async` — every cryptographic operation goes through `crypto.subtle` (in the browser) or Node's native crypto where available. ## JSON transport — sending a bundle over the wire A proof bundle is JSON on the wire. Convert byte arrays to base64 before transport, then decode them back to `Uint8Array` before calling `verifyBundle`. The bundle produced by the TypeScript SDK verifies in Go, Python, Rust, and C/C++ (and vice versa). ```ts import { verifyBundle, type DelegationCert, type HybridPublicKey, type HybridSignature, type ProofBundle, } from "@identities-ai/ratify-protocol"; const b64 = (value: Uint8Array) => Buffer.from(value).toString("base64"); const bytes = (value: string) => new Uint8Array(Buffer.from(value, "base64")); const pubToJson = (key: HybridPublicKey) => ({ ed25519: b64(key.ed25519), ml_dsa_65: b64(key.ml_dsa_65), }); const sigToJson = (sig: HybridSignature) => ({ ed25519: b64(sig.ed25519), ml_dsa_65: b64(sig.ml_dsa_65), }); export function proofBundleToJson(bundle: ProofBundle): string { return JSON.stringify({ ...bundle, agent_pub_key: pubToJson(bundle.agent_pub_key), challenge: b64(bundle.challenge), challenge_sig: sigToJson(bundle.challenge_sig), session_context: bundle.session_context ? b64(bundle.session_context) : "", stream_id: bundle.stream_id ? b64(bundle.stream_id) : "", delegations: bundle.delegations.map((cert) => ({ ...cert, issuer_pub_key: pubToJson(cert.issuer_pub_key), subject_pub_key: pubToJson(cert.subject_pub_key), signature: sigToJson(cert.signature), })), }); } export function proofBundleFromJson(json: string): ProofBundle { const raw = JSON.parse(json); const decodePub = (key: any): HybridPublicKey => ({ ed25519: bytes(key.ed25519), ml_dsa_65: bytes(key.ml_dsa_65), }); const decodeSig = (sig: any): HybridSignature => ({ ed25519: bytes(sig.ed25519), ml_dsa_65: bytes(sig.ml_dsa_65), }); return { ...raw, agent_pub_key: decodePub(raw.agent_pub_key), challenge: bytes(raw.challenge), challenge_sig: decodeSig(raw.challenge_sig), session_context: raw.session_context ? bytes(raw.session_context) : undefined, stream_id: raw.stream_id ? bytes(raw.stream_id) : undefined, delegations: raw.delegations.map((cert: any): DelegationCert => ({ ...cert, issuer_pub_key: decodePub(cert.issuer_pub_key), subject_pub_key: decodePub(cert.subject_pub_key), signature: decodeSig(cert.signature), })), }; } // Agent side: serialize for transport. const bundleJson = proofBundleToJson(bundle); const proofHeader = Buffer.from(bundleJson, "utf8").toString("base64"); // Send proofHeader as X-Ratify-Proof, a WebSocket message, a queue field, etc. const app = express(); app.post("/tool/execute", async (req, res) => { const rawProof = req.headers["x-ratify-proof"]; if (!rawProof) return res.status(401).json({ error: "missing proof" }); let bundle: ProofBundle; try { bundle = proofBundleFromJson( Buffer.from(rawProof as string, "base64").toString("utf8") ); } catch { return res.status(400).json({ error: "malformed proof" }); } const result = await verifyBundle(bundle, { required_scope: "execute:tool", }); if (!result.valid) { return res.status(403).json({ error: result.identity_status, reason: result.error_reason, }); } // Proceed — agent is authorized res.json({ ok: true, agent_id: result.agent_id }); }); ``` Standard convention: agents PUT the bundle JSON in the `X-Ratify-Proof` header (base64-encoded). See the [Agentic API guide](/guides/api-gateway/) for the full middleware pattern. ## Browser usage The same SDK works in browsers. No bundler-specific configuration required for Vite, Next.js, SvelteKit, or Astro. The cryptographic primitives use `crypto.subtle` where available and fall back to pure-JS Noble implementations elsewhere. ```ts // Bundle arrives from your server as the same base64-wrapped JSON string. const bundleJson = atob(proofHeaderFromServer); const bundle = proofBundleFromJson(bundleJson); const result = await verifyBundle(bundle, { required_scope: "files:read", }); ``` Approximate bundle size for the verifier path: **~50 KB minified + gzipped** (most of which is the ML-DSA-65 verifier — Ed25519 alone would be much smaller). ## Verifier-side: branching on `identity_status` `verifyBundle` always resolves with a `VerifyResult`. Inspect `result.valid` first; if it's false, the specific failure mode is in `result.identity_status`. The type is a string-literal union — your editor will autocomplete the full set: ```ts switch (result.identity_status as IdentityStatus) { case "authorized_agent": // ✓ All checks passed. result.granted_scope is the intersection of // every cert's scope (effective scope across the chain). break; case "expired": // ✗ At least one cert in the chain is past its expires_at, OR not-yet-valid. break; case "revoked": // ✗ A cert ID in the chain matched a revoked entry returned by your // revocation provider (or legacy is_revoked closure). break; case "scope_denied": // ✗ required_scope was not in the chain's effective (intersected) scope, // OR an advanced PolicyProvider rejected the bundle. break; case "constraint_denied": // ✗ A first-class constraint (geo/time/amount/rate) evaluated to // "outside the allowed range". break; case "constraint_unverifiable": // ✗ A constraint required input the caller didn't provide // (e.g. cert has geo_circle but context.current_lat was undefined). break; case "constraint_unknown": // ✗ A cert declared a constraint type this SDK build doesn't recognize. // Fail-closed by design. break; case "delegation_not_authorized": // ✗ An intermediate cert in the chain sub-delegated, but its parent // never granted "identity:delegate". The sub-delegation gate. break; case "invalid": // ✗ Catch-all for structural / cryptographic failures. // result.error_reason carries a stable machine-readable prefix // (e.g. "stale_challenge: challenge is 31 seconds old (max 300)"). break; default: // The compiler will flag this if the protocol adds new statuses. const _exhaustive: never = result.identity_status; } ``` The verifier is **fail-closed by default** — any error path returns `valid: false`. The machine-readable failure codes inside `error_reason` (e.g. `stale_challenge`, `bad_signature`, `broken_chain`, `key_mismatch`) are stable; route audit pipelines on those without parsing prose. ## Constraints (geo / time / amount / rate / speed) When a delegation declares constraints, supply the runtime context: ```ts const ctx: VerifierContext = { current_lat: 37.7749, current_lon: -122.4194, }; const result = await verifyBundle(bundle, { required_scope: "drone:deliver", context: ctx, }); ``` In TypeScript, the `has_*` flags from the Go API are inferred from field presence — leaving `current_lat` and `current_lon` `undefined` is equivalent to "no location supplied" and any `geo_circle` / `geo_polygon` / `geo_bbox` constraint on the cert will return `constraint_unverifiable`. 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](/concepts/constraints/). ## 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 `delegation_not_authorized`. ```ts // Alice → A: meeting:attend + identity:delegate const aliceToA: DelegationCert = { cert_id: "cert-alice-to-a", version: PROTOCOL_VERSION, issuer_id: alice.id, issuer_pub_key: alice.public_key, subject_id: agentA.id, subject_pub_key: agentA.public_key, scope: [SCOPE_MEETING_ATTEND, SCOPE_IDENTITY_DELEGATE], issued_at: now, expires_at: now + 86400, signature: { ed25519: new Uint8Array(0), ml_dsa_65: new Uint8Array(0) }, }; await issueDelegation(aliceToA, alicePriv); // A → B: a subset of A's grant const aToB: DelegationCert = { cert_id: "cert-a-to-b", version: PROTOCOL_VERSION, issuer_id: agentA.id, issuer_pub_key: agentA.public_key, subject_id: agentB.id, subject_pub_key: agentB.public_key, scope: [SCOPE_MEETING_ATTEND], issued_at: now, expires_at: now + 3600, signature: { ed25519: new Uint8Array(0), ml_dsa_65: new Uint8Array(0) }, }; await issueDelegation(aToB, agentAPriv); // Chain order: [leaf, ..., root]. A→B is the leaf, Alice→A is the root. const bundle: ProofBundle = { agent_id: agentB.id, agent_pub_key: agentB.public_key, delegations: [aToB, aliceToA], challenge, challenge_at: challengeAt, challenge_sig: agentBSig, }; const result = await verifyBundle(bundle, { required_scope: SCOPE_MEETING_ATTEND, }); ``` The effective `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) Every hook from the §17 surface is exported as an interface: ```ts import type { RevocationProvider, PolicyProvider, AuditProvider, ConstraintEvaluator, AnchorResolver, PolicyVerdict, } from "@identities-ai/ratify-protocol"; const result = await verifyBundle(bundle, { required_scope: "meeting:attend", revocation: myRevocationProvider, // §17.1 policy: myPolicyProvider, // §17.2 audit: myAuditProvider, // §17.3 constraint_evaluators: myExtensionRegistry, // §17.7 anchor_resolver: myAnchorResolver, // §17.8 policy_verdict: cachedVerdict, // §17.6 policy_secret: policySecret, context: ctx, }); ``` Each hook has fail-closed semantics — a `RevocationProvider` that resolves to an error fails the bundle with `identity_status: "invalid"` + `error_reason: "revocation_error: ..."`, not a silent "unknown means allow." See [Provider architecture](/concepts/provider-architecture/) for the full reference. ## Type-safe scope strings The SDK exports every canonical scope as a constant. Use the constants in your code so the compiler catches typos before they reach the verifier: ```ts import { SCOPE_MEETING_ATTEND, SCOPE_MEETING_SPEAK, SCOPE_PAYMENTS_AUTHORIZE, SCOPE_IDENTITY_DELEGATE, } from "@identities-ai/ratify-protocol"; // Compiler-safe const scopes = [SCOPE_MEETING_ATTEND, SCOPE_PAYMENTS_AUTHORIZE]; // Compiler can't help you here — typo "meting:attend" would only surface at verify time const scopesUnsafe = ["meeting:attend", "payments:athorize"]; ``` The full canonical 54-scope vocabulary is documented at [Scopes](/concepts/scopes/) and exposed in `src/scope.ts`. ## Running the full test suite ```bash cd ratify-protocol/sdks/typescript npm test # → 416 tests pass (79 conformance fixtures + cross-SDK vectors + wire codecs + challenge stores + providers) ``` To run just the 79 canonical conformance fixtures: ```bash npm run test:conformance # → 79/79 fixtures pass ``` If any conformance fixtures fail, the bytes have drifted from the reference — file a bug. ## Where to next - [Protocol concepts: Delegate → Present → Verify](/concepts/delegate-present-verify/) — every primitive in depth - [Constraints](/concepts/constraints/) — full constraint vocabulary + `VerifierContext` reference - [Provider architecture](/concepts/provider-architecture/) — the §17 hooks plus the provider levers (`VerificationReceipt`, `PolicyVerdict`) - [Integration guides](/guides/api-gateway/) — surface-specific patterns --- # Python SDK > Use the Python reference SDK to issue delegations, build proof bundles, and verify them. Source: https://docs.identities.ai/sdk/python/ The Python SDK is byte-for-byte interoperable with Go, TypeScript, Rust, and C/C++. Python 3.10+. Runtime dependencies are `cryptography` (Ed25519) and `pqcrypto` (ML-DSA-65) — both audited, both maintained by mainstream Python crypto teams. **Stability:** every primitive on this page is stable in `1.0.0a20`. There are no preview / experimental APIs in the snippets below. ## Install ```bash pip install ratify-protocol==1.0.0a20 ``` The package on PyPI is `ratify-protocol`. The module you import is `ratify_protocol` (PEP 8 naming). ## Three minutes, end to end ```python import time from ratify_protocol import ( PROTOCOL_VERSION, SCOPE_MEETING_ATTEND, SCOPE_MEETING_SPEAK, DelegationCert, ProofBundle, VerifyOptions, generate_agent, generate_challenge, generate_human_root, issue_delegation, sign_challenge, verify_bundle, ) # 1. Alice generates her hybrid (Ed25519 + ML-DSA-65) root identity. alice, alice_priv = generate_human_root() # 2. Her AI agent generates its own hybrid keypair. agent, agent_priv = generate_agent("Alice's Scheduler", "custom") # 3. Alice signs a delegation cert for the agent. now = int(time.time()) cert = DelegationCert( cert_id="cert-001", version=PROTOCOL_VERSION, issuer_id=alice.id, issuer_pub_key=alice.public_key, subject_id=agent.id, subject_pub_key=agent.public_key, scope=[SCOPE_MEETING_ATTEND, SCOPE_MEETING_SPEAK], issued_at=now, expires_at=now + 7 * 24 * 3600, signature=None, # populated in place by issue_delegation ) issue_delegation(cert, alice_priv) # 4. Verifier issues a challenge. Agent signs it. challenge = generate_challenge() challenge_sig = sign_challenge(challenge, now, agent_priv) # 5. Agent assembles a proof bundle. bundle = ProofBundle( agent_id=agent.id, agent_pub_key=agent.public_key, delegations=[cert], challenge=challenge, challenge_at=now, challenge_sig=challenge_sig, ) # 6. Verifier runs the verifier in a single call. result = verify_bundle( bundle, VerifyOptions(required_scope=SCOPE_MEETING_ATTEND), ) if result.valid: print(f"✓ Authorized") print(f" human_id: {result.human_id}") print(f" agent_id: {result.agent_id}") print(f" granted_scope: {result.granted_scope}") print(f" identity_status: {result.identity_status}") else: print(f"✗ 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: b3ef7456eb0a741008ee97cb30ee4aec agent_id: 98f087953d2da9d52e78f02300df380d granted_scope: ['meeting:attend', 'meeting:speak'] identity_status: authorized_agent ``` ## JSON transport — sending a bundle over the wire A proof bundle is JSON on the wire. Convert `bytes` fields to base64 before transport, then decode them back to SDK dataclasses before calling `verify_bundle`. The result verifies in Go, TypeScript, Rust, and C/C++ without changing the protocol bytes. ```python from fastapi import FastAPI, Request, HTTPException import base64, json from ratify_protocol import ( Constraint, DelegationCert, HybridPublicKey, HybridSignature, ProofBundle, VerifyOptions, verify_bundle, ) def b64e(value: bytes) -> str: return base64.b64encode(value).decode("ascii") def b64d(value: str) -> bytes: return base64.b64decode(value.encode("ascii")) def pub_to_json(key: HybridPublicKey) -> dict: return {"ed25519": b64e(key.ed25519), "ml_dsa_65": b64e(key.ml_dsa_65)} def sig_to_json(sig: HybridSignature) -> dict: return {"ed25519": b64e(sig.ed25519), "ml_dsa_65": b64e(sig.ml_dsa_65)} def proof_bundle_to_json(bundle: ProofBundle) -> str: return json.dumps({ "agent_id": bundle.agent_id, "agent_pub_key": pub_to_json(bundle.agent_pub_key), "delegations": [{ "cert_id": cert.cert_id, "version": cert.version, "issuer_id": cert.issuer_id, "issuer_pub_key": pub_to_json(cert.issuer_pub_key), "subject_id": cert.subject_id, "subject_pub_key": pub_to_json(cert.subject_pub_key), "scope": cert.scope, "constraints": [c.to_canonical_dict() for c in cert.constraints], "issued_at": cert.issued_at, "expires_at": cert.expires_at, "signature": sig_to_json(cert.signature), } for cert in bundle.delegations], "challenge": b64e(bundle.challenge), "challenge_at": bundle.challenge_at, "challenge_sig": sig_to_json(bundle.challenge_sig), "session_context": b64e(bundle.session_context) if bundle.session_context else "", "stream_id": b64e(bundle.stream_id) if bundle.stream_id else "", "stream_seq": bundle.stream_seq, }) def proof_bundle_from_json(raw_json: str) -> ProofBundle: raw = json.loads(raw_json) def pub(raw_key: dict) -> HybridPublicKey: return HybridPublicKey(ed25519=b64d(raw_key["ed25519"]), ml_dsa_65=b64d(raw_key["ml_dsa_65"])) def sig(raw_sig: dict) -> HybridSignature: return HybridSignature(ed25519=b64d(raw_sig["ed25519"]), ml_dsa_65=b64d(raw_sig["ml_dsa_65"])) return ProofBundle( agent_id=raw["agent_id"], agent_pub_key=pub(raw["agent_pub_key"]), delegations=[ DelegationCert( cert_id=cert["cert_id"], version=cert["version"], issuer_id=cert["issuer_id"], issuer_pub_key=pub(cert["issuer_pub_key"]), subject_id=cert["subject_id"], subject_pub_key=pub(cert["subject_pub_key"]), scope=cert["scope"], constraints=[Constraint(**c) for c in cert.get("constraints", [])], issued_at=cert["issued_at"], expires_at=cert["expires_at"], signature=sig(cert["signature"]), ) for cert in raw["delegations"] ], challenge=b64d(raw["challenge"]), challenge_at=raw["challenge_at"], challenge_sig=sig(raw["challenge_sig"]), session_context=b64d(raw["session_context"]) if raw.get("session_context") else b"", stream_id=b64d(raw["stream_id"]) if raw.get("stream_id") else b"", stream_seq=raw.get("stream_seq", 0), ) # Agent side: serialize for transport. bundle_json = proof_bundle_to_json(bundle) proof_header = base64.b64encode(bundle_json.encode("utf-8")).decode("ascii") # Send proof_header as X-Ratify-Proof, an MQTT payload field, gRPC metadata, etc. app = FastAPI() @app.post("/tool/execute") async def execute_tool(request: Request): raw_proof = request.headers.get("x-ratify-proof") if not raw_proof: raise HTTPException(401, "missing proof") try: bundle = proof_bundle_from_json(base64.b64decode(raw_proof).decode("utf-8")) except Exception: raise HTTPException(400, "malformed proof") result = verify_bundle(bundle, VerifyOptions(required_scope="execute:tool")) if not result.valid: raise HTTPException(403, { "error": result.identity_status, "reason": result.error_reason, }) return {"ok": True, "agent_id": result.agent_id} ``` Standard convention: agents send the bundle JSON in the `X-Ratify-Proof` header (base64-encoded). See the [Agentic API guide](/guides/api-gateway/). ## Verifier-side: branching on `identity_status` `verify_bundle` always returns a `VerifyResult`. Inspect `result.valid` first; if it's false, the specific failure mode is in `result.identity_status` (a literal string, matching the byte-for-byte protocol-level enum across all five SDKs): ```python status = result.identity_status if status == "authorized_agent": # ✓ All checks passed. result.granted_scope is the intersection of # every cert's scope (effective scope across the chain). pass elif status == "expired": # ✗ At least one cert is past its expires_at OR not-yet-valid. pass elif status == "revoked": # ✗ A cert ID in the chain matched a revoked entry from the # revocation provider (or the legacy is_revoked closure). pass elif status == "scope_denied": # ✗ required_scope was not in the chain's effective (intersected) # scope, OR an advanced PolicyProvider rejected the bundle. pass elif status == "constraint_denied": # ✗ A first-class constraint (geo/time/amount/rate) evaluated to # "outside the allowed range". pass elif status == "constraint_unverifiable": # ✗ A constraint required input the caller didn't provide # (e.g. cert has geo_circle but VerifierContext.current_lat was None). pass elif status == "constraint_unknown": # ✗ A cert declared a constraint type this SDK build doesn't recognize. pass elif status == "delegation_not_authorized": # ✗ An intermediate cert sub-delegated without identity:delegate. pass elif status == "invalid": # ✗ Catch-all for structural / cryptographic failures. # result.error_reason carries a stable machine-readable prefix # (e.g. "stale_challenge: challenge is 31 seconds old (max 300)"). pass ``` The verifier is **fail-closed by default** — any error path returns `valid=False`. ## Constraints (geo / time / amount / rate / speed) When a delegation declares constraints, supply the runtime context: ```python from ratify_protocol import VerifierContext ctx = VerifierContext( current_lat=37.7749, current_lon=-122.4194, ) result = verify_bundle( bundle, VerifyOptions( required_scope="drone:deliver", context=ctx, ), ) ``` Unset fields (`None`) on `VerifierContext` mean "not supplied" — any `geo_circle` / `geo_polygon` / `geo_bbox` constraint will return `constraint_unverifiable` if you don't set both `current_lat` and `current_lon`. The full constraint vocabulary (`geo_circle`, `geo_polygon`, `geo_bbox`, `time_window`, `max_speed_mps`, `max_amount`, `max_rate`) and `VerifierContext` field requirements are documented in [Constraints](/concepts/constraints/). ## 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 `delegation_not_authorized`. ```python from ratify_protocol import SCOPE_IDENTITY_DELEGATE # Alice → A: meeting:attend + identity:delegate alice_to_a = DelegationCert( cert_id="cert-alice-to-a", version=PROTOCOL_VERSION, issuer_id=alice.id, issuer_pub_key=alice.public_key, subject_id=agent_a.id, subject_pub_key=agent_a.public_key, scope=[SCOPE_MEETING_ATTEND, SCOPE_IDENTITY_DELEGATE], issued_at=now, expires_at=now + 86400, signature=None, ) issue_delegation(alice_to_a, alice_priv) # A → B: a subset of A's grant a_to_b = DelegationCert( cert_id="cert-a-to-b", version=PROTOCOL_VERSION, issuer_id=agent_a.id, issuer_pub_key=agent_a.public_key, subject_id=agent_b.id, subject_pub_key=agent_b.public_key, scope=[SCOPE_MEETING_ATTEND], issued_at=now, expires_at=now + 3600, signature=None, ) issue_delegation(a_to_b, agent_a_priv) # Chain order: [leaf, ..., root]. A→B is the leaf, Alice→A is the root. bundle = ProofBundle( agent_id=agent_b.id, agent_pub_key=agent_b.public_key, delegations=[a_to_b, alice_to_a], challenge=challenge, challenge_at=challenge_at, challenge_sig=agent_b_sig, ) result = verify_bundle( bundle, VerifyOptions(required_scope=SCOPE_MEETING_ATTEND), ) ``` `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) The provider hooks land on `VerifyOptions` as `Optional[...]` fields: ```python from ratify_protocol import ( VerifyOptions, RevocationProvider, PolicyProvider, AuditProvider, ConstraintEvaluator, AnchorResolver, ) result = verify_bundle( bundle, VerifyOptions( required_scope="meeting:attend", revocation=my_revocation_provider, # §17.1 policy=my_policy_provider, # §17.2 audit=my_audit_provider, # §17.3 constraint_evaluators=my_ext_registry, # §17.7 anchor_resolver=my_anchor_resolver, # §17.8 policy_verdict=cached_verdict, # §17.6 policy_secret=policy_secret, context=ctx, ), ) ``` `RevocationProvider`, `PolicyProvider`, `AuditProvider`, `ConstraintEvaluator`, and `AnchorResolver` are `typing.Protocol` classes — duck-typing applies. Any class implementing the expected method signature works; no inheritance required. Each hook has fail-closed semantics. See [Provider architecture](/concepts/provider-architecture/) for the full reference. ## Running the conformance suite ```bash cd ratify-protocol/sdks/python pip install -e ".[dev]" pytest -q ``` You should see all 79 canonical wire-format fixtures plus the 10 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 - [Protocol concepts: Delegate → Present → Verify](/concepts/delegate-present-verify/) — every primitive in depth - [Constraints](/concepts/constraints/) — full constraint vocabulary + `VerifierContext` reference - [Provider architecture](/concepts/provider-architecture/) — the §17 hooks plus the provider levers (`VerificationReceipt`, `PolicyVerdict`) - [Integration guides](/guides/api-gateway/) — surface-specific patterns --- # Rust SDK > Use the Rust reference SDK to issue delegations, build proof bundles, and verify them. Source: https://docs.identities.ai/sdk/rust/ 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.20`. There are no preview / experimental APIs in the snippets below. ## Install ```bash cargo add ratify-protocol@1.0.0-alpha.20 ``` Or from source: ```bash 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`. ## Three minutes, end to end ```rust 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"`). ## JSON transport — sending a bundle over the wire `ProofBundle` and `DelegationCert` derive `serde::Serialize` / `serde::Deserialize`. Serialize with `serde_json::to_string`; deserialize with `serde_json::from_str`. Bundles produced by the Rust SDK verify in Go, TypeScript, Python, and C/C++ (and vice versa). ```rust use ratify_protocol::ProofBundle; // Agent side: serialize for transport let bundle_json = serde_json::to_string(&bundle)?; // Send over HTTP header, message queue, gRPC metadata, etc. // Verifier side (Axum middleware example): use axum::{extract::Request, http::StatusCode, middleware::Next, response::Response}; use base64::prelude::*; use ratify_protocol::{verify_bundle, VerifyOptions}; pub async fn ratify_auth(req: Request, next: Next) -> Result { let proof_header = req .headers() .get("x-ratify-proof") .and_then(|v| v.to_str().ok()) .ok_or(StatusCode::UNAUTHORIZED)?; let bundle_bytes = BASE64_STANDARD .decode(proof_header) .map_err(|_| StatusCode::BAD_REQUEST)?; let bundle: ProofBundle = serde_json::from_slice(&bundle_bytes) .map_err(|_| StatusCode::BAD_REQUEST)?; let result = verify_bundle( &bundle, &VerifyOptions { required_scope: "execute:tool".into(), ..Default::default() }, ); if !result.valid { return Err(StatusCode::FORBIDDEN); } Ok(next.run(req).await) } ``` See the [Agentic API guide](/guides/api-gateway/) for the full middleware pattern. ## 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: ```rust 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) When a delegation declares constraints, supply the runtime context: ```rust 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` — `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](/concepts/constraints/). ## 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`. ```rust 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. ## Provider hooks (SPEC §17) Each hook is a trait that you implement and place in `VerifyOptions`: ```rust use ratify_protocol::{ RevocationProvider, PolicyProvider, AuditProvider, ConstraintEvaluator, AnchorResolver, }; struct MyRevocation { /* … */ } impl RevocationProvider for MyRevocation { fn is_revoked(&self, cert_id: &str) -> Result { /* … */ } } 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](/concepts/provider-architecture/) for the full reference. ## Running the conformance suite ```bash cd ratify-protocol/sdks/rust cargo test ``` You should see all 79 canonical wire-format fixtures plus the 10 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 - [Protocol concepts: Delegate → Present → Verify](/concepts/delegate-present-verify/) — every primitive in depth - [Constraints](/concepts/constraints/) — full constraint vocabulary + `VerifierContext` reference - [Provider architecture](/concepts/provider-architecture/) — the §17 hooks plus the provider levers - [Integration guides](/guides/api-gateway/) — surface-specific patterns --- # Meetings > Pre-announce and verify AI agents before they join meetings. Source: https://docs.identities.ai/guides/meetings/ Meetings is an additional Ratify integration surface, not a prerequisite for using Ratify Verify. The initial managed-product wedge is Agentic API. The Meetings control plane exists in alpha, while the Zoom adapter remains under validation. Meeting integrations use the same proof bundle as API integrations, but the protected action is meeting attendance. Ratify is useful here when a customer wants a meeting-note agent, a sales-assistant agent, or a support agent to join Zoom, Microsoft Teams, or Google Meet only when it is explicitly authorized. The managed meetings surface lives in the [Ratify Verify console](https://dev.identities.ai). ## Flow 1. Agent platform obtains consent from the human or organization. 2. The platform creates a proof bundle for `meeting:attend`, and optionally `meeting:speak`, `meeting:video`, or `meeting:share_screen`. 3. The platform pre-announces the agent before the meeting. 4. The meeting adapter verifies the proof when participant events arrive. 5. Policy decides whether to allow, label, warn, or remove the participant. Meetings are an organization/team surface. Personal accounts can integrate SDKs and Agentic API, but meeting enforcement requires an organization context. ## Meeting note-taker example For a meeting-note platform, your runtime usually does this: - generate or load agent keys - get a delegation from the workspace admin - sign a challenge when the meeting surface asks for proof - keep the proof bundle available to the meeting adapter The meeting adapter or Ratify-managed surface usually does this: - issue the challenge - verify the signature - check revocation and expiry - decide whether to allow, label, challenge, or remove the agent ## SDK examples ```go result := ratify.Verify(bundle, ratify.VerifyOptions{ RequiredScope: ratify.ScopeMeetingAttend, }) if !result.Valid { http.Error(w, result.ErrorReason, http.StatusForbidden) return } ``` ```ts const result = await verifyBundle(bundle, { required_scope: SCOPE_MEETING_ATTEND, }); if (!result.valid) { throw new Error(result.error_reason); } ``` ```python result = verify_bundle(bundle, VerifyOptions(required_scope=SCOPE_MEETING_ATTEND)) if not result.valid: raise RuntimeError(result.error_reason) ``` ```rust let result = verify_bundle( &bundle, &VerifyOptions { required_scope: SCOPE_MEETING_ATTEND.into(), ..Default::default() }, ); assert!(result.valid, "{}", result.error_reason); ``` ```c /* Adapter receives the bundle_json from the agent's pre-announce. Verify before allowing the bot to join. */ RatifyVerifyResult *result = NULL; char *err = NULL; ratify_verify_bundle(bundle_json, "meeting:attend", (int64_t)time(NULL), &result, &err); if (ratify_verify_result_is_valid(result)) { char *agent_id = ratify_verify_result_agent_id(result); char *human_id = ratify_verify_result_human_id(result); /* Allow join and label the bot with the human principal's identity */ allow_join_and_label(meeting_id, agent_id, human_id); ratify_string_free(agent_id); ratify_string_free(human_id); } else { char *status = ratify_verify_result_identity_status(result); deny_join(meeting_id, status); /* remove agent from lobby */ ratify_string_free(status); } ratify_verify_result_free(result); ratify_error_free(err); ``` ## Callback contract Your `callback_url` is where Ratify posts lifecycle events about the meeting connection. For the implementation details, see [Callbacks and Webhooks](/verify/callback-contract/). --- # Voice > Verify and authorize live voice agents, telephony bots, and speaking surfaces with Ratify. Source: https://docs.identities.ai/guides/voice/ The Voice surface is a commercial surface in the [Ratify Verify console](https://dev.identities.ai). Canonical protocol references: - [README](https://github.com/identities-ai/ratify-protocol/blob/main/README.md) - [SPEC.md](https://github.com/identities-ai/ratify-protocol/blob/main/SPEC.md) - [SDK matrix](https://github.com/identities-ai/ratify-protocol/blob/main/docs/SDKS.md) Ratify v1 does not define a separate `voice:*` scope family. Voice is modeled with the canonical meeting and communication scopes, plus ordinary data or tool scopes when the agent does more than speak. ## What Voice is for Use the Voice surface when you need to authorize: - outbound sales or support calls - voice intake and identity verification - live assistant speaking on a call - call-center or telephony agent actions - speech plus downstream app actions during the same session ## How to think about it Voice is not just "can the model speak". The important questions are: 1. Is this voice agent authorized to represent the person or organization? 2. Is it allowed to speak in this session? 3. Is it allowed to perform follow-up actions after speaking, such as creating a record or sending a message? That usually maps to: - `meeting:attend` for joining or participating in a live session - `meeting:speak` for speaking permission - `comms:message:send` for voice-driven outbound messaging workflows - `execute:tool` for tool actions triggered by a voice session - `data:write` for CRM or case creation ## Voice flow ```mermaid sequenceDiagram autonumber participant Org as Customer Org participant Platform as Voice Platform participant Ratify as Ratify Platform participant Agent as Voice Agent participant Call as Call / Meeting Surface Org->>Platform: Enable voice integration Platform->>Ratify: Register Voice surface + callback URL Ratify-->>Platform: API key, signing secret, webhook secret Org->>Platform: Approve voice policy Platform->>Agent: Load delegated identity Call->>Ratify: Request proof / challenge Ratify->>Agent: Challenge Agent->>Agent: Sign with SDK Agent-->>Call: Proof bundle Call->>Call: Verify proof and scope Ratify-->>Platform: Callback for lifecycle or revocation ``` ## SDK examples The example below treats a live voice agent as a speaking participant. If the session also performs app actions, add the required action scope alongside the voice scope. ```go result := ratify.Verify(bundle, ratify.VerifyOptions{ RequiredScope: ratify.ScopeMeetingSpeak, }) if !result.Valid { http.Error(w, result.ErrorReason, http.StatusForbidden) return } ``` ```ts const result = await verifyBundle(bundle, { required_scope: SCOPE_MEETING_SPEAK, }); if (!result.valid) { throw new Error(result.error_reason); } ``` ```python result = verify_bundle(bundle, VerifyOptions(required_scope=SCOPE_MEETING_SPEAK)) if not result.valid: raise RuntimeError(result.error_reason) ``` ```rust let result = verify_bundle( &bundle, &VerifyOptions { required_scope: SCOPE_MEETING_SPEAK.into(), ..Default::default() }, ); assert!(result.valid, "{}", result.error_reason); ``` ```c /* Voice gateway intercepts the call via signaling. Verify the agent's mandate before the call connects. */ RatifyVerifyResult *result = NULL; char *err = NULL; ratify_verify_bundle(bundle_json, "meeting:speak", (int64_t)time(NULL), &result, &err); if (!ratify_verify_result_is_valid(result)) { char *status = ratify_verify_result_identity_status(result); fprintf(stderr, "voice agent rejected: %s\n", status); ratify_string_free(status); ratify_verify_result_free(result); ratify_error_free(err); reject_call(call_id); return; } /* Call proceeds — log the authorized principal for audit */ char *agent_id = ratify_verify_result_agent_id(result); char *human_id = ratify_verify_result_human_id(result); log_authorized_call(call_id, agent_id, human_id); ratify_string_free(agent_id); ratify_string_free(human_id); ratify_verify_result_free(result); ratify_error_free(err); ``` ## Registration 1. Register the platform in the [Ratify Verify console](https://dev.identities.ai). 2. Choose the Voice surface. 3. Configure your callback URL. 4. Store the API key and webhook secret. 5. Connect the voice runtime to the proof bundle flow. ## What your app must do Your voice platform usually owns: - the telephony provider or media stack - session establishment - turn-taking and transcript state - the SDK call that signs or verifies the proof bundle - the callback endpoint for lifecycle state Ratify owns the managed registration, policy, audit, revocation, and enforcement surface. If the voice session also writes to CRM, sends email, or calls tools, those actions need their own scopes in addition to the speaking scope. --- # Agentic API > Require Ratify proofs before MCP, A2A, finance, or REST requests reach application code. Source: https://docs.identities.ai/guides/api-gateway/ Agentic API is Ratify's initial managed-product wedge. Use it when an AI agent is about to call an MCP tool, send an A2A request, invoke a REST endpoint, or trigger another consequential programmatic action. Ratify Verify returns the authorization decision and reason. Your gateway, MCP server, or application middleware decides whether the protected action executes. The managed audit trail records the verification attempt. The open-source SDKs let you build the same check fully offline; the managed [Ratify Verify API](/verify/quickstart/) adds metering, hosted revocation checks, and the recorded audit trail. This is an integration pattern using the managed Verify API, not a prebuilt commercial gateway. This is the surface you use when an agent platform needs to authorize: - MCP tool calls - A2A requests - REST API actions - financial actions such as `payments:send` - data reads and writes - regulated internal tools The gateway pattern is: 1. Map each route, tool, or operation to a required Ratify scope. 2. Extract the proof bundle from the request. 3. Reconstruct verifier context from observable request data. 4. Call the SDK verifier. 5. Reject failed proofs before the request reaches the protected handler. ## Header convention Use a single proof header unless the transport has a native auth envelope: ```http X-Ratify-Proof: ``` ## Scope mapping ```yaml routes: "GET /v1/accounts/:id": required_scope: "data:read" "POST /v1/payments": required_scope: "payments:send" "POST /mcp/tools/send_email": required_scope: "execute:tool" "POST /v1/ledger/post": required_scope: "payments:send" "PATCH /v1/customers/:id": required_scope: "data:write" ``` ## Enforcement rule If verification fails, return `401` for missing proof and `403` for invalid, expired, revoked, under-scoped, or constraint-denied proof. Never let callers provide their own constraint context for high-stakes checks. The gateway should derive amount, route, IP, tenant, and request body hash from the request it actually received. ## Gateway code example ```go result := ratify.Verify(bundle, ratify.VerifyOptions{ RequiredScope: ratify.ScopePaymentsSend, Context: ratify.VerifierContext{ // populate amount, route, tenant, etc from the request }, }) if !result.Valid { http.Error(w, result.ErrorReason, http.StatusForbidden) return } ``` ```ts const result = await verifyBundle(bundle, { required_scope: SCOPE_PAYMENTS_SEND, context: { // populate amount, route, tenant, etc from the request }, }); if (!result.valid) { return new Response(result.error_reason, { status: 403 }); } ``` ```python result = verify_bundle(bundle, VerifyOptions(required_scope=SCOPE_PAYMENTS_SEND)) if not result.valid: raise PermissionError(result.error_reason) ``` ```rust let result = verify_bundle( &bundle, &VerifyOptions { required_scope: SCOPE_PAYMENTS_SEND.into(), ..Default::default() }, ); assert!(result.valid, "{}", result.error_reason); ``` ```c /* Extract and base64-decode X-Ratify-Proof header, then verify before passing to the route handler. */ const char *proof_hdr = http_get_header(req, "X-Ratify-Proof"); if (!proof_hdr) { http_respond(req, 401, "missing proof"); return; } char bundle_json[MAX_BUNDLE]; if (base64_decode(proof_hdr, bundle_json, sizeof(bundle_json)) < 0) { http_respond(req, 400, "malformed proof"); return; } RatifyVerifyResult *result = NULL; char *err = NULL; ratify_verify_bundle(bundle_json, "payments:send", (int64_t)time(NULL), &result, &err); if (!ratify_verify_result_is_valid(result)) { char *status = ratify_verify_result_identity_status(result); /* 401 for missing (handled above), 403 for all verification failures */ http_respond(req, 403, status); ratify_string_free(status); ratify_verify_result_free(result); ratify_error_free(err); return; } ratify_verify_result_free(result); ratify_error_free(err); /* proceed to route handler */ ``` --- # Physical AI > Verify Ratify proofs on robots, vehicles, drones, and infrastructure. Source: https://docs.identities.ai/guides/physical-ai/ The Physical AI surface is for actions that affect the real world. This is one of the commercial surfaces of the [Ratify Verify console](https://dev.identities.ai). The same protocol can also be implemented independently if you do not want the managed product. Canonical protocol references: - [README](https://github.com/identities-ai/ratify-protocol/blob/main/README.md) - [SPEC.md](https://github.com/identities-ai/ratify-protocol/blob/main/SPEC.md) - [SDK matrix](https://github.com/identities-ai/ratify-protocol/blob/main/docs/SDKS.md) Examples: - robot operation - robot movement - vehicle operation - drone flight - infrastructure control - actuator access ## What the surface does Physical AI uses the same Ratify proof bundle flow, but the verifier usually runs on-device or at the edge. The device or controller checks: - who authorized the action - what scope was granted - whether the delegation is still valid - whether the current context satisfies the constraints If the proof fails, the device should fail closed. SDKs are available in Go, TypeScript, Python, Rust, and C/C++. The C/C++ SDK ships `libratify_c.a` (static) and `libratify_c.so` (shared) with a cbindgen-generated header, supports RTOS targets (FreeRTOS, Zephyr) via custom entropy, and is `no_std` + `alloc` compatible for embedded deployments. See the [C/C++ SDK page](/sdk/c/) for build instructions and target matrix. ## Typical flow ```mermaid sequenceDiagram autonumber participant Admin as Org Admin participant App as Control Plane participant Device as Robot / Vehicle / Controller participant Ratify as Ratify SDK / Policy Admin->>App: Authorize device or mission App->>Ratify: Issue delegation for physical scope Device->>Ratify: Request verification or challenge Ratify-->>Device: Challenge / verification request Device->>Device: Sign challenge with SDK Device-->>Ratify: Proof bundle Ratify->>Ratify: Verify scope + constraints + revocation Ratify-->>Device: Allow or deny ``` ## SDK examples ```go result := ratify.Verify(bundle, ratify.VerifyOptions{ RequiredScope: ratify.ScopeRobotOperate, }) if !result.Valid { panic("deny actuation: " + result.ErrorReason) } ``` ```ts const result = await verifyBundle(bundle, { required_scope: SCOPE_ROBOT_OPERATE, }); if (!result.valid) { throw new Error(result.error_reason); } ``` ```python result = verify_bundle(bundle, VerifyOptions(required_scope=SCOPE_ROBOT_OPERATE)) if not result.valid: raise PermissionError(result.error_reason) ``` ```rust let result = verify_bundle( &bundle, &VerifyOptions { required_scope: SCOPE_ROBOT_OPERATE.into(), ..Default::default() }, ); assert!(result.valid, "{}", result.error_reason); ``` ```c /* Embedded controller: verify before actuating. Runs offline — no network call at verify time. */ #include "ratify.h" #include #include int authorize_actuation(const char *bundle_json) { RatifyVerifyResult *result = NULL; char *err = NULL; /* Simple path: scope check only, use system clock */ ratify_verify_bundle(bundle_json, "robot:operate", (int64_t)time(NULL), &result, &err); int authorized = ratify_verify_result_is_valid(result); if (!authorized) { char *status = ratify_verify_result_identity_status(result); fprintf(stderr, "actuation denied: %s\n", status); ratify_string_free(status); } ratify_verify_result_free(result); ratify_error_free(err); return authorized; } ``` With geo-constraints (robot must be within an authorized zone): ```c RatifyVerifierContext ctx = {0}; ctx.current_lat = 47.6062; ctx.current_lon = -122.3321; ctx.has_location = 1; RatifyVerifyOptions opts = {0}; opts.required_scope = "robot:operate"; opts.context = &ctx; RatifyVerifyResult *result = NULL; char *err = NULL; ratify_verify_bundle_opts(bundle_json, &opts, &result, &err); if (!ratify_verify_result_is_valid(result)) { /* Constraint denied, expired, revoked, etc. — fail closed */ halt_actuation(); } ratify_verify_result_free(result); ratify_error_free(err); ``` For RTOS targets (FreeRTOS, Zephyr) register your hardware TRNG before issuing any delegation or challenge: ```c static int board_entropy(uint8_t *buf, size_t len) { /* STM32 HAL example — replace with your platform TRNG */ for (size_t i = 0; i < len; i += 4) { uint32_t rnd; if (HAL_RNG_GenerateRandomNumber(&hrng, &rnd) != HAL_OK) return -1; size_t copy = (len - i < 4) ? (len - i) : 4; memcpy(buf + i, &rnd, copy); } return 0; } /* Call once at board init, before any ratify_* call */ ratify_set_entropy_source(board_entropy); ``` Build against the static library (no OS dependencies): ```bash # Cross-compile for ARM Cortex-M4 (STM32, NXP) cargo build --release --target thumbv7em-none-eabihf \ --features custom-entropy # Link in your firmware build arm-none-eabi-gcc main.c \ -I ratify-c/include \ -L ratify-c/target/thumbv7em-none-eabihf/release \ -lratify_c -o firmware.elf ``` See the [C/C++ SDK page](/sdk/c/) for the full target matrix and API reference. ## Scope examples Use `robot:operate` for high-level operation permission, `robot:move` for motion-only authorization, `vehicle:operate` for drive/flight systems, `physical:actuate` for direct actuation, and `infrastructure:control` or `infrastructure:access` for plant, building, or equipment control. ## Infrastructure example For infrastructure control, use `infrastructure:control` or `infrastructure:access` and bind the verifier context to the actual device state, location, time window, or other operational guardrails. That matters when the caller is not a person but a robot, PLC, controller, or remote operator. The important part is the same: the platform proves authorization, and the device enforces it before it moves the world. If you are building firmware, the integration point is the same proof bundle and verifier semantics, just compiled down into the embedded controller. The C/C++ SDK is available now — link against `libratify_c.a` and wire in your hardware TRNG via `ratify_set_entropy_source()` for RTOS targets. --- # Model Context Protocol (MCP) > Verify Ratify proofs before an MCP server executes a tool call. Source: https://docs.identities.ai/guides/mcp/ The Model Context Protocol (MCP) lets AI agents call tools, read resources, and use prompts hosted by MCP servers. By default, MCP has no authorization model — any client that can reach the server can invoke any exposed tool. Ratify adds the missing layer: **prove that a specific human authorized this agent to use this tool, with these scopes, for this long.** ``` ┌─────────────────────────────────┐ │ MCP Host (Claude Desktop, │ │ Cursor, etc.) — the AI app │ └────────────────┬────────────────┘ │ │ runs MCP client ▼ ┌─────────────────────────────────┐ │ AI Agent │ │ Holds: bundle{ │ │ delegations: [cert], │ │ challenge_sig: ... │ │ } │ └────────────────┬────────────────┘ │ │ MCP tools/call │ + Ratify proof bundle ▼ ┌─────────────────────────────────┐ │ MCP Server (your code) │ │ │ │ Before invoking the tool: │ │ - issue challenge │ │ - verify_bundle(...) │ │ - check required scope │ │ On valid → run the tool │ │ On invalid → return MCP error │ └─────────────────────────────────┘ ``` ## The pattern 1. **MCP server advertises a required scope per tool.** When the client lists tools, the server includes a `_meta.ratify_scope` field on each tool definition. 2. **Client sends a Ratify proof bundle along with the tool call.** Embedded in the request's `_meta` field (MCP-spec extension). 3. **Server verifies before executing.** Standard verifier algorithm; reject with an MCP error on any failure. ## Tool advertisement When the MCP server lists tools, it adds `_meta.ratify_scope`: ```json { "tools": [ { "name": "send_email", "description": "Send an email on the user's behalf", "inputSchema": { /* ... */ }, "_meta": { "ratify_scope": "email:send", "ratify_sensitive": true } }, { "name": "list_inbox", "description": "Read the user's inbox", "inputSchema": { /* ... */ }, "_meta": { "ratify_scope": "email:read" } } ] } ``` The client can decide upfront: "the user's proof bundle covers `email:read` but not `email:send`. Don't even expose `send_email` to the model." ## Tool call with proof The client wraps the proof in the MCP request's `_meta`: ```json { "jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": { "name": "send_email", "arguments": { /* ... */ }, "_meta": { "ratify_proof": { "agent_id": "b4a4c71795d676b6", "agent_pub_key": { "ed25519": "...", "ml_dsa_65": "..." }, "delegations": [ /* cert chain */ ], "challenge": "Zx8t4vQrM2...", "challenge_at": 1800000000, "challenge_sig": { "ed25519": "...", "ml_dsa_65": "..." } } } } } ``` ## Server-side verification ```python from mcp.server import Server from ratify_protocol import verify_bundle, IdentityStatus, ProofBundle server = Server("my-mcp-server") # Each tool has an associated required scope TOOL_SCOPES = { "send_email": "email:send", "list_inbox": "email:read", } @server.list_tools() async def list_tools(): return [ { "name": name, "description": "...", "inputSchema": {}, "_meta": {"ratify_scope": scope}, } for name, scope in TOOL_SCOPES.items() ] @server.call_tool() async def call_tool(name: str, arguments: dict, meta: dict | None = None): # 1. Extract the proof bundle proof_data = (meta or {}).get("ratify_proof") if not proof_data: raise McpError("missing_ratify_proof", "All tools require a Ratify proof") bundle = ProofBundle.from_dict(proof_data) # 2. Verify against the tool's required scope required = TOOL_SCOPES.get(name) if not required: raise McpError("unknown_tool", name) result = verify_bundle(bundle, required_scope=required) if result.identity_status != IdentityStatus.VALID: raise McpError( f"ratify_{result.identity_status.value}", result.error_reason or "authorization failed", ) # 3. Execute the actual tool return await execute_tool(name, arguments, principal_id=result.principal_id) ``` ```ts const TOOL_SCOPES: Record = { send_email: "email:send", list_inbox: "email:read", }; const server = new Server("my-mcp-server"); server.setRequestHandler("tools/list", async () => ({ tools: Object.entries(TOOL_SCOPES).map(([name, scope]) => ({ name, description: "...", inputSchema: {}, _meta: { ratify_scope: scope }, })), })); server.setRequestHandler("tools/call", async (request) => { const { name, arguments: args, _meta } = request.params; const proof = _meta?.ratify_proof as ProofBundle | undefined; if (!proof) { throw new Error("missing_ratify_proof"); } const required = TOOL_SCOPES[name]; if (!required) throw new Error("unknown_tool"); const result = await verifyBundle(proof, { required_scope: required }); if (result.identity_status !== IDENTITY_STATUS_VALID) { throw new Error(`ratify_${result.identity_status}`); } return await executeTool(name, args, { principal_id: result.principal_id }); }); ``` ```c /* * C/C++ MCP servers are uncommon — MCP tooling is Python/TypeScript-first. * If you are embedding MCP verification in a C/C++ host process, call * ratify_verify_bundle() directly on the proof bundle received from _meta. */ #include "ratify.h" int verify_mcp_tool_call(const char *tool_name, const char *bundle_json_from_meta) { /* Map tool name to required scope */ const char *required_scope = NULL; if (strcmp(tool_name, "send_email") == 0) required_scope = "comms:email:send"; else if (strcmp(tool_name, "list_inbox") == 0) required_scope = "comms:email:read"; else { return -1; /* unknown tool — reject */ } if (!bundle_json_from_meta) return 0; /* missing proof — reject */ RatifyVerifyResult *result = NULL; char *err = NULL; ratify_verify_bundle(bundle_json_from_meta, required_scope, (int64_t)time(NULL), &result, &err); int authorized = ratify_verify_result_is_valid(result); if (!authorized) { char *status = ratify_verify_result_identity_status(result); fprintf(stderr, "tool %s rejected: %s\n", tool_name, status); ratify_string_free(status); } ratify_verify_result_free(result); ratify_error_free(err); return authorized; } ``` ## The challenge problem MCP is stateless from the server's perspective — the client could come and go between calls. There's no natural place to put a long-lived challenge protocol. Two solutions: ### Option A: Client supplies its own challenge (simpler) The client generates a fresh challenge per request, signs it, and sends both. The server checks that the challenge timestamp is fresh and the signature verifies. The protocol's freshness window is a constant 300 seconds; a high-assurance server can apply a stricter application-level age check on top before accepting. No server-side challenge state. This is acceptable for many cases because: - Replay is bounded by the freshness window (and by your stricter app-level age check) - The server doesn't need to track per-client challenge state - Network round-trips stay at 1 (request + response) The bound is not single-use: a captured request can be re-presented within the window. If a tool call must never be accepted twice, use Option B with server-issued, tracked challenges, or dedupe by request ID at the application layer. ### Option B: Server issues challenge first (stricter) For sensitive tools (`email:send`, `payment:initiate`, etc.), the server can require an explicit challenge round: ``` 1. Client: tools/call request WITHOUT proof 2. Server: rejects with `ratify_challenge_required`, includes a fresh nonce in the error's _meta.ratify_challenge field 3. Client: re-issues tools/call WITH a proof bundle whose challenge matches the server's nonce 4. Server: verifies; on success executes the tool ``` This doubles the round-trip cost but gives the server tight control: it knows the challenge is fresh because it issued it 50ms ago. Use Option B for `ratify_sensitive: true` tools, Option A for everything else. ## Scope-aware tool exposure The MCP host (Claude Desktop, Cursor, etc.) can read the `_meta.ratify_scope` from the tool list and refuse to even surface tools the agent's bundle can't cover. This is a UX win: ``` User: "Send an email to my team about the meeting" Host: (checks bundle's effective scope, sees [email:read] but not [email:send]) Host: "I have your delegation for reading email but not sending. Want to re-authorize with email:send scope?" ``` This is much better than letting the model attempt the call and discover via an error response. ## Common pitfalls | Pitfall | Fix | |---|---| | Server verifies once at MCP handshake, then trusts subsequent requests | Verify on every `tools/call` — delegations can expire mid-session | | Tool scope hardcoded in server logic, not advertised in `_meta.ratify_scope` | Advertise the scope so the client/host can pre-filter | | Returning generic "unauthorized" on Ratify failure | Return the specific `identity_status` so the client can ask the user to re-authorize for the right scope | | Tool's required scope drifts from the actual action it performs | Keep `TOOL_SCOPES` next to the tool implementation; CI test that every tool has a registered scope | ## Where to next - [Agent-to-Agent (A2A)](/guides/a2a/) — when one MCP agent delegates to another - [Agentic API](/guides/api-gateway/) — the same pattern applied to general REST APIs - [Scopes](/concepts/scopes/) — the canonical scope vocabulary (`mcp:tool`, `mcp:resource`, `mcp:prompt` are also defined) --- # Agent-to-Agent (A2A) > Patterns for one agent transacting with, hiring, or sub-delegating to another agent. Source: https://docs.identities.ai/guides/a2a/ A2A — one agent transacting with another — is **the same primitive** as human-to-agent. Same `DelegationCert`, same `ProofBundle`, same verifier algorithm. The only thing that changes is who's at the root of the chain. ``` Human-to-agent Agent-to-agent ────────────── ────────────── Alice signs Alice signs │ │ ▼ ▼ Cert: Alice → Agent-A Cert: Alice → Agent-A │ │ │ │ then Agent-A signs: │ │ │ ▼ │ Cert: Agent-A → Agent-B │ │ ▼ ▼ Agent-A presents Agent-B presents bundle bundle with with TWO certs in chain ONE cert in chain [Alice→A, A→B] │ │ ▼ ▼ Verifier accepts Verifier walks the chain and intersects scopes ``` ## Three patterns ``` Pattern 1: Sub-delegation Pattern 2: Mutual auth ───────────────────────── ───────────────────── Agent-A hires Agent-B for a Two agents need to specific scoped task. Agent-B transact with each other. acts under Agent-A's authority, Both present bundles to transitively under Alice's. the other; both verify. Pattern 3: Signed receipts ───────────────────────── Either: a verifier emits a VerificationReceipt to its audit log; or N parties sign an atomic TransactionReceipt envelope. Both are stable and byte-for-byte interoperable. ``` ## Pattern 1: Sub-delegation The classic case. Alice's calendaring agent (Agent-A) needs to hire a travel-booking agent (Agent-B) to handle the actual reservation. Alice doesn't know Agent-B at all — she trusts Agent-A to scope it appropriately. For Agent-A to be allowed to sub-delegate at all, **Alice must grant the `identity:delegate` privilege** on her cert to Agent-A. Without it, the verifier rejects A→B with `identity_status: delegation_not_authorized`. (See [Scopes](/concepts/scopes/) for the full rule.) ``` Alice │ │ scope: [payments:send, identity:delegate] │ — identity:delegate is what lets Agent-A sub-delegate at all │ ▼ Agent-A (calendaring) │ │ scope: [payments:send] │ — Agent-A grants a subset to Agent-B; identity:delegate │ is NOT passed on, so Agent-B cannot further sub-delegate. │ ▼ Agent-B (travel booking) │ │ Agent-B presents a bundle to the airline API: │ delegations: [A→B, Alice→A] (leaf-first per SPEC §6.5) │ challenge_sig signed by Agent-B's hybrid key │ ▼ Airline API ✓ Both hybrid signatures (Ed25519 + ML-DSA-65) verify on every cert ✓ Chain well-formed (A→B.issuer == Alice→A.subject) ✓ Effective scope = lex-sorted intersection = [payments:send] ✓ identity:delegate present in A's grant from Alice ✓ ✓ No cert expired, none revoked, challenge fresh → identity_status: authorized_agent ``` ### Issuing the sub-delegation ```go // Alice's cert to Agent-A MUST include identity:delegate for A to sub-delegate. // (omitted here — assume aliceToA was issued with scope: // ["payments:send", ratify.ScopeIdentityDelegate]) // Agent-A (calendaring) sub-delegates to Agent-B (travel booking) subCert := ratify.DelegationCert{ CertID: "cert-a-to-b", Version: ratify.ProtocolVersion, IssuerID: agentA.ID, IssuerPubKey: agentA.PublicKey, SubjectID: agentB.ID, SubjectPubKey: agentB.PublicKey, Scope: []string{"payments:send"}, // identity:delegate intentionally NOT passed on IssuedAt: time.Now().Unix(), ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), // shorter than parent } ratify.IssueDelegation(&subCert, agentAPriv) // Agent-B builds a bundle. Chain order is leaf-first: [A→B, Alice→A]. bundle := ratify.ProofBundle{ AgentID: agentB.ID, AgentPubKey: agentB.PublicKey, Delegations: []ratify.DelegationCert{subCert, aliceToA}, Challenge: challengeFromVerifier, ChallengeAt: time.Now().Unix(), ChallengeSig: signedByAgentB, } ``` ```ts // aliceToA was previously issued with scope: // [SCOPE_PAYMENTS_SEND, SCOPE_IDENTITY_DELEGATE] const subCert: DelegationCert = { cert_id: "cert-a-to-b", version: PROTOCOL_VERSION, issuer_id: agentA.id, issuer_pub_key: agentA.public_key, subject_id: agentB.id, subject_pub_key: agentB.public_key, scope: [SCOPE_PAYMENTS_SEND], // identity:delegate NOT passed on constraints: [], issued_at: Math.floor(Date.now() / 1000), expires_at: Math.floor(Date.now() / 1000) + 24 * 3600, signature: { ed25519: new Uint8Array(), ml_dsa_65: new Uint8Array() }, }; issueDelegation(subCert, agentAPrivateKey); // Chain order: [leaf, root]. A→B is the leaf. const bundle: ProofBundle = { agent_id: agentB.id, agent_pub_key: agentB.public_key, delegations: [subCert, aliceToA], challenge: challengeFromVerifier, challenge_at: challengeAt, challenge_sig: signChallenge(challenge, challengeAt, agentBPriv), session_context: new Uint8Array(), stream_id: new Uint8Array(), stream_seq: 0, }; ``` ```python # alice_to_a was previously issued with scope: # [SCOPE_PAYMENTS_SEND, SCOPE_IDENTITY_DELEGATE] sub_cert = DelegationCert( cert_id="cert-a-to-b", version=PROTOCOL_VERSION, issuer_id=agent_a.id, issuer_pub_key=agent_a.public_key, subject_id=agent_b.id, subject_pub_key=agent_b.public_key, scope=[SCOPE_PAYMENTS_SEND], # identity:delegate NOT passed on issued_at=int(time.time()), expires_at=int(time.time()) + 24 * 3600, signature=None, # populated in place by issue_delegation ) issue_delegation(sub_cert, agent_a_priv) # Chain order: [leaf, root]. A→B is the leaf. bundle = ProofBundle( agent_id=agent_b.id, agent_pub_key=agent_b.public_key, delegations=[sub_cert, alice_to_a], challenge=challenge_from_verifier, challenge_at=challenge_at, challenge_sig=sign_challenge(challenge_from_verifier, challenge_at, agent_b_priv), ) ``` ```rust // alice_to_a was previously issued with scope: // vec![SCOPE_PAYMENTS_SEND.into(), SCOPE_IDENTITY_DELEGATE.into()] let mut sub_cert = 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_PAYMENTS_SEND.into()], // identity:delegate NOT passed on constraints: Vec::new(), issued_at: now, expires_at: now + 24 * 3600, signature: HybridSignature { ed25519: Vec::new(), ml_dsa_65: Vec::new() }, }; issue_delegation(&mut sub_cert, &agent_a_priv); // Chain order: [leaf, root]. A→B is the leaf. let bundle = ProofBundle { agent_id: agent_b.id.clone(), agent_pub_key: agent_b.public_key.clone(), delegations: vec![sub_cert, alice_to_a], challenge: challenge_from_verifier, challenge_at: now, challenge_sig: sign_challenge(&challenge_from_verifier, now, &agent_b_priv), session_context: Vec::new(), stream_id: Vec::new(), stream_seq: 0, }; ``` ```c /* alice_to_a was previously issued with scope: ["payments:send","identity:delegate"] Agent A now sub-delegates to Agent B — payments:send only, identity:delegate NOT forwarded. */ RatifyDelegationCert *sub_cert = NULL; char *err = NULL; /* ratify_sub_delegation_issue validates that alice_to_a_json grants identity:delegate and that the new scope is a subset. */ ratify_sub_delegation_issue( agent_a, agent_b, "[\"payments:send\"]", /* subset scope; no identity:delegate */ now, now + 86400LL, alice_to_a_json, /* parent cert JSON validates the chain */ &sub_cert, &err ); char *sub_cert_json = ratify_delegation_cert_to_json(sub_cert, &err); /* Build the two-hop proof bundle. Chain order: [leaf, root] — sub_cert is the leaf. */ char chain[8192]; snprintf(chain, sizeof(chain), "[%s,%s]", sub_cert_json, alice_to_a_json); RatifyProofBundle *bundle = NULL; ratify_proof_bundle_create_chain( agent_b, chain, challenge, 32, now, &bundle, &err ); char *bundle_json = ratify_proof_bundle_to_json(bundle, &err); /* Verifier: result.human_id is Alice, result.agent_id is Agent B */ RatifyVerifyResult *result = NULL; ratify_verify_bundle(bundle_json, "payments:send", now, &result, &err); ``` ### Verifier behavior with a chain The chain order on the wire is `[leaf, …, root]` per SPEC §6.5. The verifier walks **root-to-leaf** internally: ``` For chain [A→B, Alice→A] (leaf first, then root): 1. Both hybrid signatures on Alice→A verify with Alice's pubkey 2. Both hybrid signatures on A→B verify with Agent-A's pubkey (whose identity is established by Alice→A.subject_pub_key) 3. Alice→A.subject_id == A→B.issuer_id (chain well-formed) 4. Alice→A's scope contains identity:delegate (else delegation_not_authorized) 5. Both certs are within their issued_at / expires_at windows 6. Challenge signature verifies with Agent-B's pubkey (whose identity is established by A→B.subject_pub_key) 7. Effective scope = scope[Alice→A] ∩ scope[A→B], lex-sorted = [payments:send, identity:delegate] ∩ [payments:send] = [payments:send] 8. required_scope ("payments:send") ∈ effective scope ✓ 9. No constraints denied 10. No cert in chain is revoked → identity_status: authorized_agent ``` The intersection is **strict**: Agent-B never gets more than Agent-A was given, which is never more than Alice gave. This is the structural invariant. Notice `identity:delegate` does **not** appear in the effective scope — A intentionally chose not to grant it on, so B cannot sub-delegate further. ## Pattern 2: Mutual authentication Two agents need to trust each other. Each presents a bundle; each runs the verifier on the other's bundle. ``` ┌───────────────────────────────────────────────┐ │ │ │ Agent-A Agent-B │ │ │ │ ───── here's my bundle ─────▶ │ │ ◀──── verify_bundle(bundle_a) ── │ │ ◀──── here's my bundle ───── │ │ ──── verify_bundle(bundle_b) ─▶ │ │ │ │ Both ✓ → trust established │ │ Either ✗ → abort, log, no transaction │ │ │ └───────────────────────────────────────────────┘ ``` The challenge in each direction can use a **fresh nonce per side** so neither agent can replay the other's bundle. A typical handshake: ``` 1. Agent-A → Agent-B: nonce_a (32 random bytes) 2. Agent-B → Agent-A: nonce_b, bundle_b (Agent-B signed nonce_a in challenge_sig) 3. Agent-A: verify_bundle(bundle_b, challenge_was: nonce_a) → must pass 4. Agent-A → Agent-B: bundle_a (Agent-A signed nonce_b in challenge_sig) 5. Agent-B: verify_bundle(bundle_a, challenge_was: nonce_b) → must pass 6. Both trust each other; proceed ``` This is two challenge-response rounds, one per direction. Both agents prove they hold the live key for their respective identities. ## Pattern 3: Signed receipts Two **stable** receipt primitives ship in every SDK. They are interoperable byte for byte across Go, TypeScript, Python, Rust, and C/C++: | Primitive | What it attests | Who signs | |---|---|---| | `VerificationReceipt` | "I, this verifier, saw this exact bundle and decided `` at time T." | The **verifier** | | `TransactionReceipt` | "These N parties all atomically committed to these terms." | **Every party**, all over the same canonical signable | `VerificationReceipt` is the audit-trail primitive for a one-sided event ("Agent-B accepted Agent-A's proof"). `TransactionReceipt` is the atomic-commit primitive for an N-party deal — no partial-valid state, alter one party and every other party's signature breaks. ### VerificationReceipt — "I verified this bundle" `VerificationReceipt` is single-verifier; Agent-B signs an attestation that captures the bundle hash, the decision, and chains to the previous receipt by `prev_hash`, making modification, reordering, and missing interior entries in a verifier's log detectable when later receipts or an independently retained chain head are available. ```go // Agent-B, having Verify-ed Agent-A's bundle, issues an attestation: receipt, err := ratify.IssueVerificationReceipt( bundle, // what was verified result, // the VerifyResult prevReceiptHash, // chain to previous; 32 zero bytes for genesis time.Now().Unix(), agentBPriv, // the verifier's hybrid private key ) // receipt is byte-for-byte canonicalized; any auditor with agent_b.public_key // can VerifyVerificationReceipt(receipt) without trusting the verifier operator. ``` The wire shape is fixed (see `types.go` → `VerificationReceipt`): ```json { "version": 1, "verifier_id": "", "verifier_pub": { "ed25519": "...", "ml_dsa_65": "..." }, "bundle_hash": "<32-byte SHA-256 of canonical bundle bytes>", "decision": "authorized_agent", "human_id": "", "agent_id": "", "granted_scope": ["payments:send"], "verified_at": 1799999999, "prev_hash": "<32 bytes; zeros for genesis>", "signature": { "ed25519": "...", "ml_dsa_65": "..." } } ``` Auditors call `VerifyVerificationReceipt(receipt)` (or per-SDK equivalent) to re-check the signature against `verifier_pub`. ### TransactionReceipt — N-party atomic commit `TransactionReceipt` is the multi-party envelope. Every party presents a ProofBundle AND signs the same canonical envelope. Alter any party's `agent_id`, role, or pub key — every other party's signature becomes invalid. ```go // Both parties have agreed to terms; both produce signatures. // terms_canonical_json is the application's own schema — Ratify doesn't interpret it. receipt := ratify.TransactionReceipt{ Version: ratify.ProtocolVersion, TransactionID: "tx-abc-123", CreatedAt: time.Now().Unix(), TermsSchemaURI: "https://example.com/schemas/booking/v1.json", TermsCanonicalJSON: termsBytes, Parties: []ratify.ReceiptParty{ {PartyID: "p1", Role: "buyer", AgentID: agentA.ID, AgentPubKey: agentA.PublicKey, ProofBundle: bundleA}, {PartyID: "p2", Role: "seller", AgentID: agentB.ID, AgentPubKey: agentB.PublicKey, ProofBundle: bundleB}, }, PartySignatures: []ratify.ReceiptPartySignature{ {PartyID: "p1", Signature: sigByAgentA}, {PartyID: "p2", Signature: sigByAgentB}, }, } // Any auditor — including each party — runs the canonical envelope check: res := ratify.VerifyTransactionReceipt(&receipt, ratify.VerifyReceiptOptions{ PartyVerifyOptions: func(role string) ratify.VerifyOptions { switch role { case "buyer": return ratify.VerifyOptions{RequiredScope: "payments:send"} case "seller": return ratify.VerifyOptions{RequiredScope: "transact:sell"} } return ratify.VerifyOptions{} }, }) // res.Valid is true iff: // - every party's ProofBundle independently verifies under that role's options // - every listed party has exactly one signature // - every signature verifies against that party's agent_pub_key over the canonical envelope ``` Failure modes are atomic: any per-party failure → `res.Valid == false` with `ErrorReason` pinpointing the party that failed (`party_bundle_invalid`, `party_signature_invalid`, `duplicate_party_signature`, `missing_party_signature`, etc.). ### When to use which - **Audit logs / receipt streams** → `VerificationReceipt`. Cheap, single-signer, `prev_hash` chain detects backdating. - **N-party atomic commerce** (escrow, swap, multi-sig action) → `TransactionReceipt`. The envelope's whole point is that you cannot tamper with one party's role without invalidating every other signature. Both are byte-for-byte interoperable — a Go-issued receipt verifies in Python, TypeScript, Rust, and C/C++ without rounding error. ## Common pitfalls | Pitfall | Why it's wrong | Fix | |---|---|---| | Sub-delegating a wider scope than received | Verifier rejects via intersection; chain produces empty effective scope | Sub-delegate a subset | | Sub-delegation expiry longer than parent | Verifier rejects: child cert is expired-by-parent at the parent's `expires_at` | Set child expiry ≤ parent expiry | | Forgetting to include the parent cert in the bundle | Verifier can't find Agent-A's pubkey to verify A→B's signature; `bad_signature` | Include every cert in the chain | | Reusing a challenge from a previous handshake | Verifier rejects: stale `challenge_at`. **Mutual auth must use fresh nonces per direction.** | Generate fresh nonces | ## Source Chain semantics: [SPEC.md §6 (Delegation chain)](https://github.com/identities-ai/ratify-protocol/blob/main/SPEC.md). Receipts: `types.go` → `VerificationReceipt` and `TransactionReceipt`; `receipt_verify.go` → `VerifyTransactionReceipt`. Every primitive on this page is stable in `1.0.0-alpha.20` and byte-for-byte interoperable across all five reference SDKs. ## Where to next - [MCP guide](/guides/mcp/) — A2A in the specific context of Model Context Protocol - [Delegate, Present, Verify](/concepts/delegate-present-verify/) — chain verification details - [Scopes](/concepts/scopes/) — what scope intersection looks like --- # Commercial Ratify Platform > How a developer registers with Ratify, connects an agent platform, and uses the commercial surfaces. Source: https://docs.identities.ai/verify/overview/ Ratify Verify is the managed authorization-decision service built on the open Ratify Protocol. Its initial wedge is Agentic API: receiving systems call Verify before executing an MCP tool, A2A request, REST operation, or other consequential agent action. The [Ratify Verify console](https://dev.identities.ai) (alpha) is the commercial control plane that sits on top of the free protocol and SDKs. The production console hostname is not yet announced; public documentation links only to the live alpha console. Canonical protocol sources: - [README](https://github.com/identities-ai/ratify-protocol/blob/main/README.md) - [SPEC.md](https://github.com/identities-ai/ratify-protocol/blob/main/SPEC.md) - [SDK matrix](https://github.com/identities-ai/ratify-protocol/blob/main/docs/SDKS.md) Use this guide when you are building an agent platform, a service that accepts delegated proof bundles, or a customer-facing product that needs hosted policy, audit, or enforcement. ## What you actually integrate You still use the SDKs. The SDKs do the cryptographic work: - create human and agent identities - issue delegation certificates - generate challenges - sign proof bundles - verify proof bundles The commercial Ratify Platform by Identities AI supplies the hosted control plane around those SDK calls: - the managed Verify endpoint (`POST /v1/ratify/verify`), called from your middleware or gateway - middleware and gateway integration: map each protected operation to a required scope, then enforce the result in your code - revocation, audit, and approval workflows - developer registration, publisher profile, and domain verification - platform registration, API key and signing-secret management - team/org delegation controls - Agentic API through the managed Verify endpoint, with Conversational AI and additional surface integrations on the roadmap ## Registration flow 1. Sign in to the [Ratify Verify console](https://dev.identities.ai). 2. Decide whether you are integrating as a personal account or an organization. 3. If you only need Agentic API development, a personal account can register one draft platform. 4. If you need publishing, Conversational AI, Meetings, Physical AI, team members, or multiple platforms, create an organization. 5. For an organization, verify the publisher domain before publishing platforms. 6. Register a platform in the Developer Console. 7. Choose the surface type: Agentic API, Conversational AI, Meetings, or Physical AI. 8. Copy the API key and signing secret shown at registration. 9. Connect your app or gateway with those credentials. If you are specifically building a voice agent or telephony product, see [Voice Surface](/guides/voice). ```mermaid flowchart LR A[Sign in to the alpha console at dev.identities.ai] --> B[Choose personal account or organization] B --> C[Register a platform] C --> D[Receive API key + secrets] D --> E[Connect your runtime] E --> F[Verify bundles at Agentic API, Conversational AI, Meetings, or Physical AI] F --> G[Receive callbacks for lifecycle and revocation] ``` ## Which surface to choose | Surface | Who can use it | Role and status | | --- | --- | --- | | Agentic API | personal accounts and organizations | Initial wedge for MCP, A2A, REST, and consequential programmatic actions. Managed Verify API in alpha; customers enforce results in middleware or gateways | | Conversational AI | organizations | Next expansion for voice, video, and real-time agent interactions (call-signaling verification, live consent, speaking authorization). Managed integration roadmap | | Meetings | organizations | Additional integration and visual demonstration surface (join enforcement, participant policy). Control plane implemented; Zoom adapter under validation | | Physical AI | enterprises | Longer-term managed surface for robots, vehicles, drones, and infrastructure. Protocol and SDK primitives exist; managed integration later | ## Agentic API integration For Agentic API, your agent platform or backend verifies proof bundles before executing a protected request. Typical flow: 1. Receive the agent request and its proof (an `X-Ratify-Proof` header or equivalent transport envelope). 2. Map the requested operation to a required scope such as `execute:tool`, `data:read`, or `payments:send`. 3. Call Ratify Verify. 4. Inspect the decision and reason. 5. Execute only when authorized; reject failed proofs before business logic runs. 6. Retain the verification record. ## Platform connection flow When your product connects to a customer organization, use the connection credentials from the Ratify Platform: - `X-Ratify-API-Key` to authenticate calls to the Ratify API - `X-Ratify-Signature` to verify inbound events and webhooks Use the SDKs to build the proofs that the platform verifies. Use the platform to manage the customer-facing configuration, policy, and audit trail. ### Minimal platform skeleton ```go apiKey := os.Getenv("RATIFY_API_KEY") webhookSecret := os.Getenv("RATIFY_WEBHOOK_SECRET") _ = apiKey _ = webhookSecret ``` ```ts const apiKey = process.env.RATIFY_API_KEY; const webhookSecret = process.env.RATIFY_WEBHOOK_SECRET; void apiKey; void webhookSecret; ``` ```python api_key = os.getenv("RATIFY_API_KEY") webhook_secret = os.getenv("RATIFY_WEBHOOK_SECRET") ``` ```rust let api_key = std::env::var("RATIFY_API_KEY").ok(); let webhook_secret = std::env::var("RATIFY_WEBHOOK_SECRET").ok(); let _ = (api_key, webhook_secret); ``` ```c const char *api_key = getenv("RATIFY_API_KEY"); const char *webhook_secret = getenv("RATIFY_WEBHOOK_SECRET"); /* Both are required — abort early if missing */ if (!api_key || !webhook_secret) { fprintf(stderr, "RATIFY_API_KEY and RATIFY_WEBHOOK_SECRET must be set\n"); exit(1); } ``` ## What to build in your app If you are integrating Ratify into your own agentic product, your app usually has three responsibilities: 1. Generate or load identities through the SDK. 2. Present proof bundles whenever your agent acts. 3. Call Ratify Platform endpoints when you need hosted registration, policy, or enforcement. That is the clean split: - protocol and SDKs for cryptography - commercial platform by Identities AI for operations For callback behavior and event payloads, see [Callbacks and Webhooks](/verify/callback-contract/). --- # Agent Platform Integration > How an agentic platform integrates Ratify Protocol and the commercial Ratify surface. Source: https://docs.identities.ai/verify/platform-onboarding/ This is the page to read if you are building a meeting-note platform, a finance-agent platform, a generic MCP/A2A platform, or a physical AI platform. Ratify gives you two things: - a free, open protocol and SDK layer for proof objects - a commercial surface for customer onboarding, policy, audit, callbacks, and managed enforcement Canonical protocol sources: - [README](https://github.com/identities-ai/ratify-protocol/blob/main/README.md) - [SPEC.md](https://github.com/identities-ai/ratify-protocol/blob/main/SPEC.md) - [SDK matrix](https://github.com/identities-ai/ratify-protocol/blob/main/docs/SDKS.md) The managed product lives in the [Ratify Verify console](https://dev.identities.ai). ## What the platform developer owns Your platform owns: - the user experience - agent runtime - customer workspace setup - local caching and session state - UX for delegations, consent, and revocation - application-specific business logic ## What Ratify owns Ratify owns: - the proof format - the verifier semantics - the SDK implementations - the hosted control plane - the callback delivery layer - Ratify's product model covers Agentic API, Conversational AI, Meetings, and Physical AI; implementation status differs by surface ## Concrete integration patterns | Platform type | What the customer wants | Ratify surface | What your runtime does | | --- | --- | --- | --- | | Generic MCP / A2A platform | "Only approved tools or peers may be invoked." | Agentic API | Verify proof bundles before tool dispatch or agent-to-agent routing | | Finance agent | "Only approved agents may call payments or ledger routes." | Agentic API | Present a proof for `payments:send`, `data:read`, or `execute:tool` | | Voice platform | "Only approved voice agents may speak in a live call." | Conversational AI | Present a proof carrying the scope required by the call operation and respond to signaling challenges | | Meeting note taker | "Only approved agents may join our Zoom/Teams/Meet calls." | Meetings | Present a proof for `meeting:attend` and respond to challenges | | Physical AI platform | "Only approved devices may actuate or move." | Physical AI | Verify proof bundles on-device before actuation or motion | ## Registration flow 1. Sign in to the [Ratify Verify console](https://dev.identities.ai). 2. Decide whether you are starting with a personal account or an organization. 3. Personal accounts can register one draft `Agentic API` platform. 4. If you need publishing, Conversational AI, Meetings, Physical AI, or multiple platforms, create an organization. 5. For organizations, verify the publisher domain if you want publishing. 6. Register a platform in the Developer Console. 7. Choose the surface type: `Agentic API`, `Conversational AI`, `Meetings`, or `Physical AI`. 8. Ratify issues an API key, a signing secret, and a webhook secret. 9. Your platform stores those credentials in its own control plane. 10. Your customer admins connect their workspace to your platform. ## End-to-end flow ```mermaid sequenceDiagram autonumber participant Customer as Customer Org Admin participant Platform as Your Agent Platform participant Ratify as Ratify Platform participant Agent as Agent Runtime participant Surface as Protected Surface Customer->>Platform: Enable integration Platform->>Ratify: Register platform + callback URL Ratify-->>Platform: API key, signing secret, webhook secret Customer->>Platform: Approve agent usage in workspace Platform->>Agent: Create or load delegated identity Surface->>Ratify: Request challenge or verification Ratify->>Agent: Challenge / verification request Agent->>Agent: Sign with Ratify SDK Agent-->>Surface: Proof bundle Surface->>Surface: Verify bundle or forward to Ratify-managed enforcement Ratify-->>Platform: Callback event for lifecycle or approval ``` ## Who challenges whom The verifier challenges the agent. In practice: - the meeting surface, gateway, or device issues a fresh challenge - the agent runtime signs that challenge with the SDK - the proof bundle is returned to the verifier - the verifier checks scope, expiry, revocation, constraints, and freshness The platform does not challenge Ratify. The agent proves itself to Ratify. ## SDK verification examples The SDK call pattern is the same across languages. Only the language syntax changes. ### Meeting scope ```go result := ratify.Verify(bundle, ratify.VerifyOptions{ RequiredScope: ratify.ScopeMeetingAttend, }) if !result.Valid { http.Error(w, result.ErrorReason, http.StatusForbidden) return } ``` ```ts const result = await verifyBundle(bundle, { required_scope: SCOPE_MEETING_ATTEND, }); if (!result.valid) { throw new Error(result.error_reason); } ``` ```python result = verify_bundle(bundle, VerifyOptions(required_scope=SCOPE_MEETING_ATTEND)) if not result.valid: raise RuntimeError(result.error_reason) ``` ```rust let result = verify_bundle( &bundle, &VerifyOptions { required_scope: SCOPE_MEETING_ATTEND.into(), ..Default::default() }, ); assert!(result.valid, "{}", result.error_reason); ``` ```c RatifyVerifyResult *result = NULL; char *err = NULL; ratify_verify_bundle(bundle_json, "meeting:attend", (int64_t)time(NULL), &result, &err); if (!ratify_verify_result_is_valid(result)) { char *status = ratify_verify_result_identity_status(result); http_respond(req, 403, status); ratify_string_free(status); ratify_verify_result_free(result); ratify_error_free(err); return; } ratify_verify_result_free(result); ratify_error_free(err); ``` ### Finance / Agentic API scope ```go result := ratify.Verify(bundle, ratify.VerifyOptions{ RequiredScope: ratify.ScopePaymentsSend, }) if !result.Valid { http.Error(w, result.ErrorReason, http.StatusForbidden) return } ``` ```ts const result = await verifyBundle(bundle, { required_scope: SCOPE_PAYMENTS_SEND, }); if (!result.valid) { return new Response(result.error_reason, { status: 403 }); } ``` ```python result = verify_bundle(bundle, VerifyOptions(required_scope=SCOPE_PAYMENTS_SEND)) if not result.valid: raise PermissionError(result.error_reason) ``` ```rust let result = verify_bundle( &bundle, &VerifyOptions { required_scope: SCOPE_PAYMENTS_SEND.into(), ..Default::default() }, ); assert!(result.valid, "{}", result.error_reason); ``` ```c RatifyVerifyResult *result = NULL; char *err = NULL; ratify_verify_bundle(bundle_json, "payments:send", (int64_t)time(NULL), &result, &err); if (!ratify_verify_result_is_valid(result)) { char *status = ratify_verify_result_identity_status(result); http_respond(req, 403, status); ratify_string_free(status); ratify_verify_result_free(result); ratify_error_free(err); return; } ratify_verify_result_free(result); ratify_error_free(err); ``` ### Physical AI scope ```go result := ratify.Verify(bundle, ratify.VerifyOptions{ RequiredScope: ratify.ScopeRobotOperate, }) if !result.Valid { return fmt.Errorf("deny actuation: %s", result.ErrorReason) } ``` ```ts const result = await verifyBundle(bundle, { required_scope: SCOPE_ROBOT_OPERATE, }); if (!result.valid) { throw new Error(result.error_reason); } ``` ```python result = verify_bundle(bundle, VerifyOptions(required_scope=SCOPE_ROBOT_OPERATE)) if not result.valid: raise PermissionError(result.error_reason) ``` ```rust let result = verify_bundle( &bundle, &VerifyOptions { required_scope: SCOPE_ROBOT_OPERATE.into(), ..Default::default() }, ); assert!(result.valid, "{}", result.error_reason); ``` ```c RatifyVerifyResult *result = NULL; char *err = NULL; ratify_verify_bundle(bundle_json, "robot:operate", (int64_t)time(NULL), &result, &err); if (!ratify_verify_result_is_valid(result)) { char *status = ratify_verify_result_identity_status(result); /* Fail closed — deny actuation before any physical action */ deny_actuation(device_id, status); ratify_string_free(status); ratify_verify_result_free(result); ratify_error_free(err); return; } ratify_verify_result_free(result); ratify_error_free(err); /* proceed with actuation */ ``` ## The callback contract Your platform also needs an HTTPS callback endpoint. Ratify uses the callback URL for asynchronous state changes, not for proof verification itself. The endpoint receives: - `connection.created` - `connection.key_rotated` - `connection.removed` - `delegation.approved` - `delegation.denied` - `delegation.revoked` For exact event payloads and signature rules, see [Callbacks and Webhooks](/verify/callback-contract/). ## What the callback endpoint must do Your callback implementation should: - verify `X-Ratify-Signature` - verify `X-Ratify-Timestamp` - reject stale requests - deduplicate with `X-Ratify-Delivery-ID` - respond with a 2xx status to acknowledge success - return a non-2xx status if you want Ratify to retry Use the callback for state updates: - store connection metadata - update caches after key rotation - mark delegations approved or denied - invalidate permissions after revocation - deactivate a removed connection ## Simple mental model If you are building a platform, the split is: - your product owns the agent runtime and customer UX - Ratify owns the cryptographic proof model - Ratify's commercial surface owns customer onboarding, policy, audit, callbacks, and managed enforcement - the SDKs are the bridge between your runtime and the verifier --- # Callbacks and Webhooks > What Ratify sends to your callback URL and what your platform must implement. Source: https://docs.identities.ai/verify/callback-contract/ When you register a platform, Ratify stores a `callback_url` for that platform or connection. Ratify uses that URL to deliver asynchronous events back to your platform. This is how your app learns about connection lifecycle changes, delegation decisions, key rotation, and revocation state without polling. The callback URL is part of the commercial surface operated by Identities AI — the [Ratify Verify console](https://dev.identities.ai). It complements the free protocol and SDKs; it does not replace them. ## What it is used for Use the callback URL to keep your platform in sync with Ratify-managed state: - record that a connection was created - persist the connection's API key prefix and webhook secret - react when a delegation is approved or denied - invalidate caches when a delegation is revoked - update a connected workspace when a key rotates - deactivate a customer connection when it is removed Think of it as a control-plane feed. It is not where proof verification happens. It is where your platform learns, "something changed, update your local state." If the callback succeeds, Ratify assumes your system has consumed the event. If the callback fails, Ratify retries later. ## What Ratify sends Current events include: - `connection.created` - `connection.key_rotated` - `connection.removed` - `delegation.approved` - `delegation.denied` - `delegation.revoked` Each delivery is `POST`ed as JSON over HTTPS. ### Delivery envelope Every delivery includes: - `Content-Type: application/json` - `X-Ratify-Signature` - `X-Ratify-Timestamp` - `X-Ratify-Delivery-ID` The JSON body always includes an `event` field and event-specific fields. ## How to consume it Your platform should: 1. read the raw request body 2. verify the signature using the shared webhook secret 3. reject stale timestamps 4. deduplicate by delivery ID 5. parse `event` 6. update local state for that event 7. return `204 No Content` or any other 2xx once the event is consumed Do not do proof verification inside the callback handler. The callback is for lifecycle state, not for live authorization checks. ## What the endpoint must do Your endpoint should: 1. accept only HTTPS 2. verify `X-Ratify-Signature` 3. verify `X-Ratify-Timestamp` 4. reject stale deliveries 5. deduplicate using `X-Ratify-Delivery-ID` 6. return a 2xx status to acknowledge success 7. return a non-2xx status if you want Ratify to retry the delivery Ratify signs the payload with HMAC-SHA256 using the platform's webhook secret. The signature covers the timestamp and the body. ## Verification example ```go func verifyRatifyWebhook(secret string, timestamp string, sig string, body []byte) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(timestamp)) mac.Write([]byte(".")) mac.Write(body) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(sig), []byte(expected)) } ``` ```ts import { createHmac, timingSafeEqual } from "node:crypto"; function verifyRatifyWebhook(secret: string, timestamp: string, sig: string, body: string): boolean { const expected = createHmac("sha256", secret) .update(`${timestamp}.${body}`) .digest("hex"); return timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); } ``` ```python def verify_ratify_webhook(secret: str, timestamp: str, sig: str, body: bytes) -> bool: expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest() return hmac.compare_digest(sig, expected) ``` ```rust fn verify_ratify_webhook(secret: &str, timestamp: &str, sig: &str, body: &[u8]) -> bool { let mut mac = Hmac::::new_from_slice(secret.as_bytes()).unwrap(); mac.update(timestamp.as_bytes()); mac.update(b"."); mac.update(body); hex::encode(mac.finalize().into_bytes()) == sig } ``` ```c #include #include #include /* Returns 1 if valid, 0 if invalid. Uses timing-safe comparison. */ int verify_ratify_webhook(const char *secret, const char *timestamp, const char *sig, const uint8_t *body, size_t body_len) { /* Build the signed payload: timestamp + "." + body */ size_t ts_len = strlen(timestamp); uint8_t payload[ts_len + 1 + body_len]; memcpy(payload, timestamp, ts_len); payload[ts_len] = '.'; memcpy(payload + ts_len + 1, body, body_len); uint8_t digest[32]; unsigned int digest_len = 32; HMAC(EVP_sha256(), secret, (int)strlen(secret), payload, sizeof(payload), digest, &digest_len); /* Hex-encode the digest */ char expected[65]; for (int i = 0; i < 32; i++) sprintf(expected + 2*i, "%02x", digest[i]); expected[64] = '\0'; /* Timing-safe compare — CRYPTO_memcmp from OpenSSL */ return CRYPTO_memcmp(sig, expected, 64) == 0; } ``` ## Consumer example ```go func handleRatifyWebhook(w http.ResponseWriter, r *http.Request, secret string) { body, _ := io.ReadAll(r.Body) timestamp := r.Header.Get("X-Ratify-Timestamp") sig := r.Header.Get("X-Ratify-Signature") deliveryID := r.Header.Get("X-Ratify-Delivery-ID") if !verifyRatifyWebhook(secret, timestamp, sig, body) { http.Error(w, "invalid signature", http.StatusUnauthorized) return } if isDuplicateDelivery(deliveryID) { w.WriteHeader(http.StatusNoContent) return } var event map[string]any _ = json.Unmarshal(body, &event) switch event["event"] { case "connection.created": markConnectionActive(event["connection_id"].(string), event["api_key"].(string)) case "connection.key_rotated": updateAPIKeyPrefix(event["connection_id"].(string), event["api_key_prefix"].(string)) case "delegation.approved": storeDelegation(event["cert_json"]) case "delegation.denied": markRequestDenied(event["request_id"].(string)) case "delegation.revoked": invalidateCert(event["cert_id"].(string)) case "connection.removed": deactivateConnection(event["connection_id"].(string)) } rememberDelivery(deliveryID) w.WriteHeader(http.StatusNoContent) } ``` ```ts import { createHmac, timingSafeEqual } from "node:crypto"; export async function handleRatifyWebhook(req: Request): Promise { const body = await req.text(); const timestamp = req.headers.get("X-Ratify-Timestamp") ?? ""; const sig = req.headers.get("X-Ratify-Signature") ?? ""; const deliveryId = req.headers.get("X-Ratify-Delivery-ID") ?? ""; const expected = createHmac("sha256", WEBHOOK_SECRET) .update(`${timestamp}.${body}`) .digest("hex"); if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { return new Response("invalid signature", { status: 401 }); } if (await isDuplicateDelivery(deliveryId)) { return new Response(null, { status: 204 }); } const event = JSON.parse(body) as { event: string; [key: string]: unknown }; switch (event.event) { case "connection.created": await markConnectionActive(String(event.connection_id), String(event.api_key)); break; case "connection.key_rotated": await updateAPIKeyPrefix(String(event.connection_id), String(event.api_key_prefix)); break; case "delegation.approved": await storeDelegation(event.cert_json); break; case "delegation.denied": await markRequestDenied(String(event.request_id)); break; case "delegation.revoked": await invalidateCert(String(event.cert_id)); break; case "connection.removed": await deactivateConnection(String(event.connection_id)); break; } await rememberDelivery(deliveryId); return new Response(null, { status: 204 }); } ``` ```python def handle_ratify_webhook(request): body = request.get_data() timestamp = request.headers.get("X-Ratify-Timestamp", "") sig = request.headers.get("X-Ratify-Signature", "") delivery_id = request.headers.get("X-Ratify-Delivery-ID", "") if not verify_ratify_webhook(WEBHOOK_SECRET, timestamp, sig, body): return ("invalid signature", 401) if is_duplicate_delivery(delivery_id): return ("", 204) event = json.loads(body) etype = event["event"] if etype == "connection.created": mark_connection_active(event["connection_id"], event["api_key"]) elif etype == "connection.key_rotated": update_api_key_prefix(event["connection_id"], event["api_key_prefix"]) elif etype == "delegation.approved": store_delegation(event["cert_json"]) elif etype == "delegation.denied": mark_request_denied(event["request_id"]) elif etype == "delegation.revoked": invalidate_cert(event["cert_id"]) elif etype == "connection.removed": deactivate_connection(event["connection_id"]) remember_delivery(delivery_id) return ("", 204) ``` ```rust fn handle_ratify_webhook(req: Request, secret: &str) -> Response { let timestamp = req.header("X-Ratify-Timestamp").unwrap_or(""); let sig = req.header("X-Ratify-Signature").unwrap_or(""); let delivery_id = req.header("X-Ratify-Delivery-ID").unwrap_or(""); let body = req.body_bytes(); if !verify_ratify_webhook(secret, timestamp, sig, &body) { return response(401, "invalid signature"); } if is_duplicate_delivery(delivery_id) { return response(204, ""); } let event: serde_json::Value = serde_json::from_slice(&body).unwrap(); match event["event"].as_str().unwrap_or("") { "connection.created" => mark_connection_active( event["connection_id"].as_str().unwrap(), event["api_key"].as_str().unwrap(), ), "connection.key_rotated" => update_api_key_prefix( event["connection_id"].as_str().unwrap(), event["api_key_prefix"].as_str().unwrap(), ), "delegation.approved" => store_delegation(&event["cert_json"]), "delegation.denied" => mark_request_denied(event["request_id"].as_str().unwrap()), "delegation.revoked" => invalidate_cert(event["cert_id"].as_str().unwrap()), "connection.removed" => deactivate_connection(event["connection_id"].as_str().unwrap()), _ => {} } remember_delivery(delivery_id); response(204, "") } ``` ```c #include "ratify.h" #include int handle_ratify_webhook(HttpRequest *req, const char *secret) { const char *timestamp = http_header(req, "X-Ratify-Timestamp"); const char *sig = http_header(req, "X-Ratify-Signature"); const char *delivery_id = http_header(req, "X-Ratify-Delivery-ID"); const uint8_t *body = http_body(req); size_t body_len = http_body_len(req); if (!verify_ratify_webhook(secret, timestamp, sig, body, body_len)) return http_respond(req, 401, "invalid signature"); if (is_duplicate_delivery(delivery_id)) return http_respond(req, 204, ""); /* Parse event type from JSON body */ char event_type[64], connection_id[64], cert_id[64], api_key[128], api_key_prefix[32], cert_json[4096], request_id[64]; json_get_string(body, "event", event_type, sizeof(event_type)); json_get_string(body, "connection_id", connection_id, sizeof(connection_id)); json_get_string(body, "cert_id", cert_id, sizeof(cert_id)); json_get_string(body, "api_key", api_key, sizeof(api_key)); json_get_string(body, "api_key_prefix",api_key_prefix,sizeof(api_key_prefix)); json_get_string(body, "cert_json", cert_json, sizeof(cert_json)); json_get_string(body, "request_id", request_id, sizeof(request_id)); if (strcmp(event_type, "connection.created") == 0) mark_connection_active(connection_id, api_key); else if (strcmp(event_type, "connection.key_rotated") == 0) update_api_key_prefix(connection_id, api_key_prefix); else if (strcmp(event_type, "delegation.approved") == 0) store_delegation(cert_json); else if (strcmp(event_type, "delegation.denied") == 0) mark_request_denied(request_id); else if (strcmp(event_type, "delegation.revoked") == 0) invalidate_cert(cert_id); else if (strcmp(event_type, "connection.removed") == 0) deactivate_connection(connection_id); remember_delivery(delivery_id); return http_respond(req, 204, ""); } ``` ## Minimal verification logic ```text expected = HMAC_SHA256(secret, timestamp + "." + raw_body) accept only if expected matches X-Ratify-Signature accept only if abs(now - timestamp) < 5 minutes ``` ## Event payloads ### `connection.created` Use this to persist the platform connection and mark the integration active. Important fields: - `connection_id` - `platform_id` - `org_id` - `org_name` - `allowed_scopes` - `callback_url` - `api_key` - `webhook_signing_secret` Consumer action: - store the connection record - store the API key prefix or key reference - store the webhook secret securely - mark the integration as active - optionally pre-seed the workspace with the allowed scopes ### `connection.key_rotated` Use this to replace the stored API key prefix or update connection state after key rotation. Important fields: - `connection_id` - `api_key_prefix` - `previous_prefix` Consumer action: - replace the stored key reference - invalidate any cached API auth material - notify operators if the platform relies on long-lived credentials ### `delegation.approved` Use this to record that a human approved a delegation request and to provision downstream access if needed. Important fields: - `request_id` - `connection_id` - `org_id` - `agent_id` - `approved_scope` - `cert_json` - `delegation_key_statement` Consumer action: - persist the approved delegation certificate - mark the request as approved in your UI - let the agent runtime resume or continue the session - attach the cert to your audit log ### `delegation.denied` Use this to stop pending authorization flows and surface the denial in your UI or audit log. Important fields: - `request_id` - `connection_id` - `org_id` - `agent_id` Consumer action: - stop the pending authorization flow - surface the denial reason in your UI - keep the agent blocked until a new request is approved ### `delegation.revoked` Use this to invalidate cached permissions immediately. Important fields: - `cert_id` - `agent_id` - `connection_id` - `org_id` - `reason` Consumer action: - remove the cert from your cache - reject any session or token derived from that cert - notify the connected user or admin that access was revoked ### `connection.removed` Use this to disable the integration and remove any tokens or local state associated with that connection. Important fields: - `connection_id` - `org_id` - `removed_at` Consumer action: - disable the integration locally - delete API credentials and cached scopes - stop accepting proofs for that connection ## What your code should do after verification Treat the callback as a state update, not a request to do business logic synchronously. Good handling: - update your local connection record - refresh cached scopes - record the event in your audit log - notify the workspace admin if a connection was removed or revoked Bad handling: - trusting the body before verifying the signature - using the callback as the source of a user-facing action without idempotency - retrying forever on 4xx errors ## Response rules - return `204 No Content` when you have processed the event - return another 2xx if your framework prefers it - return `401` only for bad signatures or stale timestamps - return `500`/`503` for temporary local failures so Ratify retries - never return `200` before your handler has stored or queued the event ## Example platform endpoint ```text POST /ratify-webhook Headers: X-Ratify-Signature: X-Ratify-Timestamp: X-Ratify-Delivery-ID: Body: JSON event payload ``` ```go func handleRatifyWebhook(w http.ResponseWriter, r *http.Request, secret string) { timestamp := r.Header.Get("X-Ratify-Timestamp") sig := r.Header.Get("X-Ratify-Signature") deliveryID := r.Header.Get("X-Ratify-Delivery-ID") body, _ := io.ReadAll(r.Body) mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(timestamp)) mac.Write([]byte(".")) mac.Write(body) if !hmac.Equal([]byte(sig), []byte(hex.EncodeToString(mac.Sum(nil)))) { http.Error(w, "invalid signature", http.StatusUnauthorized) return } _ = deliveryID // use this for idempotency w.WriteHeader(http.StatusNoContent) } ``` The callback URL is your integration surface. Ratify is the sender. Your platform is the receiver. The SDKs are still used for proof creation and verification elsewhere in the flow. If you are implementing the protocol yourself, host this endpoint in your own control plane. If you are using the managed product, configure it in the [Ratify Verify console](https://dev.identities.ai). --- # Pricing > The protocol is free forever. Ratify Verify charges per verification for the managed verifier, and on annual contract for compliance-grade trust and sovereign deployments. Source: https://docs.identities.ai/verify/pricing/ The **Ratify Protocol** is Apache-2.0 open source and free forever — every SDK, every primitive, every cryptographic interface. Running your own verifier with the open SDKs costs you nothing and never will. **Ratify Verify** is the managed layer maintained by [Identities AI](https://identities.ai). It exists for teams who don't want to run their own verifier infrastructure, want compliance-grade audit chains, or need to deploy in sovereign / regulated environments. It is sold across **four motions**, each with a different shape, a different buyer, and a different commitment level. ## The four motions | Motion | What it is | Who buys it | Commitment | Price | |---|---|---|---|---| | **Open Protocol** | The protocol, the spec, and five open SDKs | Anyone | None | **Free, forever** | | **Verify Operational** | Managed verifier endpoint with metered verification, audit trail, and delegation controls | Engineering teams putting Ratify into production | Pay-as-you-go | **$0.002 / verification** (orgs: $50/mo minimum; personal workspaces: no minimum) | | **Verify Trust** | Signed-receipt audit chain, anchor-bound identity, custom constraint registry (capabilities scoped per engagement) | Compliance officers, CTOs, regulated industries | Annual design-partner contract | **From $30,000 / year** | | **Verify Sovereign** | Licensed self-hosted Verify control plane, no phone-home, customer-owned keys (scoped per engagement) | EU institutions, federal / defense, regulated finance, healthcare with strict residency | Annual design-partner license | **From $50,000 / year** | Cutting across all four motions is one horizontal capability — the **Platform Registry** — which is **free for any platform** that wants to publish a verifiable identity in the Ratify ecosystem. It's a Plaid-style directory, not a tier. See the dedicated section below the four motions. Each motion is explicit about what it enables and what it doesn't. There is no tier ladder to climb — you choose the motion that matches what you're actually doing, and you can combine them (a customer can be on Operational for day-to-day usage and Sovereign for one regulated workload at the same time). --- --- ## Surfaces are not pricing-gated Ratify's four product surfaces are not pricing tiers. Agentic API is the current alpha wedge, delivered through the managed Verify endpoint and integrated into customer middleware or gateways. Conversational AI is the next managed integration target. The Meetings control plane and Zoom adapter are under validation. Physical AI is a later managed integration, while its protocol primitives are already available in the open SDKs. | Surface | What it authorizes | Example platforms | |---|---|---| | **Agentic API** | AI agents making programmatic calls via MCP, A2A, REST | Any MCP server, A2A agent, REST API | | **Conversational AI** | AI agents in real-time conversations, voice or video | Bland, Retell, Vapi, ElevenLabs, HeyGen, Tavus | | **Meetings** | AI agents in structured, multi-party sessions | Zoom, Teams, Google Meet | | **Physical AI** | Embodied AI agents in the physical world | Drones, robots, vehicles, infrastructure | Voice and video are unified under **Conversational AI** — the authorization model is identical whether the channel is audio or video. Platforms like HeyGen and Tavus (real-time video avatars) and Bland or Retell (voice agents) both fit here. The channel is an adapter configuration detail, not a product distinction. --- ## Capability matrix Verify Trust and Verify Sovereign are design-partner offerings. Capabilities and deployment requirements are scoped contractually. Items marked roadmap are not currently available. | | Verify Operational | Verify Trust | Verify Sovereign | |---|---|---|---| | **Surface pricing access** | Not gated | Not gated | Not gated | | **Agentic API** | Managed Verify API alpha | Managed Verify API alpha; Trust evidence capabilities per the rows below | Roadmap: customer-deployed Verify | | **Conversational AI** | Roadmap | Roadmap | Roadmap | | **Meetings** | Control plane implemented; Zoom under validation | Same | Deployment packaging unverified | | **Physical AI** | Managed integration later | Managed integration later | Protocol primitives available; managed packaging later | | **Hash-chained audit trail (organization scope)** | Alpha | Included | Customer-controlled (Sovereign scope) | | **Audit retention** | 30 days (alpha) | 365 days, through design partnership | Customer-defined (Sovereign scope) | | **Signed VerificationReceipts** | — | Available through design partnership | Available through design partnership | | **Anchor-bound identity in audit** | — | Available through design partnership | Available through design partnership | | **Custom constraint registry** | — | Design-partner development | Design-partner development | | **SOC2 / ISO evidence export** | — | Roadmap | Roadmap | | **SAML / SSO** | — | Under validation | Under validation | | **Custom DPA** | — | Available by agreement, subject to legal review | Available by agreement, subject to legal review | | **Priority support** | — | Scoped contractually | Scoped contractually | | **Self-hosted deployment** | — | — | Roadmap; available for design-partner development | | **No phone-home** | — | — | Required design target; verified per deployment | | **Customer-owned keys / HSM** | — | — | Roadmap | For the custom constraint registry: the protocol extension mechanism is implemented; the managed registry distribution is not yet available. The motions differ in what each engagement is scoped to deliver, not in artificial feature flags: - **Operational → Trust**: the goal is cryptographic evidence your verifications happened and are defensible in an audit; not just logs, but tamper-evident, independently verifiable receipts, scoped per engagement. - **Trust → Sovereign**: the goal is data residency. The Verify control plane moves inside your infrastructure, so no third party, including Identities AI, sits in your verification data path. --- ## Platform catalog — publisher trust levels Any platform can publish to the Ratify catalog. The trust badge reflects how the publisher's identity was verified. Downstream consumers (and their connection policies) decide what trust level they require. | Badge | What it proves | Requirements | |---|---|---| | **Personal Publisher** | Verified Ratify account | Email verification | | **Verified Individual** | Real identity with established external anchor | Email + GitHub or Google OAuth | | **Verified Publisher** | Organization that owns its claimed domain | Org + DNS TXT record verification | There is no separate "Trust-Verified" catalog badge — the Verify Trust commercial tier gives *the buyer* better compliance infrastructure; it is not a signal about the publisher's trustworthiness. A Verified Publisher on Operational and one on Trust are identical in the catalog, because what matters for the catalog is domain verification, not the buyer's compliance tier. --- ## Platform Registry — free for any platform Cutting across all four motions is the **Platform Registry**, a free, public directory of platforms that participate in the Ratify ecosystem. Platforms here include the verifier side (companies that *accept* Ratify proofs from external agents — meeting hosts, voice gateways, API platforms, custodial wallets) and the issuer side (companies whose users *delegate to agents* that go act on other platforms). Registration is **free**, regardless of which motion (if any) the platform is on. The incentive structure mirrors Plaid's institution directory or the GitHub Apps marketplace: Identities AI captures value from network effects, not from charging the platforms. ### What a registered platform gets - A **verified PlatformID** (e.g. `platform.otter.ai`, `platform.slack.com`). Once claimed and DNS-verified, the ID is yours; nobody else can publish under it. - **DNS domain verification** via TXT record (same flow as the Organization domain claim). - **A public listing** in the platform catalog — discoverable by any other platform looking for who supports what. - **A `.well-known/ratify-platform.json` bundle** published at your verified domain. Any verifier anywhere can fetch it offline; this is how third parties discover and trust your public keys. - The **"Ratify Protocol Verified Platform" badge** for your own site, GitHub repository, README, or product UI. - A **profile page** with name, logo, what you verify or what you issue, supported scopes, contact, links to your own docs. ### What it does NOT cost extra for Operating a verifier — using the open SDK, your own `RevocationProvider`, your own audit log — is the **Open Protocol** motion. Registering as a platform on top of that costs nothing. Many platforms will only ever use the Open Protocol + Platform Registry combination and never pay for Verify managed infrastructure. That's fine; you're a brand-credibility signal for the protocol. ### Where it lands inside the motions | Account type | Platforms you can claim | |---|---| | **Personal Account** (Operational) | 1 platform under any domain you can DNS-verify | | **Organization** (Operational / Trust / Sovereign) | Unlimited platforms under the Organization's verified domain | A Personal Account user wanting to register a second platform creates an Organization (the Org owns the platforms, the human is the Owner-role member). This mirrors how GitHub treats `@me`'s 1 free private repo vs. unlimited under an Organization. ### What this means concretely - **Otter.ai (verifier-side platform):** stays on Open Protocol. Implements `verify_bundle()` at the meeting-join handler. Registers `platform.otter.ai` for $0. Gets a public listing and the verified badge. Pays nothing to Identities AI. - **Slack (verifier + issuer):** registers `platform.slack.com` for $0. Probably uses Verify Operational ($0.002/verification) because they want centrally managed revocation checks across their infrastructure, but the registry profile itself is still free. - **A solo dev's hobby project:** signs up with Personal Account, registers `platform.solodev.dev` for $0 under their own verified domain. Starts with $50 in alpha credits (≈25,000 verifications); the platform listing is permanent. ### How to register In the alpha console at `https://dev.identities.ai`, the flow is: 1. Sign in with Google, Microsoft, or email magic link. 2. Choose "Register a platform" from the Studio dashboard. 3. Enter the domain you want to claim. Studio shows you a DNS TXT record. 4. Add the TXT record at your DNS provider; click "Verify." 5. Fill out the profile page (name, logo, description, supported scopes, contact). 6. Optional: tick "Publish to public directory." Detailed registration steps live at [Onboard a platform](/verify/platform-onboarding/). --- ## Open Protocol — free, forever The protocol is yours. Implement it, fork it, run it in production behind your firewall, build a competing managed verifier with it — none of that is gated by anyone. ### What you get - The protocol specification (CC-BY-4.0) — the normative source of truth at [SPEC.md](https://github.com/identities-ai/ratify-protocol/blob/main/SPEC.md). - Five reference SDKs (Go, TypeScript, Python, Rust, C/C++) under Apache-2.0. All five produce byte-identical canonical JSON and verify each other's proofs. - Every cryptographic primitive: hybrid Ed25519 + ML-DSA-65 signing and verification, delegation chains, scope intersection, first-class constraints, revocation lists, session tokens, transaction receipts, witness entries. - Every provider interface defined in [SPEC §17](https://github.com/identities-ai/ratify-protocol/blob/main/SPEC.md#17-sdk-architecture-provider-interfaces): `RevocationProvider`, `PolicyProvider`, `AuditProvider`, `ConstraintEvaluator`, `AnchorResolver`. Each comes with a working no-op default; you supply your own implementations. - The full 79-fixture cross-language conformance suite and the cross-SDK byte-equivalence corpus (10 vectors covering `BundleHash`, `VerifierContextHash`, `PolicyVerdictSignBytes`, `VerificationReceiptSignBytes`). ### What you don't get (without Verify) - A hosted verifier endpoint; you operate your own. - Hosted revocation checks; you operate your own revocation provider and synchronization infrastructure. - A signed, hash-chained audit archive; you can build one against the open `VerificationReceipt` primitive, but you maintain it. - Managed surface integrations; commercially, Agentic API runs through the managed Verify endpoint today, a Meetings control plane and Zoom adapter are under validation, and further surfaces are on the roadmap. - A managed policy authoring UI (on the Verify roadmap); your team writes policy in code or manages a Rego/OPA service themselves. ### Who this is right for - Developers evaluating Ratify before committing. - Open-source projects that need cryptographic agent authorization but don't need managed infrastructure. - Teams with strong internal infrastructure who prefer to operate the verifier themselves. - Air-gapped deployments where the managed Verify is structurally inappropriate (see Sovereign for an air-gappable commercial alternative). --- ## Verify Operational — managed verifier, pay per verification Verify Operational provides the managed, metered authorization endpoint for Agentic API workflows. Call `POST /v1/ratify/verify` from an MCP server, A2A service, API gateway, or application middleware before executing a protected action. Each decision is attributed to the calling connection and recorded in the managed audit trail. **Pricing:** `$0.002 per verification, $50/month minimum.` The $50 minimum is your monthly commitment; it includes the first 25,000 verifications. There is no verification cap above that; usage past the minimum bills at the same per-verification rate. ### Two onboarding paths The Operational plane supports two account types. The Studio renders identically for both; the only differences are how identity is anchored and whether a Verify Trust contract is possible. #### Personal Developer Account For a solo developer building, evaluating, or running a side project under their own identity. The fastest path from "I want to try Verify" to a working API key. - Sign in with Google, Microsoft, or email magic link. - Account ID is auto-generated (`usr_xK2pA9...`); no domain claim required. - **$50 in alpha credits on first sign-in** (≈25,000 verifications) so you can integrate and ship before putting in a card. Credits are pre-loaded automatically. - Same Studio, same API, same pricing past the credits ($0.002/verification). Personal workspaces have **no monthly minimum** — you pay for exactly what you use. - Cannot enter into a Verify Trust contract (Trust requires a verified legal entity). - Can create an Organization later. Creating an Organization does not consume or change your Personal account; both coexist. #### Organization For teams putting Ratify into production behind a company or product. - One human creates the Organization. That human becomes the first Owner. They invite teammates (Owner / Admin / Member roles). - Organization claims a domain via DNS TXT verification. The verified domain becomes the Organization's Publisher Domain — visible in any `Anchor` records the Organization signs on behalf of human roots. - Same pricing as Personal: $0.002/verification, $50/month minimum. Organizations do not receive the $50 alpha credit. - An Organization can be the legal-entity counter-party to a Verify Trust or Sovereign contract. A single human can own one Personal account and be a member of any number of Organizations. The Studio's top-left context switcher (same UX as GitHub, Stripe, Vercel) lets you move between contexts; each has its own billing, audit history, and API keys. #### Migration and lifecycle - A Personal account can **create** an Organization, but cannot **convert** itself into one. Both coexist after creation. - An Organization can **transfer** specific platforms to itself from any member's Personal account, with audit-log entries on both sides. - An Organization can be **deleted** by an Owner (irreversible, members lose access, billing closes out, optionally exports the audit archive on the way out). - An Organization **cannot downgrade** to a Personal account. Legal-entity boundaries and audit-trail attribution must stay intact. ### What Verify Operational enables Current capabilities: - **Managed, API-key-authenticated verification endpoint**, metered per decision. - **Hosted revocation checks** on every verification. - **Delegation and scope controls.** - **30-day searchable audit retention.** - **Decision records** for successful and rejected verification attempts. On the roadmap: - Conversational AI integrations. - Managed policy authoring and distribution ([`PolicyProvider`](/concepts/provider-architecture#policyprovider-evaluate_policybundle-context---bool-error) backed). - Managed push-based revocation. - Verification-decision webhooks into your log / SIEM pipeline. - Additional surface adapters. - Meetings control plane and Zoom adapter, currently under validation. - Physical AI managed integration, later. ### What Verify Operational does NOT include - **Signed `VerificationReceipt` archive.** You can issue receipts via the open SDK; the *managed, chained, signed-with-Verify's-keys archive* is part of Verify Trust. - **Anchor-bound audit.** The open `AnchorResolver` interface is yours to wire to your own IdP; Verify's *managed* SSO-attested identity binding is part of Verify Trust. - **Custom constraint type registry.** Anyone can register `com.mycompany.*` constraint evaluators in their own SDK call; the *managed cluster-wide registry* that survives the customer's deployment is part of Verify Trust. - **365-day audit retention, SAML/SSO into Studio, custom DPAs.** All part of Verify Trust. - **Self-hosted / air-gapped deployment.** Part of Verify Sovereign. --- ## Verify Trust — annual contract for compliance-grade audit Trust is the layer that says *"if something goes wrong with an agent, this is what defends you."* It is sold as an annual contract because the value is in the contractual commitments (data retention, audit-trail integrity, identity attestation), not in per-call metering. Trust is a design-partner offering: each engagement scopes the capabilities below contractually, and items marked roadmap are not currently available. **Pricing:** design-partner engagements **from $30,000 / year**. ### What a Trust engagement scopes on top of Operational - **Signed `VerificationReceipt` archive.** Every verification produces a hybrid-signed, hash-chained receipt (see [SPEC §17.5](/concepts/provider-architecture#lever-1--verificationreceipt-175)). Signed receipts bind each recorded decision to its predecessor, so modification, reordering, and missing interior entries are detectable when later receipts or an independently retained chain head are available; detecting tail truncation requires external retention or checkpointing. A downloadable SOC2/ISO evidence bundle is on the roadmap. - **Custom constraint type registry** (design-partner development). Define types such as `com.mycompany.region_allow` or `com.mycompany.parental_consent`, with Verify maintaining the cluster-wide registry so any of your tenants' agents can reference them. Extension parameters are already carried in the certificate's signed bytes. Managed registry distribution is not yet available. Issuers using a custom type must ensure every downstream verifier registers the same exact name. - **Anchor-bound audit.** Every verification chain links to the human root's SSO assertion (Okta, Azure AD, Google Workspace). Auditors can prove *"this verification was tied to a SAML-asserted identity from Okta"*, not just *"this verification happened."* - **SAML / SSO** for the Studio web app itself. Admins sign in via your IdP. - **90-day standard audit retention.** Optional 365-day retention for regulated industries. - **Custom DPA, sub-processor list, contractual data-residency commitments** (US / EU / Switzerland). - **Priority support**, scoped contractually. ### Who Trust is right for - Companies pursuing SOC2 / ISO 27001 / HIPAA / PCI-DSS and needing a defensible audit chain for AI-agent-driven actions. - Mid-market and enterprise customers whose agents touch money, identity, or regulated data. - Anyone selling agent-driven products to enterprise customers who will ask for SOC2 evidence. --- ## Verify Sovereign — licensed, self-hosted, no phone-home Sovereign is for customers who cannot have a third-party verifier in their data path. The EU institution, the federal agency, the regulated bank with national-residency requirements. They take the Verify binaries, run them inside their own infrastructure, and hold 100% of the root revocation keys themselves. Sovereign is a design-partner offering: deployment packaging is not yet available, and each engagement scopes delivery, support, and validation contractually. **Pricing:** design-partner licenses **from $50,000 / year**. Support and Technical Account Manager options are scoped contractually. ### What a Sovereign engagement scopes - Trust capabilities running entirely inside the customer's network, per engagement. - **No phone-home** as a target requirement, verified per deployment: the verifier does not communicate with Identities AI infrastructure. - **Customer-owned root revocation keys.** Identities AI never has access. - Delivery packaging (Helm chart, Docker image, or hardened VM image) is not yet available; it is scoped by design partnership. - Air-gapped upgrade path: signed Verify releases verifiable via the open `VerificationReceipt` chain, scoped per engagement. - Contractual data-residency commitment tied to the customer's chosen geography. ### Who Sovereign is right for - EU sovereign deployments (Schrems-II strict residency). - Federal / defense customers requiring FedRAMP-style isolation. - Regulated finance customers with national-residency requirements. - Healthcare systems with HIPAA + state-specific residency. - Customers in F500 industries with internal-only AI deployments and "no SaaS on this data" policies. --- ## Why this model, and not tiered SaaS The "Bronze / Silver / Gold" SaaS tier ladder works when the underlying product is the same and you're price-discriminating on feature flags. Verify is structurally different from that. The four motions correspond to **four genuinely different things you can be buying**: - The Open Protocol is **a public good** — a standard you can adopt without a vendor relationship. - Verify Operational is **a service** — managed infrastructure that you pay for as you use it, no commitment, no tier negotiation. - Verify Trust is **a contractual artifact** — an audit chain that defends you in a dispute, sold on annual contract because the value is the commitment. - Verify Sovereign is **a licensed product** — software you operate yourself, with contractual support. These are different motions because they involve different commitments, different relationships, and different buyers. Collapsing them into a tier ladder would have hidden the actual structure of the offering. Specifically, this model rejects three common SaaS-era pricing patterns that don't fit: - **No per-seat pricing.** Agents make calls; seats don't. Charging per seat is a pre-AI assumption that no longer maps to how AI products operate. - **No artificial verification caps as upgrade gates.** Volume is metered honestly across motions; it's never the lever that forces an upgrade. The lever is whether you need signed-receipt-archive (Trust) or self-hosting (Sovereign) — capabilities, not counters. - **No black-box "Contact Sales for Enterprise."** Trust starts at $30K/year. Sovereign starts at $50K/year. Both are published floors. Contracts above those numbers are negotiated; nothing below. --- ## When the open protocol is enough You do **not** need to pay Verify if you are: - Building an open-source tool that uses the Ratify primitive for local-only checks. - Operating your own verifier infrastructure with self-managed revocation, policy, and audit. - Building inside an air-gapped environment where the managed Verify is structurally inappropriate (and you don't need a Sovereign license). Specifically: **the authorization decision, observability, and identity binding are all in the protocol.** A `PolicyProvider` returning `false` rejects the bundle (the decision your organization then enforces). An `AuditProvider` sees every verification (observability). An `AnchorResolver` binds verifications to external identity (attestation). The open SDK ships interfaces and working defaults for all of it. Ratify Verify sells the managed operational layer around authorization decisions. In alpha today, that means the metered Verify API, hosted revocation checks, delegation controls, and a hash-chained audit trail. Agentic API is the initial wedge: developers place the decision call before MCP tools, A2A requests, REST operations, and other consequential agent actions. Conversational AI and surface-specific integrations build on the same service. The protocol is a public good. We charge for operating the managed infrastructure around it. --- **Maintained by [Identities AI, Inc.](https://identities.ai)** Ratify Protocol™ and identities.ai™ are trademarks of Identities AI, Inc. U.S. patent application pending. --- # Specification > Where the normative specification lives and how to read it. Source: https://docs.identities.ai/reference/spec/ The normative specification is **[SPEC.md in the protocol repo](https://github.com/identities-ai/ratify-protocol/blob/main/SPEC.md)**. It is the source of truth for: - Data structure shapes (`DelegationCert`, `ProofBundle`, `RevocationList`, `HumanRoot`, `AgentIdentity`, `HybridPublicKey`, `HybridPrivateKey`, `HybridSignature`) - The canonical JSON serialization algorithm - The signing functions (`delegationSignBytes`, `challengeSignBytes`) - The hybrid signature pair (Ed25519 + ML-DSA-65) - The verifier algorithm (every check, every fail-closed branch, every status code) - The 54-scope canonical vocabulary plus the 14 wildcards and the `custom:` extension - Constraint encoding (`geo_circle`, `geo_polygon`, `geo_bbox`, `time_window`, `max_speed_mps`, `max_amount`, `max_rate`) - The revocation list format and verifier behavior - Versioning rules (when fixture bytes are allowed to change) ## How to read it The spec is structured for two audiences: **Implementers** start at §5 (data structures) and §7 (signing functions). Read straight through to §11 (revocation). The 79 conformance fixtures verify everything in those sections; if your implementation matches the bytes, it matches the spec. **Reviewers and auditors** start at §2 (threat model) and §3 (security goals). §4 covers the hybrid signature rationale. §12 documents non-goals — what Ratify explicitly does NOT solve (transport security, key recovery without backup, hardware attestation). ## Spec license The specification text is licensed under **[CC-BY-4.0](https://github.com/identities-ai/ratify-protocol/blob/main/docs/LICENSES.md)**. You can copy, redistribute, adapt, and build upon the spec for any purpose, including commercial, as long as you give attribution. The reference code (SDKs, CLI, fixture generator) is licensed under **Apache-2.0**. Different license; same repo. ## Related normative documents | Document | What it specifies | |---|---| | [SPEC.md](https://github.com/identities-ai/ratify-protocol/blob/main/SPEC.md) | The protocol | | [docs/EXPLAINED.md](https://github.com/identities-ai/ratify-protocol/blob/main/docs/EXPLAINED.md) | Threat model + worked attack scenarios | | [docs/AGENT_TO_AGENT.md](https://github.com/identities-ai/ratify-protocol/blob/main/docs/AGENT_TO_AGENT.md) | A2A patterns (mutual, sub-delegation, receipts) | | [docs/SDKS.md](https://github.com/identities-ai/ratify-protocol/blob/main/docs/SDKS.md) | Conformance contract for new SDKs | | [docs/RELEASES.md](https://github.com/identities-ai/ratify-protocol/blob/main/docs/RELEASES.md) | Release process and version coordination | ## Trademark Ratify Protocol™ and identities.ai™ are trademarks of **Identities AI, Inc.** The trademark and patent rights are not licensed under either the Apache-2.0 or CC-BY-4.0 licenses governing the code or spec. See [docs/LICENSES.md](https://github.com/identities-ai/ratify-protocol/blob/main/docs/LICENSES.md) for the full per-asset license breakdown. U.S. patent application pending. ## Where to next - [Conformance suite](/reference/conformance/) — what implementations must pass - [Versioning](/reference/versioning/) — when the spec is allowed to change - [Glossary](/reference/glossary/) — every term used in the spec, defined --- # Versioning > How protocol versions and SDK versions work, when fixture bytes can change, and the alpha → stable ladder. Source: https://docs.identities.ai/reference/versioning/ The Ratify Protocol uses **semantic versioning** with one important twist: the version applies to **all five SDKs as a single coordinated release**. At any tagged version `vX.Y.Z`, every SDK produces and verifies byte-identical canonical JSON. You can't have Go on v1.0.0-alpha.20 and Python on v1.0.0-alpha.5 and expect interop. ## Two version concepts ### Protocol version The integer `version` field on every signed object. **Currently `1`.** This changes only for wire-breaking changes: - New required field in `DelegationCert` or `ProofBundle` - Change to `delegationSignBytes` / `challengeSignBytes` algorithm - Change to verifier algorithm semantics - Change to a canonical scope's meaning - Change to canonical JSON serialization rules Protocol version bumps are rare and **always trigger a major version bump in the SDKs.** v1 SDKs cannot consume v2 bundles by default; v2 SDKs may accept v1 bundles during a documented migration window. ### SDK version Standard semver `MAJOR.MINOR.PATCH[-pre]`: ``` 1.0.0-alpha.20 │ │ │ │ │ │ │ │ │ └── pre-release identifier (alpha/beta/rc + counter) │ │ │ └──────── patch │ │ └─────────── minor │ └───────────── major └─────────────── protocol version (matches `version` field) ``` | Bump | Triggers | Fixture-byte change? | |---|---|---| | **Patch** (`1.0.0` → `1.0.1`) | Bug fixes, performance improvements, doc updates | No | | **Minor** (`1.0.x` → `1.1.0`) | New optional fields, new scopes, new helper functions | No — every v1.0 fixture must still pass byte-identically | | **Major** (`1.x.x` → `2.0.0`) | Protocol-version bump. Wire-breaking. | Yes — new `testvectors/v2/` ships alongside v1 | | **Pre-release** (`-alpha.x`, `-beta.x`, `-rc.x`) | Active development before a stable major | **Maybe** — alpha can change fixtures. Beta/rc cannot. | ## The alpha → stable ladder ``` 1.0.0-alpha.1 → initial drop ← fixtures: MAY change hybrid Ed25519 + ML-DSA-65 working conformance suite established 1.0.0-alpha.N → alpha series ← fixtures: MAY change bug fixes, polish, design partner feedback. Each alpha documented in the GitHub Release notes if fixtures change. 1.0.0-beta.1 → feature-complete, audit started ← fixtures: FROZEN no more fixture changes; only bugs and security fixes. 1.0.0-rc.N → release candidate ← fixtures: FROZEN audit results integrated. Last chance to find regressions. 1.0.0 → stable ← fixtures: FROZEN FOR v1 every external audit passed first design-partner production deployment is live at least 3 SDKs published to mainstream registries 1.0.1, 1.0.2... → patches ← fixtures: never change 1.1.0, 1.2.0... → minor — additive only ← fixtures: never change (new fields are optional; v1.0 fixtures still pass) 2.0.0 → major — wire-breaking ← fixtures: NEW SUITE adds testvectors/v2/ v1 fixtures remain for migration ``` **Where we are now: v{PROTOCOL_VERSION}.** Fixture bytes may still change between alphas. Consumers should pin to an exact alpha version (`==1.0.0a20` in Python, `@v1.0.0-alpha.20` in Go, etc.) rather than a range. ## SDK version tags Every SDK release is tagged in git as `sdk--` alongside the protocol-level tag: ``` v1.0.0-alpha.20 ← protocol-level tag (what Go modules consume) sdk-go-v1.0.0-alpha.20 ← Go SDK marker sdk-typescript-v1.0.0-alpha.20 ← TypeScript SDK marker sdk-python-v1.0.0-alpha.20 ← Python SDK marker sdk-rust-v1.0.0-alpha.20 ← Rust SDK marker sdk-c-v1.0.0-alpha.20 ← C / C++ SDK marker ``` All five tags point at the same commit. The protocol-level `v*` tag is what `go get` consumes; the `sdk-*` tags are what the release workflow uses to decide which registries to push to. ## Pinning recommendations | Use case | Recommended pin | |---|---| | Alpha consumer | Exact version (`==1.0.0a20`, `@v1.0.0-alpha.20`) | | Beta consumer | Exact version, refresh on each beta release | | RC consumer | Exact version | | Stable (`1.x`) consumer | Caret/tilde range (`^1.0.0` in npm, `~=1.0` in Python). Minor bumps are additive only. | | Major (`2.x`) migration | Explicit version bump; read the v2 migration guide | ## Conformance is the contract The 79 fixtures in `testvectors/v1/` are the **canonical conformance set** for v1. Any implementation passing all 79 is byte-for-byte interoperable with every other implementation at that version. If a minor release adds an 80th fixture, every v1 SDK must pass it. If a major v2 release ships, `testvectors/v2/` replaces v1 for new code and v1 stays for backward-compatible migration paths. ## Where to next - [SDK packages](/sdk/packages/) — current versions per registry - [Conformance suite](/reference/conformance/) — what the fixtures actually check - [Specification](/reference/spec/) — the normative protocol --- # Conformance suite > The 79 canonical test vectors that define byte-for-byte interop across every Ratify implementation. Source: https://docs.identities.ai/reference/conformance/ Every Ratify SDK ships with a test that loads the canonical fixtures from `testvectors/v1/` and runs them through every protocol operation. The current conformance corpus is: - **{FIXTURE_COUNT} wire-format / verifier fixtures** — each is one JSON file. Cover delegation signing, challenge signing, full bundle verification (positive and negative), revocation, scope intersection, and all eight built-in constraint types. - **10 cross-SDK byte-equivalence vectors** (bundled in `cross_sdk_vectors.json`) — pin canonical bytes for `VerificationReceipt`, `TransactionReceipt`, `PolicyVerdict`, `SessionToken`, and the canonical `VerifierContext` hash so a verifier in one language can byte-for-byte verify a primitive signed in another. If all 79 wire fixtures + the 10 cross-SDK vectors pass in your SDK, your SDK is byte-for-byte interoperable with every other Ratify SDK on the planet. That is the contract. ## What's in the suite ``` testvectors/v1/ ├── happy_path_depth_{1,2,3}.json (positive verify, chain depth 1–3) ├── reject_*.json (negative verify — every rejection branch) ├── constraint_geo_*.json (geo_circle / polygon / bbox, inside / outside / antimeridian) ├── constraint_time_window_*.json (non-wrap / wrapping windows) ├── constraint_max_{amount,rate,speed_mps}_*.json ├── constraint_unknown_denied.json (fail-closed on unknown constraint type) ├── key_rotation_valid.json (KeyRotationStatement) ├── session_token_*.json (v1.1 fast path) ├── stream_bundle_*.json (v1.1 stream binding) ├── verification_receipt_*.json ├── transaction_receipt_*.json ├── presence_represent_allowed.json (presence:represent scope) ├── no_expiry_cert.json (no-expiry sentinel) └── cross_sdk_vectors.json (10 byte-equivalence vectors) ``` Each fixture is a deterministic JSON file with: 1. **Inputs.** Keys (seeded so they're reproducible), cert/bundle/list shape, verifier context. 2. **Expected canonical bytes.** What `delegationSignBytes` or `challengeSignBytes` should produce (hex-encoded). Any SDK that produces different bytes here has a non-byte-identical canonicalizer and is rejected. 3. **Expected verification outcome.** For verify fixtures: the exact `VerifyResult` struct the verifier should return, including `identity_status`, `granted_scope`, and `error_reason`. The fixtures are generated by `cmd/ratify-testvectors` in the protocol repo. The Go reference is authoritative; every other SDK must produce identical output. ## Running the suite ```bash cd ratify-protocol # Go go test ./... # TypeScript (cd sdks/typescript && npm install && npm run test:conformance) # Python (cd sdks/python && pip install -e '.[dev]' && pytest -q) # Rust (cd sdks/rust && cargo test) # C / C++ (cd sdks/c && cargo test --test conformance -- --nocapture) (cd sdks/c && cargo test --test api) (cd sdks/c && cargo test --test advanced) ``` Expected output: **all fixtures pass.** Any failure means the SDK has drifted — file a bug. ## What the rejection fixtures cover Half the value is in the rejection paths. The `reject_*` and `constraint_*_denied` fixtures exercise every branch of the verifier: - Tampered cert bodies (sig stops verifying) → `identity_status: invalid`, `error_reason: bad_*_sig: …` - Expired / not-yet-valid certs → `expired` - Out-of-scope request → `scope_denied` - Broken chain (subject ≠ next issuer) → `invalid` (chain_broken) - Chain depth exceeding `MaxDelegationChainDepth` → `invalid` (chain_too_deep) - Sub-delegation without `identity:delegate` → `delegation_not_authorized` - Cert granting a scope that is not canonical, not a wildcard, and not a `custom:` extension → `invalid_scope` (rejected before any effective-scope arithmetic) - Stale or future-dated challenge → `invalid` (`stale_challenge` / negative-age) - `SessionContext` mismatch (cross-verifier forwarding) → `invalid` - Constraint violations → `constraint_denied`, `constraint_unverifiable`, `constraint_unknown` - Sensitive scope introduced by wildcard → `scope_denied` Each rejection is checked for the **exact** `identity_status` value AND the leading `error_reason` prefix. The strictness is what makes integrators able to write deterministic error-handling code: switch on `identity_status` for the enum, then match the `error_reason:` prefix when you need the specific sub-cause. ## Hybrid signature corner cases Dedicated `reject_ed25519_only_corrupted.json` and `reject_mldsa65_only_corrupted.json` fixtures pin the defense-in-depth posture: ``` Ed25519 valid + ML-DSA-65 valid → ✓ authorized_agent Ed25519 valid + ML-DSA-65 corrupted → ✗ invalid (bad_*_sig) Ed25519 corrupted + ML-DSA-65 valid → ✗ invalid (bad_*_sig) ``` Both components must verify. There is no degradation path that accepts one signature and silently ignores the other. ## Regenerating the fixtures ```bash cd ratify-protocol go run ./cmd/ratify-testvectors -out /tmp/regen diff -rq testvectors/v1/ /tmp/regen/ # expected: identical ``` The CI workflow runs this exact diff. If a contributor's change causes the regenerated fixtures to differ from the committed ones, the change is non-deterministic (map iteration, RNG without a seed, `time.Now()` without a fixed clock) — and the PR fails. This is how the project guarantees that fixture bytes are stable across rebuilds, across runners, across machines. Anyone can reproduce the bytes from the deterministic seeds. ## Adding a new SDK To add a new language SDK (Swift, Java, etc.): 1. Open a [new-SDK coordination issue](https://github.com/identities-ai/ratify-protocol/issues/new?template=new_sdk.yml). 2. Implement the SDK following [SPEC.md](https://github.com/identities-ai/ratify-protocol/blob/main/SPEC.md). 3. Implement a conformance test that loads `testvectors/v1/*.json` and runs every fixture. 4. **Every fixture must pass before the SDK is merged.** No exceptions, no negotiated drift. See [`docs/SDKS.md`](https://github.com/identities-ai/ratify-protocol/blob/main/docs/SDKS.md) in the protocol repo for the full conformance contract. ## Where to next - [SDK packages](/sdk/packages/) — current versions and install commands - [Versioning](/reference/versioning/) — when fixture bytes can change - [Specification](/reference/spec/) — the normative protocol --- # Glossary > Every Ratify term, defined. Source: https://docs.identities.ai/reference/glossary/ A precise vocabulary matters for a protocol. This glossary defines every term used in the spec and SDKs. ## A **A2A** (agent-to-agent) — A pattern where one agent transacts with, hires, or sub-delegates to another agent without a human in the loop at the moment of the transaction. **Agent** — An autonomous software entity acting on a principal's behalf. Carries a hybrid keypair and a delegation chain. **AgentIdentity** — A named agent: agent ID, public key, agent type. Distinct from the keypair itself. **Anchor** — An out-of-protocol identity claim that a `HumanRoot` is tied to. Examples: an SSO identity, a domain-verified email, a government ID. Not normative; usable by upper layers. ## B **Bundle** — Short for `ProofBundle`. The signed object an agent presents to a verifier. ## C **Canonical JSON** — Ratify's deterministic JSON serialization: sorted keys, no insignificant whitespace, fixed number formatting. Specified in SPEC.md §6. **Cert** — Short for `DelegationCert`. **Challenge** — 32 random bytes generated by a verifier and signed (with a timestamp) by the presenting agent. Defeats replay. **Chain** — The ordered list of delegation certs in a `ProofBundle`. Chain depth 1 means one direct delegation (Alice → Agent-A); chain depth N means N sub-delegations. **Conformance suite** — The 79 canonical fixtures in `testvectors/v1/` that every SDK must pass byte-identically. **Constraint:** A condition that must be satisfied at verify time on top of scope. Eight built-in types in v1: `geo_circle`, `geo_polygon`, `geo_bbox`, `time_window`, `max_speed_mps`, `max_amount`, `max_rate`, and `resource_path`. Extension types use a registered `ConstraintEvaluator` (SPEC §17.7). **Custom scope** — An application-specific scope outside the canonical 54. Prefix: `custom:`. Treated as opaque string by the verifier (sensitivity-flag rules still apply). ## D **DEK** (data encryption key) — In Verify's custodial mode, the AES-256-GCM key that encrypts the principal's private key at rest. The DEK itself is encrypted by a KEK in Cloud KMS. **Delegate** — The verb: a principal signs a `DelegationCert` for a subject. **DelegationCert** — The signed object binding issuer → subject with scope, constraints, expiry, and a hybrid signature. **DCO** (Developer Certificate of Origin) — The lightweight contributor agreement Ratify uses instead of a CLA. Commits are signed off with `git commit -s`. ## E **Ed25519** — The classical elliptic-curve signature scheme. RFC 8032. One half of every hybrid Ratify signature. **Effective scope** — The intersection of every cert's scope in a delegation chain. The presenter can only do what every link in the chain authorized. ## F **FIPS 204** — NIST's standardization of the Dilithium post-quantum signature algorithm, finalized August 2024. The published name is ML-DSA. Ratify uses ML-DSA-65. **Freshness** — The challenge timestamp must be within `ChallengeWindowSeconds` (300 s) or the verifier rejects with `identity_status: invalid` and `error_reason: stale_challenge`. ## H **HumanRoot** — A principal that is a human, identified by a hybrid keypair plus an Anchor. **Hybrid signature** — A pair of signatures (Ed25519 + ML-DSA-65) over the same canonical bytes. Both must verify. This is Ratify's quantum-safe defense. ## I **IdentityStatus** — The verifier's verdict on a bundle, a closed enum. One of: `authorized_agent`, `verified_human`, `expired`, `revoked`, `scope_denied`, `constraint_denied`, `constraint_unverifiable`, `constraint_unknown`, `delegation_not_authorized`, `invalid_scope`, `invalid`. `invalid_scope` means a cert in the chain grants a scope that is not canonical, not a wildcard, and not a `custom:` extension — rejected before any effective-scope arithmetic (SPEC §10 step 7.a2). The `invalid` catch-all always carries a machine-parsable `error_reason` prefix (e.g. `stale_challenge:`, `bad_challenge_sig:`, `revocation_error:`). **Issuer** — The party that signed a `DelegationCert`. For the first link in a chain, this is a `HumanRoot` (or another agent for full A2A chains). ## K **KEK** (key encryption key) — In Verify's custodial mode, the Cloud KMS key that encrypts DEKs. **KeyRotationStatement** — A signed object proving identity continuity from an old keypair to a new one. Both old and new keys sign. Used for self-custody upgrades and routine rotation. ## M **ML-DSA-65** — Module-Lattice Digital Signature Algorithm, parameter set 65. The NIST-standardized form of CRYSTALS-Dilithium. FIPS 204. The other half of every hybrid Ratify signature. ## N **NO_EXPIRY_SENTINEL** — `4070908799` (2099-12-31 23:59:59 UTC). A `DelegationCert` whose `expires_at` equals this value means **"no expiry (until revoked)"**. Display and policy code must branch on the sentinel — never treat it as a literal 2099 expiry. Verification is unchanged (the sentinel is a future timestamp); revocation is the sole termination mechanism for such certs. Helpers: `IsNoExpiry()` (Go), `isNoExpiry()` (TypeScript), `is_no_expiry()` (Python, Rust), `ratify_expires_at_is_no_expiry()` (C). ## P **Present** — The verb: an agent assembles a `ProofBundle` and sends it to a verifier. **Principal** — The party at the root of a delegation chain. Usually a `HumanRoot`; can also be an `AgentIdentity` for fully autonomous chains. **ProofBundle** — What an agent presents. Contains the delegation chain plus a fresh challenge signature. ## R **Ratify Protocol™** — The open protocol for delegated-authority proofs. Spec under CC-BY-4.0, SDKs under Apache-2.0. Trademark of Identities AI, Inc. **Ratify Verify** — Identities AI's commercial managed-control-plane product on top of the open protocol. Closed-source. **RevocationList** — A signed list of cert IDs the issuer has revoked. Fields: `issuer_id`, `updated_at`, `revoked_certs`, `signature`. Consumed by verifiers via a `RevocationProvider` (SPEC §17.1), which may also accept `RevocationPush` deltas for sub- second propagation. ## S **Scope** — A string naming an allowed action. Canonical vocabulary has 54 scopes + 14 wildcards + the `custom:` extension pattern. **Sensitive scope** — A scope flagged as not eligible for wildcard expansion. Must be enumerated explicitly in a `DelegationCert.scope`. **Self-custody** — Key custody mode where the principal holds their own private key. No third party can sign on their behalf. **Subject** — The party named as the recipient in a `DelegationCert`. For sub-delegation, the subject of one cert is the issuer of the next cert in the chain. ## T **Trust root** — The first issuer in a delegation chain. A verifier must have the trust root's public key available (or look it up via the embedded `issuer_id`). ## V **Verify** — The verb: a third party runs the verifier algorithm on a `ProofBundle` and gets a yes/no answer. **Verifier** — The party running the verify algorithm. Examples: a Zoom meeting verifier (checking participants), an API gateway (checking incoming requests), a drone autopilot (checking commands). --- # Attribution & badge program > How to credit Ratify Protocol in your product, use the "Verified by Ratify Protocol" badge, and understand what the trademark policy covers. Source: https://docs.identities.ai/concepts/attribution/ Ratify Protocol is open source — Apache 2.0 for code, CC-BY-4.0 for the spec. You can use it commercially, build products on it, and modify it freely without paying anyone or asking permission. This page describes what we ask (not require) when you do. ## Why attribution matters for trust infrastructure The value of Ratify Protocol to your users isn't just the feature it enables — it's the verifiability behind the claim. When your product says "this agent is verified," a user's natural question is "verified how, by whom, and can I check?" Attributing to Ratify Protocol gives them an answer: a public spec, an open-source implementation, a conformance test suite across five SDKs, and a published threat model. That's something your marketing copy can't provide on its own. This is the same dynamic behind Signal's "Messages secured with the Signal Protocol" — not a legal requirement, but a trust amplifier. Apps that display it aren't subordinating their brand; they're grounding their security claim in something inspectable and independent. ## What we ask ### In developer documentation and security pages Include a reference to the protocol where you describe your agent authorization architecture: ```markdown Agent authorization is built on [Ratify Protocol](https://identities.ai/protocol), an open cryptographic identity protocol for AI agents with a published specification and cross-SDK conformance test suite. ``` Or the short form on a "powered by" or "open source" page: ```markdown Powered by [Ratify Protocol](https://identities.ai/protocol) ``` ### In product UIs — verification results When your product surfaces a verification result to an end user, use the "Verified by Ratify Protocol" badge. Download the SVG, host it yourself, and link it to `https://identities.ai/protocol`. | Variant | Download | Use when | |---|---|---| | Dark background | [badge-verified-dark.svg](https://github.com/identities-ai/ratify-protocol/raw/main/assets/badge-verified-dark.svg) | Dark or colored UI surfaces | | Light background | [badge-verified-light.svg](https://github.com/identities-ai/ratify-protocol/raw/main/assets/badge-verified-light.svg) | White or light UI surfaces | | Powered by (dark) | [badge-powered-dark.svg](https://github.com/identities-ai/ratify-protocol/raw/main/assets/badge-powered-dark.svg) | Footer, settings, "about" pages — dark | | Powered by (light) | [badge-powered-light.svg](https://github.com/identities-ai/ratify-protocol/raw/main/assets/badge-powered-light.svg) | Footer, settings, "about" pages — light | **Badge usage rules:** - Do not alter colors, proportions, or text. - Do not use it in a way that implies Identities AI endorses your product. - Do not use it as a primary branding element — it should live alongside your own brand. - Always link it to `https://identities.ai/protocol`. ## Trademark policy **Ratify Protocol™** and **identities.ai™** are trademarks of Identities AI, Inc. **You can:** - Say your product "uses Ratify Protocol", "is built on Ratify Protocol", or "implements the Ratify Protocol spec." - Use "Ratify Protocol" accurately as a noun to describe the open protocol. - Display the badges above with correct attribution. **You cannot:** - Use "Ratify Protocol" or any confusingly similar name as the name of your own protocol, product, or service. - Register a domain, trademark, or social handle that includes "Ratify" in a way that could be confused with Identities AI, Inc. - Imply that Identities AI, Inc. sponsors or endorses your product without a written agreement. ## Questions | Topic | Contact | |---|---| | Attribution, trademark, or commercial licensing | legal@identities.ai | | Badge misuse or trademark infringement | security@identities.ai | | General open-source questions | Open an issue on GitHub | --- # External platform references > How Ratify reference integrations are structured, tested, labeled, and contributed. Source: https://docs.identities.ai/guides/external-platform-references/ External platform references show how a real agent framework or security runtime can carry Ratify authority to an independently controlled receiver. They are executable interoperability profiles, not endorsements, partnerships, or substitutes for the platform's native identity, isolation, and policy layers. ```mermaid flowchart LR P[Principal] -->|signed, bounded delegation| A[Agent framework] A -->|operation + proof| G[Platform transport or runtime] G --> R[Independent receiver] R -->|verify before action| T[Protected tool] R -. deny .-> X[No side effect] ``` ## Why a separate authority layer is needed Workload identity proves which process connected. An agent framework selects and calls tools. A sandbox or gateway restricts what the runtime can reach. Receiver policy decides whether a request is acceptable now. Ratify supplies the evidence those layers do not: which principal delegated authority to the presenting agent, for which scope and resource, under which signed limits, and whether that authority is fresh and unrevoked. A receiver in another organization can verify the proof before accepting the consequence. ## Current profiles | Platform | Native role | Ratify seam | Evidence status | |---|---|---|---| | NVIDIA NOOA + OpenShell | Agent middleware plus runtime destination/method/tool isolation | Proof-carrying MCP request; receiver verifies semantic authority after OpenShell admission | Independent draft; hermetic and live OpenShell gates | | Google ADK | Agent loop and native MCP toolset | Proof injected after tool selection; independent Streamable HTTP MCP receiver verifies it | Independent draft; deterministic ADK and HTTP MCP gate | | LangChain | `create_agent` loop and MCP tool interceptors | Public interceptor adds a per-call proof; independent Streamable HTTP MCP receiver verifies it | Independent draft; deterministic LangGraph and HTTP MCP gate | Until a profile is merged, its pull request is the source of truth. Draft profiles must say explicitly that the named platform has not reviewed or approved them. ## Required repository structure Use one directory under `references//` and one authoritative script under `scripts/`: ```text references// ├── README.md # value, architecture, run command, claims, limits ├── DESIGN.md # threat boundary and integration decisions, if needed ├── requirements.txt # or the ecosystem's exact lock/pin file ├── / # presenter, carrier/adapter, receiver ├── tests/ # positive and adversarial cases └── evidence/ └── reference-evidence.md # dated environment and exact recorded result scripts/-reference-check.sh ``` Keep platform-specific code in the profile. Shared protocol behavior belongs in an SDK and its conformance suite, not in a copied helper hidden inside one integration. ## README contract Every reference README must include: 1. **Status and non-endorsement.** Name the exact platform and say whether its maintainers have reviewed the work. 2. **The missing question.** Explain why identity, orchestration, transport, sandboxing, and local policy do not themselves prove delegated authority. 3. **A visual boundary.** Show the principal, presenter, platform seam, independently operated receiver, and protected side effect. 4. **One authoritative command.** A clean reader must be able to reproduce the claimed result without a paid model or hidden credential. 5. **Exact versions and provenance.** Pin public dependencies and prove the gate did not accidentally import a local unpublished Ratify SDK. 6. **Acceptance matrix.** State both the expected decision and whether the protected side effect occurred. 7. **Layer separation.** Say what the platform provides, what Ratify provides, and what remains receiver policy. 8. **Non-claims and production gaps.** Cover key custody, trust distribution, durable revocation/challenges, TLS/workload identity, retries, audit storage, rate limits, carrier limits, and any fake protected service. 9. **Evidence link.** Point to the dated record rather than treating prose as proof. ## Minimum security gate A reference is not merge-ready unless its authoritative gate: - builds a clean or isolated environment from exact public pins; - asserts Ratify resolves from the intended published package; - requires an explicit expected test count; - fails on skips, xfails, collection errors, missing modules, or retries hidden as success; - exercises the platform's real public integration seam, with a deterministic model double when model judgment is irrelevant; - proves ALLOW reaches the protected handler and every DENY leaves the handler untouched; - covers expiry, revocation, replay, altered operation, wrong agent, untrusted root, scope/resource/constraint denial, malformed input, and transport denial; - tests concurrency and bounded receiver state when the profile maintains pending operations; and - records platform, language, architecture, dependency versions, warnings, and known unexecuted paths. For any proof carrier, also test missing, duplicated, malformed, and oversized values. Document intermediary limits and secret/log-redaction requirements. ## Review and upstreaming Merge into Ratify when the independent claims are reproducible and accurately bounded; external platform approval is not required for an explicitly labeled independent reference. Keep any proposed upstream adapter or platform-owned namespace in draft until the relevant maintainers agree to that interface. Ask platform maintainers to review the narrow seam—not to validate the Ratify protocol wholesale. Useful questions are: Is this the supported interception point? Is there a safer standard carrier? Which workload identity should be bound? What version-compatibility contract can the adapter rely on? --- # C / C++ SDK > Static and shared libraries for embedded, RTOS, and native C/C++ targets. cbindgen header, no_std + alloc compatible. Source: https://docs.identities.ai/sdk/c/ The C/C++ SDK wraps the Rust SDK via a C ABI and ships as `libratify_c.a` (static) and `libratify_c.so` / `libratify_c.dylib` (shared). The header (`include/ratify.h`) is auto-generated by cbindgen and includes `extern "C"` guards so it works in both C and C++. Byte-for-byte interoperable with Go, TypeScript, Python, and Rust. Apache-2.0. **Stability:** every primitive on this page is stable in `1.0.0-alpha.20`. ## When to use this SDK | You're writing | Use | |---|---| | A Go service or CLI | [Go SDK](/sdk/go/) | | A Node.js or browser app | [TypeScript SDK](/sdk/typescript/) | | A Python script or ML pipeline | [Python SDK](/sdk/python/) | | A Rust service or high-performance binary | [Rust SDK](/sdk/rust/) — use directly, no FFI overhead | | **C or C++ code** | **This SDK** | | **Firmware, RTOS, hardware driver** | **This SDK** (static library, `libratify_c.a`) | | **A language that FFIs to C** (Swift, Zig, Julia, Lua, etc.) | **This SDK** | ## Install ### Option 1 — Pre-built library (no Rust required) Download the pre-built library for your target from the [GitHub Releases page](https://github.com/identities-ai/ratify-protocol/releases). Each release ships `.tar.gz` archives for common targets: | Archive | Target | |---|---| | `ratify-c-*-x86_64-linux.tar.gz` | x86-64 Linux (server, desktop) | | `ratify-c-*-aarch64-linux.tar.gz` | ARM64 Linux (Raspberry Pi 4, embedded SBCs) | | `ratify-c-*-armv7-linux.tar.gz` | ARM32 Linux (Raspberry Pi 2/3) | | `ratify-c-*-x86_64-macos.tar.gz` | macOS Intel | | `ratify-c-*-aarch64-macos.tar.gz` | macOS Apple Silicon | | `ratify-c-*-x86_64-windows.zip` | Windows x86-64 | Each archive contains `lib/libratify_c.a`, `lib/libratify_c.so` (or `.dylib`/`.dll`), and `include/ratify.h`. The header is also committed to the repo at [`sdks/c/include/ratify.h`](https://github.com/identities-ai/ratify-protocol/blob/main/sdks/c/include/ratify.h) — you can vendor it directly without downloading a release. ### Option 2 — Build from source (requires Rust 1.70+) If your target isn't in the release archives, or you need a custom feature flag: ```bash git clone https://github.com/identities-ai/ratify-protocol cd ratify-protocol/sdks/c cargo build --release ``` Outputs: ``` target/release/libratify_c.a — static library target/release/libratify_c.so — shared library (Linux) target/release/libratify_c.dylib — shared library (macOS) include/ratify.h — C/C++ header ``` Cross-compile for embedded targets with [`cross`](https://github.com/cross-rs/cross): ```bash cargo install cross --git https://github.com/cross-rs/cross # ARM64 — Raspberry Pi 4, embedded Linux cross build --release --target aarch64-unknown-linux-gnu # ARM32 — Raspberry Pi 2/3 cross build --release --target armv7-unknown-linux-gnueabihf # ARM Cortex-M4/M7 — FreeRTOS / Zephyr rustup target add thumbv7em-none-eabihf cargo build --release --target thumbv7em-none-eabihf --features custom-entropy # RISC-V 64 cross build --release --target riscv64gc-unknown-linux-gnu ``` ## Quick start — Delegate → Present → Verify The complete agent authorization flow in C. All API entry/exit conditions are explicitly handled. ```c #include #include #include #include #include #include "ratify.h" static int fail(const char *step, char *err) { fprintf(stderr, "FAIL: %s — %s\n", step, err ? err : "unknown error"); ratify_error_free(err); return 1; } int main(void) { printf("Ratify %s — C SDK example\n\n", ratify_version()); char *err = NULL; /* 1. Generate identities */ RatifyHumanRoot *root = NULL; if (ratify_human_root_generate(&root) != RatifyOk) return fail("ratify_human_root_generate", NULL); RatifyAgent *agent = NULL; if (ratify_agent_generate("MyDroneBot", "drone", &agent) != RatifyOk) { ratify_human_root_free(root); return fail("ratify_agent_generate", NULL); } /* 2. Issue a DelegationCert */ int64_t now = (int64_t)time(NULL); RatifyDelegationCert *cert = NULL; if (ratify_delegation_issue(root, agent, "[\"physical:enter\"]", now, now + 3600LL, &cert, &err) != RatifyOk) return fail("ratify_delegation_issue", err); char *cert_json = ratify_delegation_cert_to_json(cert, &err); if (!cert_json) return fail("ratify_delegation_cert_to_json", err); /* 3. Generate challenge and build ProofBundle */ uint8_t challenge[32]; if (ratify_challenge_generate(challenge, 32) != RatifyOk) return fail("ratify_challenge_generate", NULL); RatifyProofBundle *bundle = NULL; if (ratify_proof_bundle_create(agent, cert_json, challenge, 32, now, &bundle, &err) != RatifyOk) return fail("ratify_proof_bundle_create", err); ratify_string_free(cert_json); char *bundle_json = ratify_proof_bundle_to_json(bundle, &err); if (!bundle_json) return fail("ratify_proof_bundle_to_json", err); /* 4. Verify */ RatifyVerifyResult *result = NULL; ratify_verify_bundle(bundle_json, "physical:enter", now, &result, &err); if (ratify_verify_result_is_valid(result)) { char *agent_id = ratify_verify_result_agent_id(result); printf("authorized agent: %s\n", agent_id); ratify_string_free(agent_id); } else { char *status = ratify_verify_result_identity_status(result); printf("rejected: %s\n", status); ratify_string_free(status); } /* 5. Cleanup */ ratify_verify_result_free(result); ratify_string_free(bundle_json); ratify_proof_bundle_free(bundle); ratify_delegation_cert_free(cert); ratify_agent_free(agent); ratify_human_root_free(root); return 0; } ``` Build and run: ```bash # macOS cc example.c -I include -L target/release \ -lratify_c -lpthread -framework Security -framework CoreFoundation \ -o example && ./example # Linux cc example.c -I include -L target/release \ -lratify_c -lpthread -ldl -lm -o example && ./example ``` ## Verifier-side: branching on identity status `ratify_verify_result_identity_status` returns a stable string constant you can compare directly. These values are byte-identical across every Ratify SDK. ```c char *status = ratify_verify_result_identity_status(result); if (strcmp(status, "authorized_agent") == 0) { /* ✓ All checks passed. Use ratify_verify_result_agent_id / ratify_verify_result_human_id to log the delegation chain. */ } else if (strcmp(status, "expired") == 0) { /* ✗ At least one cert in the chain is past its expires_at. */ } else if (strcmp(status, "revoked") == 0) { /* ✗ A cert ID matched a revoked entry from your revocation callback. */ } else if (strcmp(status, "scope_denied") == 0) { /* ✗ The required scope is not in the chain's effective scope. */ } else if (strcmp(status, "constraint_denied") == 0) { /* ✗ A geo / time / amount / rate constraint was violated. */ } else if (strcmp(status, "constraint_unverifiable") == 0) { /* ✗ A constraint present on a cert had no context to evaluate (e.g. geo_circle but has_location == 0). Fail-closed. */ } else if (strcmp(status, "constraint_unknown") == 0) { /* ✗ Unknown constraint type — future-proof fail-closed behavior. */ } else if (strcmp(status, "delegation_not_authorized") == 0) { /* ✗ A cert in the chain sub-delegated without identity:delegate. */ } else { /* "invalid" — bad signature, broken chain, stale challenge, etc. ratify_verify_result_error_reason() carries a stable error code. */ char *reason = ratify_verify_result_error_reason(result); fprintf(stderr, "invalid: %s\n", reason); ratify_string_free(reason); } ratify_string_free(status); ratify_verify_result_free(result); ``` ## JSON transport The C API works entirely in JSON strings. `ratify_proof_bundle_to_json` serializes a bundle; `ratify_verify_bundle` accepts the JSON string directly. This is the natural transport layer: ```c /* Agent side: serialize bundle to JSON, send over your transport. The JSON string is what you PUT in an HTTP header, publish to MQTT, write to a CAN-bus frame, or pass over any other channel. */ char *bundle_json = ratify_proof_bundle_to_json(bundle, &err); /* send bundle_json to the verifier ... */ /* Verifier side: receive the JSON string, verify inline — no deserialization step needed. */ RatifyVerifyResult *result = NULL; ratify_verify_bundle(bundle_json, "physical:enter", (int64_t)time(NULL), &result, &err); ``` Similarly, certs are serialized with `ratify_delegation_cert_to_json` for storage or forwarding, and the verifier accepts them as-is. ## Constraints (geo / time / amount / rate) Supply a `RatifyVerifierContext` when a delegation may carry constraints. Use `ratify_verify_bundle_opts` instead of the simple `ratify_verify_bundle`: ```c /* Gate: drone must be within a pre-authorized geo circle */ RatifyVerifierContext ctx = {0}; ctx.current_lat = 37.7749; ctx.current_lon = -122.4194; ctx.has_location = 1; /* MUST set this; lat/lon are ignored if 0 */ RatifyVerifyOptions opts = {0}; opts.required_scope = "drone:deliver"; opts.context = &ctx; RatifyVerifyResult *result = NULL; char *err = NULL; ratify_verify_bundle_opts(bundle_json, &opts, &result, &err); /* result is fail-closed on constraint violation: identity_status = "constraint_denied" if outside radius identity_status = "constraint_unverifiable" if has_location == 0 */ ``` Other context fields: ```c ctx.current_speed_mps = 12.5; ctx.has_speed = 1; /* max_speed_mps constraint */ ctx.current_amount = 99.0; ctx.has_amount = 1; /* max_amount constraint */ ctx.current_rate = 3.0; ctx.has_rate = 1; /* max_rate constraint */ ``` ## Sub-delegation Agent-to-agent delegation uses the same primitives. Alice grants Agent A the `identity:delegate` scope; A can then issue a delegation to Agent B. Without `identity:delegate`, any sub-delegation attempt is rejected with `delegation_not_authorized`. ```c /* ── Alice → Agent A ─────────────────────────────────────────────────── */ RatifyDelegationCert *cert_a = NULL; /* "[\"meeting:attend\",\"identity:delegate\"]" grants sub-delegation right */ ratify_delegation_issue(root, agent_a, "[\"meeting:attend\",\"identity:delegate\"]", now, now + 86400LL, &cert_a, &err); char *cert_a_json = ratify_delegation_cert_to_json(cert_a, &err); /* ── Agent A → Agent B (subset of A's grant) ─────────────────────────── */ /* Agent A re-wraps itself as a "root" for B's cert: use ratify_issue_sub_delegation. B's cert scope must be a subset; expiry must be ≤ A's cert expiry. */ RatifyDelegationCert *cert_b = NULL; ratify_sub_delegation_issue(agent_a, agent_b, "[\"meeting:attend\"]", /* no identity:delegate — B cannot sub-delegate further */ now, now + 3600LL, cert_a_json, &cert_b, &err); char *cert_b_json = ratify_delegation_cert_to_json(cert_b, &err); /* ── Build and verify the two-hop chain ──────────────────────────────── */ /* Pass both cert JSONs as a JSON array: "[cert_b_json, cert_a_json]" Order: leaf first (cert closest to agent), root last. */ char chain[8192]; snprintf(chain, sizeof(chain), "[%s,%s]", cert_b_json, cert_a_json); RatifyProofBundle *bundle = NULL; ratify_proof_bundle_create_chain(agent_b, chain, challenge, 32, now, &bundle, &err); char *bundle_json = ratify_proof_bundle_to_json(bundle, &err); RatifyVerifyResult *result = NULL; ratify_verify_bundle(bundle_json, "meeting:attend", now, &result, &err); /* result.human_id is Alice; result.agent_id is Agent B */ ``` ## Revocation Pass a revocation callback in `RatifyVerifyOptions`. The callback receives the `cert_id` string for each cert in the chain and must return `1` (revoked), `0` (not revoked), or `-1` (lookup failed — fail-closed, same as revoked). ```c typedef struct { const char **revoked_ids; size_t count; } RevocationDB; static int is_revoked(const char *cert_id, void *userdata) { RevocationDB *db = (RevocationDB *)userdata; for (size_t i = 0; i < db->count; i++) { if (strcmp(cert_id, db->revoked_ids[i]) == 0) return 1; /* revoked */ } return 0; /* not revoked */ /* return -1 on lookup error to fail-closed */ } /* Wire the callback */ RevocationDB my_db = { .revoked_ids = revoked, .count = n_revoked }; RatifyVerifyOptions opts = {0}; opts.required_scope = "physical:enter"; opts.revocation_fn = is_revoked; opts.revocation_userdata = &my_db; RatifyVerifyResult *result = NULL; char *err = NULL; ratify_verify_bundle_opts(bundle_json, &opts, &result, &err); /* identity_status == "revoked" if any cert_id is in the revoked list */ ``` ## Integration ```cmake cmake_minimum_required(VERSION 3.20) project(my_agent C) set(RATIFY_SDK_DIR "${CMAKE_SOURCE_DIR}/vendor/ratify-c") add_library(ratify STATIC IMPORTED) set_target_properties(ratify PROPERTIES IMPORTED_LOCATION "${RATIFY_SDK_DIR}/lib/libratify_c.a" INTERFACE_INCLUDE_DIRECTORIES "${RATIFY_SDK_DIR}/include" ) add_executable(my_agent main.c) target_link_libraries(my_agent PRIVATE ratify pthread dl m) ``` ```text project('my_agent', 'c') ratify_dep = declare_dependency( include_directories: include_directories('vendor/ratify-c/include'), link_args: [ '-L' + meson.source_root() / 'vendor/ratify-c/lib', '-lratify_c', '-lpthread', '-ldl', '-lm', ], ) executable('my_agent', 'main.c', dependencies: [ratify_dep]) ``` ```makefile RATIFY_SDK = vendor/ratify-c CFLAGS = -I$(RATIFY_SDK)/include LDFLAGS = -L$(RATIFY_SDK)/lib -lratify_c -lpthread -ldl -lm my_agent: main.c $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) ``` ```cpp #include "ratify.h" // extern "C" guards are built in — works as-is #include #include class RatifyVerifier { public: bool verify(const std::string& bundle_json, const std::string& scope) { RatifyVerifyResult* result = nullptr; char* err = nullptr; auto status = ratify_verify_bundle( bundle_json.c_str(), scope.empty() ? nullptr : scope.c_str(), 0, // 0 = use system clock &result, &err ); if (status != RatifyOk || !result) { std::string msg = err ? err : "unknown error"; ratify_error_free(err); throw std::runtime_error("verify failed: " + msg); } bool valid = ratify_verify_result_is_valid(result) != 0; ratify_verify_result_free(result); return valid; } }; ``` ## Embedded / RTOS — FreeRTOS custom entropy On standard OS targets (Linux, macOS, Raspberry Pi) entropy is automatic. On RTOS targets without `/dev/urandom`, enable the `custom-entropy` Cargo feature and register your hardware TRNG: **Cargo.toml:** ```toml ratify-c = { path = "…", features = ["custom-entropy"] } ``` **Application startup (STM32 example):** ```c #include "ratify.h" static int my_entropy(uint8_t *buf, size_t len) { for (size_t i = 0; i < len; i += 4) { uint32_t rnd; if (HAL_RNG_GenerateRandomNumber(&hrng, &rnd) != HAL_OK) return -1; /* -1 = entropy unavailable → library halts */ size_t copy = (len - i < 4) ? (len - i) : 4; memcpy(buf + i, &rnd, copy); } return 0; } int main(void) { /* Must be called before ratify_challenge_generate() or ratify_delegation_issue() */ ratify_set_entropy_source(my_entropy); /* … rest of your RTOS application */ } ``` The library halts if `ratify_set_entropy_source()` was never called when `custom-entropy` is enabled — generating certs with weak randomness is worse than not running at all. ### `no_std` + alloc The C SDK is compatible with RTOS environments that provide a heap (`alloc`) but not the full Rust `std`. Bare-metal Cortex-M targets with no heap at all should use the Rust SDK directly with `#[no_std] + alloc`. ## Supported targets | Architecture | Target triple | Example hardware | |---|---|---| | x86-64 | `x86_64-unknown-linux-gnu` | Intel/AMD server, Linux PC | | ARM64 | `aarch64-unknown-linux-gnu` | Raspberry Pi 4, embedded Linux, Apple Silicon | | ARM32 | `armv7-unknown-linux-gnueabihf` | Raspberry Pi 2/3, older embedded Linux | | ARM Cortex-M4/M7 | `thumbv7em-none-eabihf` | STM32, NXP — FreeRTOS, Zephyr | | x86-32 | `i686-unknown-linux-gnu` | Legacy industrial, 32-bit Linux | | RISC-V 64 | `riscv64gc-unknown-linux-gnu` | SiFive, emerging IoT | | macOS ARM64 | `aarch64-apple-darwin` | Apple Silicon Mac | | macOS x86-64 | `x86_64-apple-darwin` | Intel Mac | | Windows x86-64 | `x86_64-pc-windows-msvc` | Native Windows | A Raspberry Pi test script is available at [`sdks/c/scripts/test-raspberry-pi.sh`](https://github.com/identities-ai/ratify-protocol/blob/main/sdks/c/scripts/test-raspberry-pi.sh). ## Conformance The C SDK passes all 79 canonical fixtures — the same set used by Go, TypeScript, Python, and Rust. This includes all verify fixtures (proof-bundle verification, constraints, session/stream binding, revocation) and all non-verify fixture kinds (scope expansion, revocation lists, revocation push, key rotation, session tokens, transaction receipts, witness entries). ```bash # All 79 conformance fixtures cargo test --test conformance # Core ABI unit tests (null pointers, malformed JSON, round-trips, bad-argument detection) cargo test --test api # Advanced ABI tests (providers, receipt helpers, entropy hooks) cargo test --test advanced ``` Total: 156 tests (79 conformance + 77 ABI unit tests across `api` and `advanced`). ## Memory management Every function that returns a heap-allocated value documents which `_free` function to call. `NULL` is always safe to pass to `_free` functions. ```c ratify_human_root_free(root); ratify_agent_free(agent); ratify_delegation_cert_free(cert); ratify_proof_bundle_free(bundle); ratify_verify_result_free(result); ratify_string_free(any_string); /* for *_to_json, *_id, *_status, *_reason */ ratify_error_free(err); /* for err_out parameters */ ``` ## Where to next - [Physical AI guide](/guides/physical-ai/) — embedding the verifier on robots and controllers - [Protocol concepts: Delegate → Present → Verify](/concepts/delegate-present-verify/) — every primitive in depth - [Constraints](/concepts/constraints/) — geo, time, amount, and rate gate vocabulary - [SDK packages](/sdk/packages/) — version table and publishing process - [C SDK README](https://github.com/identities-ai/ratify-protocol/blob/main/sdks/c/README.md) — full API reference --- # Verify quickstart — allow, deny, revoke > Delegate authority to an agent, let your API enforce Ratify's decision, deny what was never delegated, revoke, and inspect the retained decision records. Source: https://docs.identities.ai/verify/quickstart/ By the end of this page your own sample API will have executed an agent's authorized action, refused an action that was never delegated, refused a proof the agent already held after you revoked it, and you will read all three decisions back from your audit history. One sentence to keep in mind throughout: **Ratify returns the decision; your application enforces it.** The verify call tells you `allow` or `deny` with a stable reason. Whether the protected code runs is your middleware's choice, and you are about to run that middleware. :::note[Alpha environment] Ratify Verify is in alpha. This quickstart targets the alpha environment: API at `https://api-dev.identities.ai`, console at `https://dev.identities.ai`. Personal workspaces start with **$50 of alpha credits**, enough for 25,000 verifications. You need `curl`, `jq`, and Node 18+. Everything runs in ONE terminal. ::: ```bash export RATIFY_API=https://api-dev.identities.ai export JAR=$(mktemp) # session cookie jar — holds credentials chmod 600 "$JAR" ``` ## 1. Sign up Authentication is email-code based and **cookie-only**; tokens never appear in response bodies. ```bash curl -fsS -c "$JAR" -X POST $RATIFY_API/v1/auth/email/send-code \ -H "Content-Type: application/json" \ -d '{"email": "you@example.com"}' # {"resent":false,"status":"code_sent"} ``` Grab the 6-digit code from your inbox, then: ```bash curl -fsS -b "$JAR" -c "$JAR" -X POST $RATIFY_API/v1/auth/email/verify-code \ -H "Content-Type: application/json" \ -d '{"email": "you@example.com", "code": "123456"}' ``` State-changing account calls need the CSRF double-submit header. Export it once from the jar, and re-export it after any call that refreshes the session: ```bash export CSRF=$(awk '/ratify_csrf/ {print $NF}' "$JAR") test -n "$CSRF" || { echo "FAILED: no CSRF cookie — repeat the verify-code step" >&2; false; } ``` Complete onboarding. `individual` gives you a personal workspace with alpha credits and no monthly minimum: ```bash curl -fsS -b "$JAR" -X POST $RATIFY_API/v1/auth/onboarding/profile \ -H "Content-Type: application/json" -H "X-Ratify-CSRF: $CSRF" \ -d '{"first_name": "Ada", "last_name": "Lovelace"}' curl -fsS -b "$JAR" -X POST $RATIFY_API/v1/auth/onboarding/account-type \ -H "Content-Type: application/json" -H "X-Ratify-CSRF: $CSRF" \ -d '{"account_type": "individual"}' # {"onboarding_step":"complete"} ``` Behind that second call, Ratify created your cryptographic root identity (a hybrid Ed25519 + ML-DSA-65 keypair) and protected its private key for the managed alpha flow. :::tip[Session lifetime] Access sessions last 15 minutes, and step 6 refreshes yours before the revocation. If any account-side call ever returns `auth_required`, run the refresh block from step 6 and re-export `CSRF`. ::: ## 2. Register a platform and pick your scopes A *platform* is the thing you are building. Registering one issues the API key your backend will use. `default_allowed_scopes` is the ceiling on what any agent on this platform may ever be delegated. **Scopes come from a fixed vocabulary.** Ratify validates every scope against the canonical vocabulary (scopes like `data:read` and `payments:send`), or your own namespace under the `custom:` prefix, such as `custom:ticket:read`. Anything else is rejected with `invalid_scopes`. This quickstart models a ticket desk, so it uses two custom scopes. See [Concepts → Scopes](/concepts/scopes/) for the full vocabulary, the wildcard rules, and which scopes are marked sensitive. ```bash RESP=$(curl -fsS -b "$JAR" -X POST $RATIFY_API/v1/ratify/platforms/personal \ -H "Content-Type: application/json" -H "X-Ratify-CSRF: $CSRF" \ -d '{ "name": "Ticket Desk", "surface_type": "agentic_api", "default_allowed_scopes": ["custom:ticket:read", "custom:ticket:refund"] }') echo "$RESP" | jq . ``` ```json { "api_key": "rat_live_...", "api_key_prefix": "rat_live_...", "connection_id": "ca750a39-993f-4d23-abac-c67e6be86162", "description": "", "expires_at": "2027-07-21T07:38:04.398366-07:00", "name": "Ticket Desk", "platform_id": "fd06b9d8-f976-4163-abed-6c08060db073", "publish_state": "draft", "signing_secret": "rat_psec_...", "surface_type": "agentic_api", "webhook_signing_secret": "rat_whsec_..." } ``` The `api_key` and both secrets are shown **exactly once**. This quickstart uses only `api_key`; in a real integration, store the `signing_secret` and `webhook_signing_secret` now, because they cannot be shown again. Capture what the rest of the quickstart needs. These captures use `jq -e`, so a missing field makes the assignment itself fail with a nonzero status; if you see a `FAILED` line, stop and inspect `$RESP` before continuing: ```bash RATIFY_API_KEY=$(echo "$RESP" | jq -er '.api_key | select(type == "string" and length > 0)') \ || { echo "FAILED: no api_key in \$RESP — stop here" >&2; false; } CONNECTION_ID=$(echo "$RESP" | jq -er '.connection_id | select(type == "string" and length > 0)') \ || { echo "FAILED: no connection_id in \$RESP — stop here" >&2; false; } export RATIFY_API_KEY CONNECTION_ID ``` ## 3. Create an agent Custodial mode: Ratify generates and holds the agent's hybrid keypair, so you can run this whole page from curl. (Holding your own keys? Use the [SDKs](/sdk/go/) and build proof bundles yourself.) ```bash AGENT_ID=$(curl -fsS -X POST \ $RATIFY_API/v1/ratify/connections/$CONNECTION_ID/agents \ -H "X-Ratify-API-Key: $RATIFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Support Agent", "agent_type": "api_agent"}' \ | jq -er '.agent_id | select(type == "string" and length > 0)') \ || { echo "FAILED: agent creation — stop here" >&2; false; } export AGENT_ID echo $AGENT_ID ``` The response is flat JSON with `agent_id`, `name`, `agent_type`, `custody_mode`, and `public_key_json`. ## 4. Delegate less than was asked The delegation request asks for the connection's full scope ceiling (both ticket scopes). This is where the human decides, and the human can grant less. Approve **only the read scope**: ```bash REQUEST_ID=$(curl -fsS -b "$JAR" -X POST \ $RATIFY_API/v1/ratify/delegations/self-request \ -H "Content-Type: application/json" -H "X-Ratify-CSRF: $CSRF" \ -d "{\"connection_id\": \"$CONNECTION_ID\"}" \ | jq -er '.request_id | select(type == "string" and length > 0)') \ || { echo "FAILED: delegation request — stop here" >&2; false; } export REQUEST_ID APPROVE=$(curl -fsS -b "$JAR" -X POST \ $RATIFY_API/v1/ratify/delegations/requests/$REQUEST_ID/approve \ -H "Content-Type: application/json" -H "X-Ratify-CSRF: $CSRF" \ -d '{"approved_scope": ["custom:ticket:read"]}') echo "$APPROVE" | jq . CERT_ID=$(echo "$APPROVE" | jq -er '.cert_id | select(type == "string" and length > 0)') \ || { echo "FAILED: approval — stop here and inspect \$APPROVE" >&2; false; } export CERT_ID ``` ```json { "approved_scope": ["custom:ticket:read"], "cert_id": "cert-aea5c8d8-6108-430d-aa2d-4e18fd557195", "expires_at": null, "issued_at": "2026-07-21T14:45:12.000000Z", "status": "approved" } ``` A signed delegation certificate now exists saying *you* authorized *this agent* for `custom:ticket:read`, and nothing else. Keep `CERT_ID`; it is your revocation handle in step 6. ## 5. Enforce decisions in your own API The sample service is a complete, runnable ticket API whose middleware calls Ratify before every protected action. Download it and start it in the background of this same terminal (so it inherits your API key), with a cleanup function armed for when you close the shell: ```bash curl -fsSO https://docs.identities.ai/examples/ticket-desk.mjs node ticket-desk.mjs > ticket-desk.log 2>&1 & export TICKET_DESK_PID=$! cleanup() { if test -n "${TICKET_DESK_PID:-}"; then kill "$TICKET_DESK_PID" 2>/dev/null || true fi rm -f "${JAR:-}" held-proof.json ticket-desk.mjs ticket-desk.log } trap cleanup EXIT sleep 1 && cat ticket-desk.log # ticket desk on 127.0.0.1:8787 ``` The part worth reading is `requireScope`, the enforcement boundary. The mapping is exhaustive, and only an explicit `allow` reaches the handler. Operational failures are never dressed up as authority decisions: a Ratify 400 means the caller demonstrated no authority (403), but a bad service key, exhausted credits, rate limiting, a verifier 5xx, a timeout, an unparseable response, or an unrecognized decision all mean no decision exists, so the sample returns 503 and refuses to act. The code below is the downloadable asset itself, rendered from the same file you just fetched, so it cannot drift: Now define one helper. It fetches a fresh challenge (each is single-use), has Ratify sign it with the agent's custodial key, and emits the proof bundle your agent would attach to its requests: ```bash proof() { local issued challenge presented issued=$(curl -fsS -X POST $RATIFY_API/v1/ratify/challenge \ -H "X-Ratify-API-Key: $RATIFY_API_KEY") \ || { echo "FAILED: challenge request" >&2; return 1; } challenge=$(echo "$issued" \ | jq -er '.challenge | select(type == "string" and length > 0)') \ || { echo "FAILED: no challenge in response" >&2; return 1; } presented=$(curl -fsS -X POST \ $RATIFY_API/v1/ratify/connections/$CONNECTION_ID/agents/$AGENT_ID/present \ -H "X-Ratify-API-Key: $RATIFY_API_KEY" -H "Content-Type: application/json" \ -d "{\"challenge\": \"$challenge\"}") \ || { echo "FAILED: present" >&2; return 1; } echo "$presented" | jq -e '{proof_bundle: .proof_bundle} | select(.proof_bundle != null)' \ || { echo "FAILED: no proof bundle in response" >&2; return 1; } } ``` Each stage is captured separately (no pipelines between curl and jq), so a failure at any stage makes `proof` itself return nonzero instead of emitting an empty bundle. **The authorized read executes** ($0.002 metered per verification): ```bash PROOF=$(proof) || { echo "FAILED: could not build proof — stop here" >&2; false; } curl -sS -X POST http://127.0.0.1:8787/tickets/read \ -H "Content-Type: application/json" -d "$PROOF" # {"tickets":[{"id":1,"subject":"Printer on fire","status":"open"}]} ``` **The refund is refused, and provably never ran.** Same agent, same valid certificate, but `custom:ticket:refund` was never delegated. The denial calls print their HTTP status so you can see the 403 for yourself (no `-f` here: the failing status is the demonstration): ```bash curl -sS http://127.0.0.1:8787/ledger # {"refundsProcessed":0} PROOF=$(proof) || { echo "FAILED: could not build proof — stop here" >&2; false; } curl -sS -w '\n[HTTP %{http_code}]\n' -X POST http://127.0.0.1:8787/tickets/refund \ -H "Content-Type: application/json" -d "$PROOF" # {"error":"not_authorized","detail":"not_delegated"} # [HTTP 403] curl -sS http://127.0.0.1:8787/ledger # {"refundsProcessed":0} <- the refund handler never executed ``` ## 6. Revoke, and defeat a proof already in the agent's hands The strongest revocation story is not "the agent can no longer get proofs". It is "the proof the agent is already holding stops working". First refresh your session, so nothing interrupts the sequence that follows (proofs are freshness-limited; do the session maintenance before minting one, not after): ```bash curl -fsS -b "$JAR" -c "$JAR" -X POST $RATIFY_API/v1/auth/refresh \ -H "X-Ratify-CSRF: $CSRF" > /dev/null export CSRF=$(awk '/ratify_csrf/ {print $NF}' "$JAR") ``` Now: the agent builds and holds a proof, you revoke immediately, and the held proof is submitted after the fact. ```bash PROOF=$(proof) || { echo "FAILED: could not build proof — stop here" >&2; false; } printf '%s' "$PROOF" > held-proof.json curl -fsS -b "$JAR" -X POST $RATIFY_API/v1/ratify/delegations/$CERT_ID/revoke \ -H "Content-Type: application/json" -H "X-Ratify-CSRF: $CSRF" \ -d '{"reason": "quickstart revocation demo"}' | jq . # {"cert_id": "cert-...", "status": "revoked"} curl -sS -w '\n[HTTP %{http_code}]\n' -X POST http://127.0.0.1:8787/tickets/read \ -H "Content-Type: application/json" -d @held-proof.json # {"error":"not_authorized","detail":"delegation_revoked"} # [HTTP 403] ``` And Ratify refuses to mint new proofs for the revoked delegation at all. This is an intentional failure, so call `/present` directly (without `-f`) to see the stable error code and status for yourself: ```bash C=$(curl -fsS -X POST $RATIFY_API/v1/ratify/challenge \ -H "X-Ratify-API-Key: $RATIFY_API_KEY" | jq -er .challenge) curl -sS -w '\n[HTTP %{http_code}]\n' -X POST \ $RATIFY_API/v1/ratify/connections/$CONNECTION_ID/agents/$AGENT_ID/present \ -H "X-Ratify-API-Key: $RATIFY_API_KEY" -H "Content-Type: application/json" \ -d "{\"challenge\": \"$C\"}" # {"error":{"code":"no_active_delegation","message":"No active delegation cert for this agent — issue one first (delegate-to-self or consent flow)","status":404,...}} # [HTTP 404] ``` Two distinct properties, both enforced: proofs already in circulation are rejected at verification time, and new proofs can no longer be created. ## 7. Read the decisions back ```bash curl -fsS -b "$JAR" "$RATIFY_API/v1/ratify/audit/personal?limit=25" \ | jq '[.events[] | select(.event_type == "proof.verified" or .event_type == "proof.rejected") | {event_type, decision: .details.decision, reason: .details.decision_reason}]' ``` The API returns events newest first, so the list reads bottom-up as the story you just lived: ```json [ { "event_type": "proof.rejected", "decision": "deny", "reason": "delegation_revoked" }, { "event_type": "proof.rejected", "decision": "deny", "reason": "not_delegated" }, { "event_type": "proof.verified", "decision": "allow", "reason": "authorized" } ] ``` Your history also contains `proof.presented` audit events. Those carry no decision fields, and that is correct: presenting builds a proof, it does not authorize anything. Only `POST /v1/ratify/verify` produces decisions. :::note[What this history is, and is not] Personal audit history is **append-only** (database-enforced), and it is **not currently hash-chained**; hash chaining applies to organization workspaces in the current alpha, and decision details are not yet covered by that chain. Retention and integrity guarantees are documented precisely in [Protocol vs. Verify](/start/protocol-vs-verify/). ::: ## Clean up The `cleanup` function from step 5 stops the sample service and removes the cookie jar, the held proof, the downloaded sample, and its log. Call it, then disarm the trap so a long-lived shell can never signal a reused PID later: ```bash cleanup trap - EXIT unset TICKET_DESK_PID RATIFY_API_KEY CONNECTION_ID AGENT_ID REQUEST_ID CERT_ID CSRF RESP APPROVE PROOF ``` ## If something fails - **`invalid_scopes` at registration** — the scope is not canonical and not `custom:`-prefixed. See [Concepts → Scopes](/concepts/scopes/). - **`auth_required` on an account call** — your 15-minute session expired. Run the refresh block in step 6 and re-export `CSRF`. - **`invalid_challenge` on verify** — challenges are single-use. Fetch a fresh one (the `proof` helper always does). - **`rate_limited` (429) on send-code** — wait a minute; the code-send endpoint is rate limited per IP. - **HTTP 5xx from verify** — if Verify cannot return a decision, the application must refuse the action and retry with a fresh challenge (the sample middleware returns 503 in this case). During alpha, recording is not exactly-once; a rare ambiguous failure may leave an earlier recorded/billed event, and such cases are reconciled operationally. ## Where to go next - **Verify proofs your customers' agents present to you**: the same `POST /v1/ratify/verify` call from your middleware, whatever the source of the bundle. - **Hold your own keys**: [SDK quickstart](/start/quickstart/) builds and verifies bundles offline, no platform account needed. - **Understand what you just did**: [Delegate → Present → Verify](/concepts/delegate-present-verify/). - **Console**: [dev.identities.ai](https://dev.identities.ai) shows the dashboard view of everything you just created. ---