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:
- Revocation freshness. A CRL file polled hourly is fine for a low-throughput verifier; a real-time payment endpoint needs sub-second push propagation.
- 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.
- 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.
The diagram
Section titled “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 / 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 chainA 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.”
| 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 | Commercial | Real-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 returnsidentity_status="scope_denied".(_, error)→ fail-closed; verification returnsidentity_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.
Lever 1 — VerificationReceipt (§17.5)
Section titled “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
AuditProviderchooses 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’sprev_hashis 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-256IssueVerificationReceipt(bundle, result, verifierID, verifierPub, verifierPriv, prevHash, ts) -> *VerificationReceiptVerifyVerificationReceipt(receipt) -> nil | errorReceiptHash(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)
Section titled “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. 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:
- Built-in evaluators handle universal types (always, by the SDK directly).
- For unknown types, the per-Verify registry is consulted.
- 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.
Lever 4 — AnchorResolver (§17.8)
Section titled “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
VerificationReceiptproves “this bundle was verified at this time.” - An anchor-bound
VerificationReceiptproves “this bundle was verified at this time, AND the human root behind it was bound to an SSO-asserted identity at Okta as ofAnchor.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
HumanRootandAgentIdentitykeypairs (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 andRevocationPushes - ✅ 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
RevocationProviderinterface - ✅ Local policy enforcement via the
PolicyProviderinterface - ✅ Local audit logging via the
AuditProviderinterface - ✅ Signed
VerificationReceiptchains (the protocol provides the primitive; you build the archive) - ✅ HMAC
PolicyVerdictcaching (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.
Compliance checklist
Section titled “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; TransactionReceipts | Per-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.
Deprecation notice
Section titled “Deprecation notice”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.
Pointers
Section titled “Pointers”- Normative spec: SPEC §17
- Changelog: v1.0.0-alpha.7 release notes
- Pricing for the managed surface: Ratify Verify pricing
- Architecture deep-dive: docs/EXPLAINED.md