Skip to content

Go SDK

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 every other SDK (TypeScript, Python, Rust) is validated against.

Stability: every primitive on this page is stable in v1.0.0-alpha.7. There are no preview / experimental APIs in the snippets below.

Terminal window
go get github.com/identities-ai/[email protected]

Module: github.com/identities-ai/ratify-protocol. Go 1.22+.

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.

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
┌─────────────┐ ┌─────────────┐
│ 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

Section titled “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*:

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 — 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)

Section titled “Constraints (geo / time / amount / rate / speed)”

If a delegation has constraints, supply the runtime context in VerifyOptions.Context:

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.

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.

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

Beyond the deterministic verifier core, VerifyOptions exposes pluggable hooks for revocation, policy, audit, anchor resolution, and extension constraint evaluators. They are all stable in alpha.7; full reference at Provider architecture.

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

Terminal window
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 59 canonical wire-format fixtures plus the 10 alpha.7 cross-SDK byte-equivalence vectors passed. Your local build is byte-for-byte interoperable with every other Ratify SDK at the same version.

The repo ships a ratify CLI for human use — create identities, sign delegations, verify bundles, list the canonical scopes.

Terminal window
go build -o /tmp/ratify ./cmd/ratify
/tmp/ratify --help

Common workflows:

Terminal window
# 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 53-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.