Skip to content

Provider architecture & the build-vs-buy boundary

The Ratify Protocol is open: wire format, hybrid Ed25519 + ML-DSA-65 signatures, the verifier state machine, the canonical 53-scope vocabulary, the seven first-class constraint types. Every conformant SDK implements those byte-for-byte — the 59 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.7 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 high-performance, stateful, compliance-grade counterpart.

[ 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 / push-sync edge cache
[ hook ] PolicyProvider ↔ none / Rego/OPA + quota
[ hook ] AuditProvider ↔ stdout / signed immutable archive
[ 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 four SDKs. Where verifiers differ is in operational surface — latency, compliance posture, integration ergonomics — not in cryptography.

The three core providers (§17.1–§17.3)

Section titled “The three core providers (§17.1–§17.3)”

RevocationProvider — is_revoked(cert_id) -> (bool, error)

Section titled “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.”

ImplementationWhere it livesOperational properties
Local file CRLOpen-source SDKPolled hourly; staleness bounded by poll interval.
In-memory bloom filterOpen-source SDKO(1) at call time; fits 1M revoked IDs in ~12 MB.
Ratify Verify push-syncCommercialReal-time stream; staleness < 100 ms globally.

The interface lives in the open-source SDK. The high-performance implementation is what customers pay for.

PolicyProvider — evaluate_policy(bundle, context) -> (bool, error)

Section titled “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)

Section titled “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)

Section titled “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.

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 VerificationReceipts 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):

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.

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. Cuts policy-server round-trips by ~95% 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)

Section titled “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.

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

The moat: Ratify Verify ships a registry of verify.* types its customers use; an OSS verifier can recognize the same types only if its operator registers each one explicitly. An issuer who uses a Verify-managed type implicitly requires every downstream verifier to be Verify-aware. Spec-conformant — and a competitive lever, because typing the registry yourself is a continuous engineering investment.

Naming convention:

  • verify.<type> — types defined by Ratify Verify.
  • <vendor>.<type> — types defined by a deployment or third party.

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)

Section titled “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 DelegationCerts with first-class constraints
  • ✅ Build, present, and verify ProofBundles with session and stream binding
  • ✅ Issue and verify RevocationLists and RevocationPushes
  • ✅ Issue and verify KeyRotationStatements
  • ✅ Issue and verify WitnessEntrys (the protocol’s append-only log primitive)
  • ✅ Issue and verify TransactionReceipts 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

Enforcement is in the protocol. A PolicyProvider returning false rejects the bundle — that is enforcement. What Ratify Verify sells is not the ability to enforce; it is the management surface that makes enforcement operable at scale: no-code policy authoring, real-time distribution, per-tenant overrides, immutable archive, SSO-bound attestation, regulated air-gap deployments.

A pragmatic mapping of common compliance regimes to what OSS provides vs. what Ratify Verify supplies on top:

RegimeOSS providesVerify supplies
SOC 2 Type 2VerificationReceipt primitive; AuditProvider interfaceHash-chained ledger, retention SLAs, exportable evidence, SAML/SSO
ISO 27001Hybrid PQC signatures; revocation; key rotationDocumented key custody, signed compliance attestations
HIPAAAll cryptographic primitivesBAA, US-only data residency, signed audit archive
GDPR / data residencyAnchor primitive; opaque references (no PII on the wire)Regional deployments, on-prem / VPC option
Air-gapped / FedRAMP-styleFull SDK works offlineRatify Air-Gap: on-prem control plane, no phone-home
PCI DSSmax_amount constraint; TransactionReceiptsPer-transaction audit chain, attested key custody

Surface adapters (out of scope for this repository)

Section titled “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.

VerifyOptions.IsRevoked (the legacy func(certID) bool closure) is deprecated in v1.0.0-alpha.7 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.