Delegation Receipt Standard

Research Project

DRS is a per-step delegation receipt standard built on top of OAuth 2.1 + RFC 8693 + MCP.

Every time an AI agent acts on your behalf, DRS produces a cryptographically signed receipt that proves — to anyone, without contacting a central authority — exactly who authorised what, under which constraints, at what time.

The problem DRS solves

Modern AI agents act through delegation chains. A human authorises an agent, which authorises a sub-agent, which calls a tool. OAuth 2.1 handles the first hop. RFC 8693 defines token exchange. But neither standard requires a receipt at every step — which means any link in the chain can be fabricated after the fact, and no tool server can independently verify the full provenance of a request.

This is the chain splicing vulnerability (CVE-2025-55241, demonstrated in Azure AD). The IETF OAuth Working Group named per-step signed receipts as mitigation #3. DRS is that mitigation, implemented as an open standard on top of the existing OAuth + MCP stack.

What DRS is not

DRS isDRS is not
A receipt standard for delegation chainsA replacement for OAuth 2.1
Built on JWTs, EdDSA, OAuth 2.1, MCPA UCAN implementation
Independently verifiable audit evidenceAn observability tool (Langfuse/Arize do that)
An open standard, not a platformA blockchain product
The authorisation provenance layerA replacement for OpenTelemetry

Who this is for

  • Developers — building MCP servers or agent runtimes who need DRS integration
  • Operators — deploying the verification server and configuring enterprise policies
  • Auditors — reconstructing delegation chains for compliance evidence
  • Contributors — who want to understand the architecture and extend the codebase

Repository structure

ComponentLanguageRole
drs-coreRustCrypto primitives, JCS canonicalisation, chain verification, WASM build
drs-verifyGoVerification HTTP server, MCP/A2A middleware, DID resolver, status list cache
drs-sdkTypeScriptDeveloper SDK (issuance path), CLI tools, browser WASM wrapper

This is a research project. The architecture, data model, and algorithms are documented throughout this site. The implementation is the reference implementation of the DRS 4.0 specification.

Start with What is DRS? for a conceptual overview, or jump straight to the Quick Start.

Quick Start

Get from zero to a verified delegation bundle in under 10 minutes.

Prerequisites

  • Node.js 20+ and pnpm
  • Go 1.22+ (to run drs-verify locally)

1. Install the SDK

pnpm add @drs/sdk

2. Generate a keypair

pnpm exec drs keygen

Output:

Private key (keep secret): <base64url-encoded 32 bytes>
DID:                        did:key:z6Mk...

Save both values. The private key signs delegation receipts. The DID is your identity on the chain.

3. Issue a root delegation

import { issueRootDelegation } from '@drs/sdk';

const privateKey = Uint8Array.from(Buffer.from('YOUR_PRIVATE_KEY', 'base64url'));
const now = Math.floor(Date.now() / 1000);

const rootDR = await issueRootDelegation({
  signingKey:  privateKey,
  issuerDid:   'did:key:z6MkYOUR_DID',
  subjectDid:  'did:key:z6MkYOUR_DID',   // human is both issuer and subject at root
  audienceDid: 'did:key:z6MkAGENT_DID',
  cmd: '/mcp/tools/call',
  policy: {
    allowed_tools: ['web_search'],
    max_cost_usd: 10.00,
    pii_access: false,
  },
  nbf: now,
  exp: now + 3600,          // 1 hour
  rootType: 'automated-system',
});

console.log('Root DR JWT:', rootDR);

4. Start drs-verify locally

cd drs-verify
go run ./cmd/server
# drs-verify listening on :8080

5. Build and verify a bundle

import { buildBundle, serialiseBundle, issueInvocation, computeChainHash } from '@drs/sdk';

const invocation = await issueInvocation({
  signingKey:  agentPrivateKey,
  issuerDid:   'did:key:z6MkAGENT_DID',
  subjectDid:  'did:key:z6MkYOUR_DID',
  cmd: '/mcp/tools/call',
  args: { tool: 'web_search', query: 'hello', estimated_cost_usd: 0.01 },
  drChain: [computeChainHash(rootDR)],
  toolServer: 'did:key:z6MkTOOL_DID',
});

const bundle = buildBundle({ invocation, receipts: [rootDR] });
import { writeFileSync } from 'fs';
writeFileSync('bundle.json', serialiseBundle(bundle));
DRS_VERIFY_URL=http://localhost:8080 pnpm exec drs verify bundle.json

Expected output:

✓ Bundle verified
  Chain depth:    1
  Root principal: did:key:z6Mk...
  Subject:        did:key:z6Mk...
  Command:        /mcp/tools/call
  Policy result:  pass
  Blocks:         A✓ B✓ C✓ D✓ E✓ F✓

Next steps

What is DRS?

DRS (Delegation Receipt Standard) is a per-step delegation receipt standard built on OAuth 2.1 + RFC 8693 + MCP.

It produces a cryptographically signed receipt at every step of an agent delegation chain so that any party — the tool server, a regulator, an auditor — can independently verify the complete provenance of any agent action without contacting a central authority.

The one-sentence definition

DRS adds a tamper-evident, independently verifiable receipt to every hop of an OAuth delegation chain.

What DRS adds to existing standards

OAuth 2.1         → handles the first delegation hop (user → agent)
RFC 8693          → defines token exchange between agents
RFC 8693 + DRS    → adds a signed receipt at EVERY hop

Without DRS, an audit trail exists only in server logs controlled by the operator. With DRS, the audit trail is in the receipts themselves — signed by the delegating party, verifiable by anyone with the public key.

The chain splicing problem

RFC 8693 allows Agent A to exchange its token for a new token representing Agent B acting on behalf of the original user. The problem: nothing prevents an attacker from splicing an unrelated token into the chain — presenting credentials from scope A while actually invoking scope B.

CVE-2025-55241 (Azure AD, March 2025) demonstrated this in production. The IETF OAuth WG's suggested mitigation #3 is per-step signed receipts. DRS is that mitigation.

How DRS works

  1. Delegation Receipt (DR): A signed JWT issued by each delegator. Contains the command, policy constraints, temporal bounds, and a SHA-256 hash of the previous DR.
  2. Chain: The linked sequence of DRs from the human root to the invoking agent. Each prev_dr_hash field links back, creating a tamper-evident chain.
  3. Invocation Receipt: A signed JWT recording the actual tool call arguments, the full chain of DR hashes, and the tool server's DID.
  4. Bundle: The invocation receipt plus all DRs, transmitted as a base64url-encoded JSON object in the X-DRS-Bundle HTTP header.

What DRS is not

DRS is frequently confused with systems it is adjacent to but distinct from:

SystemWhat it doesHow it differs from DRS
OAuth 2.1Delegates accessDRS extends it with per-step receipts
UCANCapability tokens (CBOR/IPLD)DRS uses JWTs and OAuth — different ecosystem
OpenTelemetryDistributed tracingObservability vs. authorisation provenance
Langfuse / ArizeLLM observabilityLogs vs. cryptographic proofs
Agentic JWTJWT profile for agent identityIdentity vs. delegation chain receipts
Blockchain audit logsImmutable event logDRS receipts work without blockchain (on-chain is optional tier 4)

For a detailed comparison, see DRS vs Alternatives.

Why DRS Exists

DRS exists because the AI agent ecosystem is deploying faster than the accountability infrastructure to support it.

The market reality

  • 75% of C-suite executives rank auditability as their top AI governance requirement
  • 82% of executives are confident in their AI oversight, but only 14.4% send agents to production with full approval chains
  • Only 5.2% of enterprises have AI agents in production today — the accountability gap is the primary blocker

The question every CISO asks before approving an agent deployment: "If this agent does something it shouldn't, can we prove exactly who authorised it, and what they authorised?"

OAuth 2.1 + server logs cannot answer that question. DRS can.

The RFC 8693 gap

RFC 8693 (Token Exchange) defines how Agent A exchanges its bearer token for a new token representing Agent B acting on behalf of the user. This is the correct building block for agentic delegation.

The gap: RFC 8693 tokens are bearer tokens. Any agent that obtains a valid token can present it as if it were the legitimate holder. There is no per-step binding between the token and the specific delegation act that produced it.

Chain splicing: An attacker with access to one token can splice it into a different chain, presenting apparently legitimate credentials while exceeding the scope they were actually granted. CVE-2025-55241 (Azure AD, March 2025) is a real-world exploitation.

DRS closes this gap: the prev_dr_hash field in each receipt links it cryptographically to the previous one. Any substitution breaks the chain and fails Block B of verification.

Version history: what was tried

DRS reached v4 through three prior architectures that were each scrapped. Understanding what failed is essential for contributors — see False Positives: What We Tried for the full history.

v1 — Invented from scratch

Three fundamental errors:

  1. UCAN already defines delegation chains — v1 reinvented the wheel badly
  2. Applied a binary Merkle tree to a linear chain (CVE-2012-2459 risk)
  3. Under-specified security model

v1 was scrapped. The document is preserved in docs/DRS_architecture_v1.md.

v2 — UCAN profile (against wrong version)

Correctly identified DRS should be a UCAN Profile — but built against UCAN 0.x (JWT) while the actual spec was UCAN v1.0-rc.1 (CBOR/IPLD). Additional problems: TypeScript-only verification with V8 GC pauses destroying the <5ms latency requirement, unbounded DID resolver cache, status list race condition, O(n·m) policy check.

v2 was scrapped. The document is preserved in docs/Drs_architecture_v2.md.

v3/v4 — OAuth 2.1 profile (current)

The final pivot was from UCAN to OAuth 2.1. The reason: the ecosystem standardised on OAuth. AT Protocol and MCP both chose JWT + OAuth. UCAN's production adoption is near-zero. Building on UCAN would have meant building on a standard nobody uses.

The current architecture separates concerns by language: Rust for crypto (zero GC), Go for verification middleware (goroutines), TypeScript for the developer SDK (npm ecosystem).

The Five Actors

DRS defines five actors. Every use case in this documentation maps to one or more of them.

1. End User (Human granting authority)

The human whose data or resources are at stake. They grant the initial delegation through a consent UI that translates policy into plain English.

What they see:

Research Agent wants permission to:
✓  Search the web
✓  Save files to your workspace
✗  Cannot access personal data
✗  Cannot spend more than £50.00

This permission lasts 30 days. Revoke it at any time.

The drs_consent field in the root DR records evidence of this consent: the method (explicit-ui-click), timestamp, session ID, and a SHA-256 hash of the human-readable text the user actually saw — not the machine-readable policy JSON.

Key concern: Did I actually authorise this specific action?


2. Developer

Integrates DRS into MCP tool servers or agent runtimes. Interacts with the TypeScript SDK (@drs/sdk) and the drs-verify HTTP API.

What they do:

  • Call issueRootDelegation / issueSubDelegation from the SDK
  • Add X-DRS-Bundle header to MCP requests
  • Deploy the Go middleware in front of their tool server
  • Use the CLI (drs verify, drs audit, drs policy) for debugging

Key concern: How do I integrate this in a day?


3. Agent Runtime

The automated system that acts on the user's behalf. Can be a single agent or a chain of agents delegating to sub-agents.

What it does:

  • Evaluates its policy before every action (POLA — principle of least authority)
  • Issues sub-delegation receipts when delegating to sub-agents
  • Escalates out-of-policy requests to a supervisor agent (not a human)
  • Auto-renews delegations before expiry for machine-rooted standing delegations
  • Stops immediately when any policy constraint is exceeded

Key concern: Can I take this action within my current delegation?


4. Tool Server (Operator)

Receives MCP requests with X-DRS-Bundle headers. Runs full chain verification before executing any tool. Deployed by the enterprise operator.

What it does:

  • Extracts and parses the DRS bundle from the HTTP header
  • Runs verify_chain (Blocks A–F) on every request — fail-closed
  • Rejects requests with invalid bundles
  • Rate-limits by root_principal (not just agent DID) to prevent agent-churn bypass
  • Emits drs:tool-call activity events

Key concern: Is this agent authorised to call this tool with these arguments?


5. Auditor / Compliance Officer

Reconstructs delegation chains after the fact to produce evidence for regulators, legal proceedings, or internal investigation. Does not need operator cooperation.

What they do:

drs audit retrieve --inv-jti "inv:7h5c4d3e-..."
drs verify evidence.json
drs audit export --inv-jti "inv:7h5c4d3e-..." --format eu-ai-act

Key concern: Can I prove what happened to a standard that satisfies EU AI Act Article 12?


How the actors interact

End User ──grant──► Agent Runtime ──sub-delegate──► Sub-Agent
                                                          │
                                                     invoke tool
                                                          ▼
                                                    Tool Server
                                                  (verify_chain)
                                                          │
                                                    emit event
                                                          ▼
                                                    Auditor reads
                                                    evidence later

The Developer builds both the agent runtime and the tool server integrations. The Operator deploys and configures the verification infrastructure.

Data Model

DRS defines three JWT types and one bundle format. All JWTs use the {"alg":"EdDSA","typ":"JWT"} header and are canonicalised with RFC 8785 JCS before signing.

1. Delegation Receipt (DR)

A signed JWT issued by each delegator. The root DR is issued by the human (or automated operator). Sub-DRs are issued by each agent in the chain.

Root DR payload example

{
  "aud": "did:key:z6MkAgent1...",
  "cmd": "/mcp/tools/call",
  "drs_consent": {
    "locale": "en-GB",
    "method": "explicit-ui-click",
    "policy_hash": "sha256:abc123...",
    "session_id": "sess:8f3a2b1c",
    "timestamp": "2026-03-28T10:30:00Z"
  },
  "drs_regulatory": {
    "frameworks": ["eu-ai-act-art13"],
    "retention_days": 730,
    "risk_level": "limited"
  },
  "drs_root_type": "human",
  "drs_type": "delegation-receipt",
  "drs_v": "4.0",
  "exp": 1745592000,
  "iat": 1743000000,
  "iss": "did:key:z6MkHuman...",
  "jti": "dr:8f3a2b1c-4d5e-4xxx-8b9c-0d1e2f3a4b5c",
  "nbf": 1743000000,
  "policy": {
    "allowed_tools": ["web_search", "write_file"],
    "max_calls": 100,
    "max_cost_usd": 50.00,
    "pii_access": false,
    "write_access": false
  },
  "prev_dr_hash": null,
  "sub": "did:key:z6MkHuman..."
}

Note: Keys are sorted by Unicode code point in the JWT payload (RFC 8785 JCS). This is not cosmetic — it ensures identical bytes for identical data across all implementations.

Sub-DR differences

Sub-DRs differ from root DRs in three ways:

  1. iss changes to the delegating agent's DID (not the human)
  2. policy must be a strict subset of the parent's policy (POLA)
  3. prev_dr_hash contains sha256:{hex of parent DR JWT bytes} instead of null
  4. drs_consent and drs_root_type are absent

Field reference

See JWT Fields Reference for the complete field table with types and constraints.

2. Invocation Receipt

Records the actual tool call. Issued by the agent making the call immediately before invoking the tool.

{
  "args": {
    "estimated_cost_usd": 0.02,
    "query": "Monad TPS benchmarks",
    "tool": "web_search"
  },
  "cmd": "/mcp/tools/call",
  "dr_chain": ["sha256:abc123...", "sha256:def456..."],
  "drs_type": "invocation-receipt",
  "drs_v": "4.0",
  "iat": 1743000300,
  "iss": "did:key:z6MkAgent2...",
  "jti": "inv:7h5c4d3e-2a3b-4c5d-6e7f-8a9b0c1d2e3f",
  "sub": "did:key:z6MkHuman...",
  "tool_server": "did:key:z6MkToolServer..."
}

The dr_chain array contains the sha256:{hex} hashes of every DR in the chain, in order from root (index 0) to most recent sub-DR.

3. DRS Bundle

The unit of transport. Transmitted as the X-DRS-Bundle HTTP header (base64url of the JSON):

{
  "bundle_version": "4.0",
  "invocation": "<invocation-receipt-jwt>",
  "receipts": [
    "<root-dr-jwt>",
    "<sub-dr-jwt-1>",
    "<sub-dr-jwt-2>"
  ]
}

The receipts array is ordered from root (index 0) to most recent sub-delegation.

Chain hash computation

prev_dr_hash = "sha256:" + lowercase_hex(SHA-256(UTF-8 bytes of previous DR JWT string))

The hash is computed over the raw JWT string (the header.payload.signature format), not just the payload.

// TypeScript (drs-sdk)
function computeChainHash(jwt: string): string {
  const bytes = new TextEncoder().encode(jwt);
  const digest = sha256(bytes);
  return 'sha256:' + Array.from(digest)
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}
#![allow(unused)]
fn main() {
// Rust (drs-core)
pub fn compute_chain_hash(jwt: &str) -> String {
    let digest = sha2::Sha256::digest(jwt.as_bytes());
    format!("sha256:{}", hex::encode(digest))
}
}

Both must produce identical output for the same JWT string. This is verified in the cross-implementation test suite.

Verification Algorithm

verify_chain runs six sequential blocks (A–F). The function is fail-closed: any error in any block immediately returns an error without evaluating subsequent blocks.

The six blocks

Block A — Completeness

What: Bundle has at least one delegation receipt and exactly one invocation receipt.

Fail condition: receipts array is empty, or invocation field is missing or null.


Block B — Structural Integrity

What: The delegation receipts form a valid tamper-evident chain.

For each receipt at index i:

  • receipts[i].aud must equal receipts[i+1].iss (audience of each DR is the issuer of the next)
  • receipts[i+1].prev_dr_hash must equal sha256:{SHA-256 of receipts[i] JWT bytes}
  • The invocation's dr_chain[i] must equal sha256:{SHA-256 of receipts[i] JWT bytes}

Fail condition: Any hash mismatch, any issuer/audience gap, any missing dr_chain entry.

This block defeats chain splicing: substituting any DR changes its bytes, which changes its hash, which breaks prev_dr_hash in the next DR.


Block C — Cryptographic Validity

What: Every JWT signature is valid.

For each JWT in the bundle:

  1. Parse JWT header — must be {"alg":"EdDSA","typ":"JWT"}
  2. Resolve the issuer DID to its Ed25519 public key (LRU-cached)
  3. Verify the EdDSA signature over base64url(header).base64url(payload)
  4. Enforce S < L (strict mode — rejects signature malleability)

Fail condition: Any signature invalid, any DID unresolvable, any S ≥ L.

Security: The multicodec prefix check when resolving did:key DIDs uses crypto/subtle.ConstantTimeCompare in Go and subtle::ConstantTimeEq in Rust. Using bytes.Equal or == leaks timing information.


Block D — Semantic / Policy Compliance

What: The invocation arguments comply with every policy in the chain, and no sub-DR escalates beyond its parent.

Policies are evaluated conjunctively — all policies must pass:

  • args.tool must be in policy.allowed_tools (if set) at every level
  • args.estimated_cost_usd must be ≤ policy.max_cost_usd (if set) at every level
  • args.pii_access must be false if policy.pii_access is false at any level

Sub-DR attenuation check:

  • Each sub-DR's policy must be a subset of its parent's policy
  • Any escalation (wider tool list, higher cost limit, pii_access: true when parent has false) fails this block

Fail condition: Any argument exceeds any policy constraint, or any sub-DR escalates permissions.


Block E — Temporal Validity

What: All receipts are valid at the current time, and temporal bounds are properly nested.

  • now ≥ receipt.nbf for every receipt
  • now ≤ receipt.exp for every receipt where exp is not null
  • Sub-DR nbf ≥ parent nbf
  • Sub-DR exp ≤ parent exp (when both are set)

Fail condition: Any receipt is expired, not yet valid, or has invalid temporal nesting.


Block F — Revocation

What: No receipt has been revoked via either the remote Bitstring Status List or the local revocation store.

For each delegation receipt with a drs_status_list_index (the invocation receipt is not checked for revocation):

  1. Remote check — Fetch the W3C Bitstring Status List from STATUS_LIST_BASE_URL (configurable TTL cache, default 5 minutes, with sync.Once concurrency guard). Bit 1 = revoked.
  2. Local check — Query the in-memory local revocation store. Entries are added immediately via POST /admin/revoke. This store does not survive process restart.

Both checks run for every receipt. Either alone is sufficient to fail verification.

Fail condition: Any receipt's drs_status_list_index is marked as revoked in either source, or the remote status list check returns an error (fail-closed — an unavailable status list blocks verification).

The sync.Once guard prevents double-fetch race conditions: when the remote cache expires and multiple goroutines arrive simultaneously, only one HTTP request is made — all others wait and reuse the result.


Algorithm pseudocode

verify_chain(bundle) → Result<VerifiedChain, VerifyError>:

  # Block A
  if bundle.receipts is empty: return Err(BUNDLE_INCOMPLETE)
  if bundle.invocation is null: return Err(BUNDLE_INCOMPLETE)

  drs = [decode_jwt(r) for r in bundle.receipts]
  inv = decode_jwt(bundle.invocation)

  # Block B
  for i in 0..len(drs)-1:
    if drs[i].aud != drs[i+1].iss: return Err(ISSUER_AUDIENCE_GAP)
    if drs[i+1].prev_dr_hash != sha256(bundle.receipts[i]): return Err(CHAIN_HASH_MISMATCH)
  for i, dr in enumerate(drs):
    if inv.dr_chain[i] != sha256(bundle.receipts[i]): return Err(CHAIN_HASH_MISMATCH)

  # Block C
  for (jwt, payload) in receipts + invocation:
    pub_key = resolve_did(payload.iss)  # LRU cached, constant-time prefix check
    if not ed25519_verify_strict(jwt, pub_key): return Err(SIGNATURE_INVALID)

  # Block D
  for dr in drs:
    if not args_satisfy_policy(inv.args, dr.policy): return Err(POLICY_VIOLATION)
  for i in 1..len(drs):
    if not is_attenuated_subset(drs[i].policy, drs[i-1].policy): return Err(POLICY_ESCALATION)

  # Block E
  now = unix_timestamp()
  for dr in drs:
    if now < dr.nbf: return Err(RECEIPT_NOT_YET_VALID)
    if dr.exp != null and now > dr.exp: return Err(RECEIPT_EXPIRED)
  for i in 1..len(drs):
    if drs[i].nbf < drs[i-1].nbf: return Err(TEMPORAL_BOUNDS_VIOLATION)
    if both have exp and drs[i].exp > drs[i-1].exp: return Err(TEMPORAL_BOUNDS_VIOLATION)

  # Block F
  for dr in drs:
    if dr.drs_status_list_index != null:
      if remote_status_list.is_revoked(dr.drs_status_list_index):
        return Err(RECEIPT_REVOKED)
      if local_revocation_store.is_revoked(dr.drs_status_list_index):
        return Err(RECEIPT_REVOKED)

  return Ok(VerifiedChain{root_principal, subject, chain_depth, policy_result})

Performance targets

At 10,000 requests/second on the Go verification server:

OperationCostNotes
Policy check per levelO(1) avgHash-set intersection in capability index
DID resolutionO(1) amortisedLRU cache, 10,000 entry cap, 1-hour TTL
Status list checkO(1) amortised5-min TTL, sync.Once guard
Ed25519 verify~0.1ms/siged25519-dalek 2.x
Total per request (2-hop chain)~0.8ms p99

Architecture

DRS uses a three-language stack. Each language handles the layer it is genuinely best suited for. This is not aesthetic preference — it is a consequence of the performance and correctness requirements of each layer.

The three layers

┌──────────────────────────────────────────────┐
│  TypeScript SDK  (@drs/sdk)                   │
│  Issuance path only. Low-frequency.           │
│  issueRootDelegation, issueSubDelegation,     │
│  buildBundle, CLI tools                       │
│  Delegates crypto to WASM or HTTP             │
└───────────────────┬──────────────────────────┘
                    │  WASM (drs-core compiled for browser)
                    │  HTTP (VerifyClient → drs-verify)
┌───────────────────▼──────────────────────────┐
│  Go Middleware  (drs-verify)                  │
│  Verification path. High-frequency.           │
│  verify_chain (6 blocks), MCP/A2A middleware  │
│  LRU DID resolver, status list cache          │
│  Single static binary, goroutine-based        │
└───────────────────┬──────────────────────────┘
                    │  Rust crate (native library)
                    │  WASM (browser target)
┌───────────────────▼──────────────────────────┐
│  Rust Core  (drs-core)                        │
│  Ed25519 sign/verify, SHA-256, JCS (RFC 8785) │
│  Capability index (O(1) policy check)         │
│  DID key resolution, chain hash computation   │
│  Zero GC. Stack-allocated. Deterministic.     │
└──────────────────────────────────────────────┘

Why Rust for the core

Ed25519 signature verification requires deterministic, constant-time operations. V8 (JavaScript/TypeScript) cannot guarantee either:

  • GC pauses of 50–1500ms are common under load
  • V8 does not guarantee constant-time execution of arithmetic

Rust's ed25519-dalek 2.x provides:

  • RUSTSEC-2022-0093 patched (batch verification side-channel fixed)
  • verify_strict() enforcing S < L — rejects signature malleability
  • subtle::ConstantTimeEq for security-sensitive comparisons
  • Compiles to native library (used by Go via CGO) and WASM (used in browsers)

The v2 architecture used TypeScript for verification and hit all of these problems in implementation.

Why Go for verification

The verification server handles thousands of concurrent requests. Go's goroutine model gives concurrent request handling without the complexity of async Rust, while the GC is predictable enough for the latency requirements:

  • sync.Once prevents double-fetch race conditions in the Bitstring Status List cache
  • hashicorp/golang-lru/v2 — LRU with hard cap of 10,000 entries (~640KB)
  • crypto/subtle.ConstantTimeCompare for DID multicodec prefix checks
  • CGO_ENABLED=0 go build — single static binary, no runtime dependencies
  • /healthz and /readyz endpoints for Kubernetes readiness probes

Why TypeScript for the SDK

Delegation issuance is low-frequency (human sets up a session, agent runtime boots). The developer experience matters more than raw performance. TypeScript gives:

  • Native npm ecosystem integration — one pnpm add @drs/sdk
  • IDE autocompletion and type safety
  • Browser compatibility for consent UIs
  • Matches the existing tech stack of most MCP server developers

TypeScript never handles verification — that path runs in Go or Rust.

JCS canonicalisation

All JWTs in DRS are canonicalised with RFC 8785 (JSON Canonicalization Scheme) before signing. This ensures two logically equivalent objects always produce identical JWT bytes, regardless of which implementation created them.

The rules:

  • Object keys sorted recursively by Unicode code point
  • No whitespace
  • IEEE 754 shortest number representation
// WRONG — does not sort nested object keys:
const payload = JSON.stringify(obj);

// CORRECT — RFC 8785 JCS:
const payload = jcsSerialise(obj);  // from drs-sdk/src/sdk/issue.ts

The Rust implementation uses serde-json-canonicalizer. The TypeScript implementation has an inline jcsSerialise() function. Both must produce identical output for the same logical object — this is tested in the cross-implementation test suite.

WASM build

cd drs-core
wasm-pack build --target web --features wasm
# Output: drs-core/pkg/ — publish as @drs/wasm

The @drs/wasm package is an optional peer dependency of @drs/sdk. The WASM loader (src/wasm/loader.ts) is:

  • Idempotent: initWasm() can be called multiple times safely
  • Lazy: the WASM binary is not fetched until initWasm() is awaited
  • Graceful: if @drs/wasm is not installed, a clear error is thrown

Security Model

Threat table

ThreatAttack vectorDRS mitigationResidual risk
Forged root DRAttacker creates a fake delegationEd25519 EUF-CMA: forgery requires solving the discrete logPrivate key theft
Chain splicingCompromised agent substitutes unrelated tokenprev_dr_hash: any substitution changes the hash chain — fails Block BImplementation bugs in hash computation
Policy escalationSub-DR claims wider permissions than parentcheck_policy_attenuation() at issuance + Block D at verificationPolicy schema gaps
Policy violationAgent passes arguments exceeding constraintsBlock D evaluates all policies conjunctivelyUnlisted policy fields are not checked
DR tamperingAttacker modifies a signed DREd25519 signature fails — fails Block CNone — structural
Chain injectionInsert a fake intermediate DRprev_dr_hash changes break subsequent links — fails Block BNone — structural
Replay after revocationAgent replays a revoked DRBlock F: Bitstring Status List (5-min cache TTL)Up to 5-minute stale cache window
JSON malleabilityDifferent canonical bytes for same logical JSONRFC 8785 JCS enforced at both issuance and verification endsNon-conforming JCS at one end
Signature malleability(R, S) and (R, S+L) both verify under naive checked25519-dalek 2.x enforces S < L via verify_strict()None — library enforces
DID spoofingAttacker impersonates a legitimate issuerdid:key DIDs are derived from the public key — impossible without the private keydid:web requires DNS/TLS security
Prompt injectionAttacker embeds instructions in tool contentDRS records every invocation chainOut of scope — model/runtime responsibility
Model-level bypassAdversarial prompts bypass safety constraintsModel safety ≠ execution safetyEntirely outside DRS scope

Fail-closed principle

DRS verification is fail-closed. Any error in any block returns an error and rejects the request. This applies to:

  • Unresolvable DIDs
  • Malformed JWTs
  • Network errors fetching the Bitstring Status List
  • Policy fields the verifier does not recognise

A partially valid chain is an invalid chain.

Constant-time operations

All security-sensitive comparisons use constant-time equality to prevent timing side-channels:

LanguageSafeUnsafe
Gocrypto/subtle.ConstantTimeComparebytes.Equal, ==
Rustsubtle::ConstantTimeEq== on byte slices

This applies specifically to multicodec prefix checks when resolving did:key DIDs. The two-byte prefix [0xed, 0x01] identifies an Ed25519 key. Checking it with a short-circuit comparison leaks timing information about where the mismatch occurs.

Key management

Key typeRecommended storageRotation
Human root keyHardware Security Module or Secure EnclaveNot rotated (DID is derived from key)
Operator root keyHSM required for productionAnnual with overlap period
Agent session keyEphemeral — generated per sessionPer-session, never persisted

did:key is the preferred DID method: the DID encodes the public key directly. No registry, no DNS, no trust anchor beyond the key itself. did:web is supported but requires DNS and TLS security.

What DRS does not protect against

  • Prompt injection: An attacker embedding instructions in tool output or environment data. This is a model-layer problem. DRS records that the invocation happened and under what authorisation — it does not prevent the model from following injected instructions.
  • Key compromise: If a private key is stolen, the attacker can forge receipts signed by that key. Mitigation: rotate keys, revoke outstanding delegations.
  • Post-compromise recovery: DRS does not define how to recover a system after key material is compromised. That is an operational problem.

Regulatory Alignment

DRS is designed to produce evidence that satisfies the specific record-keeping requirements of AI governance regulations. The receipts are cryptographically signed and independently verifiable — an auditor does not need operator cooperation to authenticate the evidence.

EU AI Act

Article 12 — Record-keeping for high-risk AI systems

Article 12 requires high-risk AI systems to automatically log events with sufficient detail to enable post-market monitoring and investigation. DRS Delegation Receipts satisfy this:

  • Tamper-evident: Ed25519 signatures; any modification breaks verification
  • Independently verifiable: Public keys are encoded in the DID — no central authority needed
  • Comprehensive: Every delegation hop and every invocation is receipted

Article 13 — Transparency

Article 13 requires transparency in the operation of high-risk AI systems. DRS provides:

  • Human-readable policy translation at the point of consent (the drs_consent.policy_hash covers the text the user saw — not just the machine-readable JSON)
  • Complete chain reconstruction without operator involvement
  • Per-invocation records linking every agent action to the authorising human

Export:

pnpm exec drs audit export --inv-jti "inv:..." --format eu-ai-act --output evidence.json

HIPAA §164.312(b) — Audit Controls

For healthcare deployments handling PHI, HIPAA §164.312(b) requires audit controls that record and examine activity. DRS provides:

  • Invocation Receipts recording every agent action with full delegation provenance
  • Signed proof that access was authorised before it occurred (not just a log that it happened)
  • Tier 3 (Compliant) storage with WORM policy and 7-year retention

AIUC-1 Certification

AIUC (AI Underwriting Company, founded July 2025 with $15M seed) certifies AI systems for insurance underwriting. AIUC-1 requires demonstrable proof of authorisation for every agent action — not just server logs.

The AIUC-1 requirement: "For any agent action, provide cryptographic proof that the action was within the scope of an authorisation granted by an identifiable principal."

DRS Delegation Receipts satisfy this directly. AIUC-1 is identified as the primary near-term commercial opportunity for DRS-based deployments.

SOC 2 Type II

SOC 2 requires continuous evidence of access controls. DRS provides:

  • Signed receipts for every delegation grant (who authorised what, when, with what constraints)
  • Tamper-evident chain linking every action to its authorisation
  • Revocation mechanism for compromised keys

FINOS AI Governance Framework

FINOS Tier 3–4 levels require chain-of-custody evidence admissible in legal proceedings. DRS Delegation Receipts are:

  • Based on open standards (Ed25519, JWT, OAuth 2.1) — no proprietary formats
  • Independently verifiable — no vendor lock-in for evidence authentication
  • Exportable in structured formats

Relevant financial regulations: SR 11-7 (Federal Reserve model risk management), EBA Guidelines on ICT risk, GDPR Article 22 (automated decision-making explainability), MiFID II audit trails.

Storage tiers and retention

Tierstorage_tierBackendRetentionUse case
Session0In-memoryProcess lifetimeDevelopment, testing
Ephemeral1Local filesystemConfigurable TTLNon-regulated production
Durable2S3 / GCS / Azure BlobConfigurableStandard production
Compliant3WORM object storage7 years minimumHIPAA, financial services
On-chain4Monad EVMPermanentHighest-assurance regulatory

Configure via storage_tier in the Operator Configuration.

Tutorial: Issue Your First Delegation

This tutorial walks through issuing a root delegation receipt and a sub-delegation from scratch using the TypeScript SDK. You will end up with two signed JWTs linked by prev_dr_hash.

Prerequisites

  • Node.js 20+ and pnpm
  • @drs/sdk installed: pnpm add @drs/sdk

Step 1: Generate two keypairs

# Human keypair
pnpm exec drs keygen
# Private key: <HUMAN_PRIVATE_KEY>
# DID:         did:key:z6MkHUMAN...

# Agent keypair
pnpm exec drs keygen
# Private key: <AGENT_PRIVATE_KEY>
# DID:         did:key:z6MkAGENT...

Step 2: Issue the root delegation

Create issue-demo.ts:

import { issueRootDelegation, computeChainHash } from '@drs/sdk';

const humanKey = Uint8Array.from(Buffer.from('HUMAN_PRIVATE_KEY', 'base64url'));
const now = Math.floor(Date.now() / 1000);

const rootDR = await issueRootDelegation({
  signingKey:  humanKey,
  issuerDid:   'did:key:z6MkHUMAN...',
  subjectDid:  'did:key:z6MkHUMAN...',   // human is both issuer and subject at root
  audienceDid: 'did:key:z6MkAGENT...',
  cmd: '/mcp/tools/call',
  policy: {
    allowed_tools: ['web_search', 'write_file'],
    max_cost_usd: 50.00,
    pii_access: false,
    write_access: false,
  },
  nbf: now,
  exp: now + 86400,   // 24 hours
  rootType: 'human',
  consent: {
    method: 'explicit-ui-click',
    timestamp: new Date().toISOString(),
    session_id: 'sess:' + crypto.randomUUID(),
    policy_hash: 'sha256:placeholder', // In production: sha256 of human-readable text
    locale: 'en-GB',
  },
});

console.log('Root DR JWT:', rootDR);
console.log('Root DR hash:', computeChainHash(rootDR));

Run it:

pnpm exec tsx issue-demo.ts

Step 3: Issue a sub-delegation

The agent narrows the policy before delegating further:

import { issueSubDelegation } from '@drs/sdk';

const agentKey = Uint8Array.from(Buffer.from('AGENT_PRIVATE_KEY', 'base64url'));

const parentPolicy = {
  allowed_tools: ['web_search', 'write_file'],
  max_cost_usd: 50.00,
  pii_access: false,
  write_access: false,
};

const subDR = await issueSubDelegation({
  signingKey:   agentKey,
  issuerDid:    'did:key:z6MkAGENT...',
  subjectDid:   'did:key:z6MkHUMAN...',    // subject never changes
  audienceDid:  'did:key:z6MkSUBAGENT...',
  cmd: '/mcp/tools/call',
  policy: {
    allowed_tools: ['web_search'],           // tightened: removed write_file
    max_cost_usd: 5.00,                      // tightened: £50 → £5
    pii_access: false,
    write_access: false,
  },
  nbf: now,
  exp: now + 3600,     // 1 hour (shorter than parent's 24 hours)
  parentJwt:    rootDR,
  parentPolicy: parentPolicy,
  parentNbf:    now,
  parentExp:    now + 86400,
});

console.log('Sub-DR JWT:', subDR);

What you built

You now have two JWTs where:

  • subDR payload contains "prev_dr_hash": "sha256:{hash of rootDR}"
  • The policy in subDR is strictly contained within rootDR's policy
  • Any tampering with rootDR breaks the hash chain — fails Block B of verification

What happens if you try to escalate?

Try setting max_cost_usd: 100 in the sub-delegation (exceeds the parent's 50):

DrsError: POLICY_ESCALATION — max_cost_usd 100 exceeds parent limit 50

The error fires at issuance — before any signing occurs. You cannot accidentally create an invalid chain.

Next steps

Tutorial: Verify a Bundle

This tutorial builds on Issue Your First Delegation. You will wrap the delegation chain in a bundle, send it to drs-verify, and confirm it passes all six verification blocks.

Prerequisites

  • Go 1.22+ with drs-verify source
  • The rootDR and subDR JWTs from Tutorial 1
  • @drs/sdk installed

Step 1: Start drs-verify

cd drs-verify
go run ./cmd/server
# drs-verify listening on :8080

Step 2: Issue an invocation receipt

import { issueInvocation, computeChainHash, buildBundle, serialiseBundle } from '@drs/sdk';
import { writeFileSync } from 'fs';

const agentKey = Uint8Array.from(Buffer.from('SUBAGENT_PRIVATE_KEY', 'base64url'));

const invocation = await issueInvocation({
  signingKey:  agentKey,
  issuerDid:   'did:key:z6MkSUBAGENT...',
  subjectDid:  'did:key:z6MkHUMAN...',
  cmd: '/mcp/tools/call',
  args: {
    tool: 'web_search',
    query: 'Monad TPS benchmarks',
    estimated_cost_usd: 0.02,
  },
  drChain: [
    computeChainHash(rootDR),
    computeChainHash(subDR),
  ],
  toolServer: 'did:key:z6MkTOOLSERVER...',
});

// Build and serialise the bundle
const bundle = buildBundle({
  invocation,
  receipts: [rootDR, subDR],
});

writeFileSync('bundle.json', serialiseBundle(bundle));
console.log('Bundle written to bundle.json');

Step 3: Verify via CLI

DRS_VERIFY_URL=http://localhost:8080 pnpm exec drs verify bundle.json

Expected output:

✓ Bundle verified
  Chain depth:    2
  Root principal: did:key:z6MkHUMAN...
  Subject:        did:key:z6MkHUMAN...
  Command:        /mcp/tools/call
  Policy result:  pass
  Blocks:         A✓ B✓ C✓ D✓ E✓ F✓

Step 4: Verify via HTTP API directly

curl -s -X POST http://localhost:8080/verify \
  -H "Content-Type: application/json" \
  -d @bundle.json | jq .
{
  "valid": true,
  "chain_depth": 2,
  "root_principal": "did:key:z6MkHUMAN...",
  "subject": "did:key:z6MkHUMAN...",
  "command": "/mcp/tools/call",
  "policy_result": "pass"
}

Step 5: Test a rejection

Tamper with the bundle — modify one character in rootDR:

# Create a tampered bundle
cat bundle.json | sed 's/"receipts":\["eyJ/\"receipts\":[\"fakeXXX/' > tampered.json
DRS_VERIFY_URL=http://localhost:8080 pnpm exec drs verify tampered.json

Expected:

✗ Verification failed
  Block:  B (structural integrity)
  Error:  CHAIN_HASH_MISMATCH — prev_dr_hash mismatch at chain index 1

Step 6: Print the full audit trail

pnpm exec drs audit bundle.json

This prints a human-readable breakdown of every receipt in the chain: issuers, audiences, policies, temporal bounds, and consent records.

Next steps

Tutorial: End-to-End Trace

This tutorial traces one complete tool call through the full DRS lifecycle — from the human granting authority to the auditor reading the evidence. It covers every actor and every component.

The scenario

  • Amara (End User): grants a research agent permission to use web_search, spend up to £50
  • Research Agent: delegates to a sub-agent with tighter constraints (£5, web_search only)
  • Sub-Agent: calls web_search on the tool server
  • Tool Server: runs verify_chain before executing
  • Auditor: reconstructs the chain afterwards

Step 1: Amara grants authority

Amara sees a consent UI on the developer's application:

Research Agent wants permission to:
✓  Search the web
✗  Cannot access personal data
✗  Cannot spend more than £50.00

This permission lasts 30 days.  [Allow] [Deny]

Amara clicks Allow. The SDK issues the root DR:

Root DR JWT payload (keys sorted by JCS):
{
  "aud": "did:key:z6MkAgent1...",
  "cmd": "/mcp/tools/call",
  "drs_consent": {
    "locale": "en-GB",
    "method": "explicit-ui-click",
    "policy_hash": "sha256:a1b2c3...",   ← SHA-256 of the text Amara saw
    "session_id": "sess:abc-123",
    "timestamp": "2026-03-28T10:30:00Z"
  },
  "drs_root_type": "human",
  "drs_type": "delegation-receipt",
  "drs_v": "4.0",
  "exp": 1748437800,
  "iat": 1743000000,
  "iss": "did:key:z6MkAmara...",
  "jti": "dr:8f3a2b1c-4d5e-4abc-8b9c-0d1e2f3a4b5c",
  "nbf": 1743000000,
  "policy": { "allowed_tools": ["web_search"], "max_cost_usd": 50 },
  "prev_dr_hash": null,
  "sub": "did:key:z6MkAmara..."
}

The JWT is signed with Amara's Ed25519 private key.


Step 2: Research Agent evaluates and sub-delegates

Before acting, the Research Agent evaluates the proposed web_search call against its current policy:

  • web_searchallowed_tools
  • estimated_cost: 0.02max_cost_usd: 50

The Research Agent decides to delegate to the Sub-Agent with tighter constraints (POLA):

Sub-DR JWT payload:
{
  "aud": "did:key:z6MkAgent2...",
  "cmd": "/mcp/tools/call",
  "drs_type": "delegation-receipt",
  "drs_v": "4.0",
  "exp": 1743003600,          ← 1 hour (shorter than Amara's 30 days)
  "iat": 1743000010,
  "iss": "did:key:z6MkAgent1...",
  "jti": "dr:1a2b3c4d-5e6f-4xyz-9abc-def012345678",
  "nbf": 1743000000,
  "policy": { "allowed_tools": ["web_search"], "max_cost_usd": 5 },
  "prev_dr_hash": "sha256:abc123...",   ← SHA-256 of rootDR JWT bytes
  "sub": "did:key:z6MkAmara..."         ← unchanged
}

Step 3: Sub-Agent creates the invocation receipt

The Sub-Agent records the actual tool call:

Invocation Receipt JWT payload:
{
  "args": {
    "estimated_cost_usd": 0.02,
    "query": "Monad TPS benchmarks",
    "tool": "web_search"
  },
  "cmd": "/mcp/tools/call",
  "dr_chain": [
    "sha256:abc123...",   ← SHA-256 of rootDR
    "sha256:def456..."    ← SHA-256 of subDR
  ],
  "drs_type": "invocation-receipt",
  "drs_v": "4.0",
  "iat": 1743000300,
  "iss": "did:key:z6MkAgent2...",
  "jti": "inv:7h5c4d3e-2a3b-4c5d-6e7f-8a9b0c1d2e3f",
  "sub": "did:key:z6MkAmara...",
  "tool_server": "did:key:z6MkToolServer..."
}

Step 4: Tool server receives and verifies

The Sub-Agent sends:

POST /mcp/tools/call HTTP/1.1
Authorization: Bearer <oauth-token>
X-DRS-Bundle: eyJidW5kbGVfdmVyc2lvbiI6IjQuMCIsImludm9jYXRpb24i...
Content-Type: application/json

{"tool": "web_search", "query": "Monad TPS benchmarks"}

The MCP middleware runs verify_chain:

Block A: receipts=[rootDR, subDR], invocation present                → PASS ✓
Block B: rootDR.aud == subDR.iss (z6MkAgent1)                        → PASS ✓
         subDR.prev_dr_hash == sha256(rootDR)                         → PASS ✓
         inv.dr_chain matches [sha256(rootDR), sha256(subDR)]         → PASS ✓
Block C: Ed25519 verify rootDR (Amara's key)                         → PASS ✓
         Ed25519 verify subDR (Agent1's key)                          → PASS ✓
         Ed25519 verify invocation (Agent2's key)                     → PASS ✓
Block D: web_search ∈ allowed_tools at both DR levels                → PASS ✓
         0.02 ≤ 5 (subDR) ≤ 50 (rootDR)                             → PASS ✓
         subDR policy ⊆ rootDR policy (attenuation check)            → PASS ✓
Block E: now=1743000300 ∈ [1743000000, 1743003600] (subDR window)    → PASS ✓
         now=1743000300 ∈ [1743000000, 1748437800] (rootDR window)   → PASS ✓
Block F: rootDR jti "dr:8f3a..." not in status list                  → PASS ✓
         subDR jti "dr:1a2b..." not in status list                   → PASS ✓

RESULT: VALID — executing tool call

Step 5: Tool executes and emits event

The tool server runs web_search and emits:

{
  "event": "drs:tool-call",
  "root_principal": "did:key:z6MkAmara...",
  "chain_depth": 2,
  "command": "/mcp/tools/call",
  "tool": "web_search",
  "policy_result": "pass",
  "cost_usd": 0.02,
  "inv_jti": "inv:7h5c4d3e-..."
}

Amara's activity feed updates: "Research agent used web_search — £0.02 of £50.00 budget used."


Step 6: Auditor reconstructs the chain (later)

Three months later, a compliance officer wants evidence:

pnpm exec drs audit retrieve --inv-jti "inv:7h5c4d3e-..." --output evidence.json
pnpm exec drs verify evidence.json
pnpm exec drs audit evidence.json

Output:

DRS Chain Audit
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Receipt 0 (root — human)
  Issued by:  did:key:z6MkAmara...
  Granted to: did:key:z6MkAgent1...
  Policy:     max_cost_usd=50, allowed_tools=[web_search]
  Valid:      2026-03-28 → 2026-05-28
  Consent:    explicit-ui-click at 2026-03-28T10:30:00Z (en-GB)

Receipt 1 (sub-delegation)
  Issued by:  did:key:z6MkAgent1...
  Granted to: did:key:z6MkAgent2...
  Policy:     max_cost_usd=5, allowed_tools=[web_search]
  Chain hash: sha256:def456... ✓

Invocation
  Called by:  did:key:z6MkAgent2...
  Tool:       web_search("Monad TPS benchmarks")
  Cost:       $0.02 of $5.00 (sub-delegation limit)
  Chain hash: sha256:ghi789... ✓

All 3 signatures valid. Chain intact. No revocations.

The compliance officer does not need to contact the operator. The evidence is in the receipts.

Install the SDK

Requirements

  • Node.js 20+
  • pnpm (required — do not use npm or yarn)

Install

pnpm add @drs/sdk

Optional: browser-side verification

For client-side verification in browser environments, also install the WASM package:

pnpm add @drs/wasm

@drs/sdk detects @drs/wasm at runtime. If absent, the SDK's VerifyClient falls back to calling drs-verify over HTTP. If present, initWasm() loads the WASM module for local verification.

TypeScript configuration

{
  "compilerOptions": {
    "moduleResolution": "bundler",
    "target": "ES2022",
    "lib": ["ES2022", "DOM"]
  }
}

Verify the install

pnpm exec drs keygen

Expected:

Private key (keep secret): <base64url 32 bytes>
DID:                        did:key:z6Mk...

What's in the package

Export pathContents
@drs/sdk (main)issueRootDelegation, issueSubDelegation, issueInvocation, buildBundle, serialiseBundle, parseBundle, computeChainHash, checkPolicyAttenuation, translatePolicy
@drs/sdk/verifyVerifyClient — HTTP client for drs-verify
@drs/sdk/wasminitWasm, getWasmModule, isWasmReady
@drs/sdk/typesAll TypeScript interfaces

Building the WASM package yourself

If you are developing locally or want to use unreleased drs-core changes:

cd drs-core
wasm-pack build --target web --features wasm
# Output: drs-core/pkg/

# Link it locally
cd drs-core/pkg && pnpm link --global
cd drs-sdk && pnpm link --global @drs/wasm

MCP Middleware Integration

Add DRS verification to an MCP server. The Go middleware handles all verification — your tool handler code does not change.

How it works

MCP Client
    │  POST /mcp/tools/call
    │  X-DRS-Bundle: <base64url bundle>
    ▼
drs-verify middleware
    │  parse bundle
    │  run verify_chain (blocks A–F)
    ▼ VALID
Your tool handler
    ▼
Response + drs:tool-call event emitted

If verification fails, the middleware returns 403 with a JSON error body before your handler is called.

Option A: Go middleware (native)

If your MCP server is in Go, import the middleware package directly:

package main

import (
    "net/http"
    "time"

    "github.com/yourorg/drs/drs-verify/pkg/middleware"
    "github.com/yourorg/drs/drs-verify/pkg/resolver"
    "github.com/yourorg/drs/drs-verify/pkg/verify"
)

func main() {
    res, _ := resolver.New(10_000, time.Hour)
    verifier := verify.New(res)

    mcp := middleware.NewMCP(verifier, middleware.MCPConfig{
        RequireBundle: true,   // 403 on missing X-DRS-Bundle header
        EmitEvents:    true,   // emit drs:tool-call events
    })

    mux := http.NewServeMux()
    mux.Handle("/mcp/", mcp.Wrap(yourMCPHandler))
    http.ListenAndServe(":8080", mux)
}

Option B: Sidecar (any language)

Run drs-verify as a sidecar in front of your server. It listens on :8081 and proxies verified requests to your server on :8080:

docker run -d \
  --name drs-verify \
  --network host \
  -e DRS_LISTEN_ADDR=:8081 \
  -e DRS_UPSTREAM=http://localhost:8080 \
  -e DRS_REQUIRE_BUNDLE=true \
  ghcr.io/okeyamy/drs-verify:latest

Point your MCP client at :8081. Your server on :8080 only receives requests that have passed full verification.

Option C: TypeScript VerifyClient

For TypeScript MCP servers that need fine-grained control:

import { VerifyClient, parseBundle } from '@drs/sdk';

const drs = new VerifyClient({ baseUrl: process.env.DRS_VERIFY_URL! });

app.post('/mcp/tools/call', async (req, res) => {
  const bundleHeader = req.headers['x-drs-bundle'] as string;

  if (!bundleHeader) {
    return res.status(403).json({ error: 'DRS bundle required' });
  }

  const result = await drs.verify(bundleHeader);
  if (!result.valid) {
    return res.status(403).json({
      error: result.error,
      block: result.block,
      message: result.message,
    });
  }

  // result.context contains:
  //   root_principal, subject, chain_depth, policy_result
  return executeTool(req.body, result.context);
});

Testing your integration

# Valid bundle — expect 200
DRS_VERIFY_URL=http://localhost:8081 pnpm exec drs verify bundle.json

# Missing bundle — expect 403
curl -X POST http://localhost:8081/mcp/tools/call \
  -H "Content-Type: application/json" \
  -d '{"tool":"web_search","query":"test"}'
# {"error":"DRS bundle required"}

# Tampered bundle — expect 403
curl -X POST http://localhost:8081/mcp/tools/call \
  -H "X-DRS-Bundle: $(echo 'corrupt' | base64)" \
  -H "Content-Type: application/json" \
  -d '{"tool":"web_search","query":"test"}'
# {"error":"BUNDLE_INCOMPLETE","block":"A","message":"..."}

A2A Middleware Integration

DRS integrates with the Agent-to-Agent (A2A) protocol using the same bundle mechanism as MCP.

Transport

The DRS bundle is transmitted as the X-DRS-Bundle header in A2A requests, identical to MCP:

POST /a2a/tasks/send HTTP/1.1
X-DRS-Bundle: <base64url bundle>
Content-Type: application/json

The bundle must contain the full delegation chain from the original human root through every agent hop to the current caller.

Go middleware

import "github.com/yourorg/drs/drs-verify/pkg/middleware"

a2a := middleware.NewA2A(verifier, middleware.A2AConfig{
    RequireBundle: true,
    HeaderName:    "X-DRS-Bundle",
})

mux.Handle("/a2a/", a2a.Wrap(yourA2AHandler))

Sidecar for non-Go A2A servers

docker run -d \
  -e DRS_LISTEN_ADDR=:8082 \
  -e DRS_UPSTREAM=http://localhost:8080 \
  -e DRS_REQUIRE_BUNDLE=true \
  ghcr.io/okeyamy/drs-verify:latest

Chain depth in multi-agent A2A topologies

In A2A deployments, an orchestrator agent may dispatch to multiple worker agents. Each worker must carry the full chain from the human root to itself:

Human → Orchestrator DR → Worker DR → Invocation

The Worker's bundle must include:

  • receipts[0]: Root DR (human → orchestrator)
  • receipts[1]: Sub-DR (orchestrator → worker)
  • invocation: Invocation receipt (worker → tool server)

The Orchestrator issues the Sub-DR to the Worker before dispatching the task. The Worker creates the Invocation Receipt.

Human Consent Records

When a human grants authority, the root DR must include a drs_consent record proving the user saw and approved the policy in human-readable form.

Why it matters

The policy field in a DR is machine-readable JSON. A user who clicks "Allow" on a form that shows them {"max_cost_usd":50} has not meaningfully consented — they have not understood what they approved.

The drs_consent.policy_hash is the SHA-256 of the human-readable text the user actually saw. Auditors can verify that the consent UI displayed legible information, not raw JSON.

Generating the human-readable text

Use the SDK's translatePolicy function:

import { translatePolicy } from '@drs/sdk';

const humanText = translatePolicy({
  allowed_tools: ['web_search', 'write_file'],
  max_cost_usd: 50.00,
  pii_access: false,
  write_access: false,
}, { locale: 'en-GB' });

// Output:
// Research Agent wants permission to:
// ✓  Search the web
// ✓  Save files to your workspace
// ✗  Cannot access personal data
// ✗  Cannot spend more than £50.00

Or via CLI:

echo '{"allowed_tools":["web_search"],"max_cost_usd":50}' | pnpm exec drs translate --locale en-GB

Computing the policy hash

import { computeChainHash } from '@drs/sdk';

// Hash the text the user saw — not the policy JSON
const policyHash = computeChainHash(humanText);
// "sha256:a1b2c3..."
import { issueRootDelegation, computeChainHash, translatePolicy } from '@drs/sdk';

const policy = {
  allowed_tools: ['web_search'],
  max_cost_usd: 50.00,
  pii_access: false,
};

// 1. Translate for the user
const humanText = translatePolicy(policy, { locale: 'en-GB' });

// 2. Show humanText in your consent UI
// await showConsentDialog(humanText);  — user clicks Allow

// 3. Record their consent
const rootDR = await issueRootDelegation({
  // ... other params ...
  policy,
  rootType: 'human',
  consent: {
    method: 'explicit-ui-click',
    timestamp: new Date().toISOString(),
    session_id: 'sess:' + crypto.randomUUID(),
    policy_hash: computeChainHash(humanText),
    locale: 'en-GB',
  },
});
ValueWhen to use
explicit-ui-clickUser clicked an "Allow" or "I agree" button
explicit-ui-checkboxUser checked a checkbox next to each permission
api-delegationProgrammatic delegation (organisation-rooted, no human interaction)
operator-policyAutomated-system root — no human involved

Machine-rooted delegations

If rootType is "automated-system" or "organisation", the consent field is optional. These root types are used by operators delegating to their own agents without per-session human approval.

const rootDR = await issueRootDelegation({
  // ...
  rootType: 'automated-system',
  // consent: not required
});

Sub-Delegation

Sub-delegations allow an agent to pass a subset of its authority to another agent. The child policy must not escalate beyond the parent — this is the Principle of Least Authority (POLA).

Attenuation rules

Parent policy fieldChild constraint
allowed_tools: [A, B, C]Child allowed_tools must be ⊆ {A, B, C}
max_cost_usd: 50Child max_cost_usd must be ≤ 50
pii_access: falseChild must have pii_access: false
write_access: falseChild must have write_access: false
max_calls: 100Child max_calls must be ≤ 100
exp: TChild exp must be ≤ T
nbf: TChild nbf must be ≥ T

Violation at issuance throws DrsError: POLICY_ESCALATION. Violation in a received bundle fails Block D of verify_chain.

Example: narrowing authority

import { issueSubDelegation } from '@drs/sdk';

const parentPolicy = {
  allowed_tools: ['web_search', 'write_file', 'read_file'],
  max_cost_usd: 50.00,
  pii_access: false,
  write_access: true,
};

// Agent narrows authority before delegating
const subDR = await issueSubDelegation({
  signingKey:   agentPrivateKey,
  issuerDid:    'did:key:z6MkAgent1...',
  subjectDid:   'did:key:z6MkHuman...',   // always the original human
  audienceDid:  'did:key:z6MkAgent2...',
  cmd: '/mcp/tools/call',
  policy: {
    allowed_tools: ['web_search'],         // ⊆ parent's [web_search, write_file, read_file]
    max_cost_usd:  5.00,                   // ≤ parent's 50
    pii_access:    false,                  // same (can't relax)
    write_access:  false,                  // tightened: parent allowed true
  },
  nbf:          parentNbf,                 // ≥ parent's nbf
  exp:          parentNbf + 3600,          // ≤ parent's exp
  parentJwt:    parentDR,
  parentPolicy: parentPolicy,
  parentNbf:    parentNbf,
  parentExp:    parentExp,
});

What happens if you escalate?

// This throws POLICY_ESCALATION:
await issueSubDelegation({
  policy: {
    allowed_tools: ['web_search', 'write_file', 'execute_code'],  // added execute_code
    max_cost_usd: 100,                                             // exceeded parent's 50
  },
  // ...
});
// DrsError: POLICY_ESCALATION — allowed_tools contains execute_code not in parent policy

The error fires before any signing. Invalid chains cannot be created.

The sub field never changes

The sub (subject) field represents the original resource owner — always the human at the root of the chain. It must remain identical through every sub-delegation:

rootDR.sub  = "did:key:z6MkHuman..."
subDR.sub   = "did:key:z6MkHuman..."   ← same
inv.sub     = "did:key:z6MkHuman..."   ← same

Changing sub in a sub-delegation is a structural error caught by Block B.

Deploy drs-verify

drs-verify is a single static Go binary with no runtime dependencies. It compiles with CGO_ENABLED=0 and runs in a distroless container.

docker pull ghcr.io/okeyamy/drs-verify:latest

docker run -d \
  --name drs-verify \
  -p 8080:8080 \
  -e LISTEN_ADDR=:8080 \
  -e DID_CACHE_SIZE=10000 \
  -e DID_CACHE_TTL_SECS=3600 \
  -e STATUS_LIST_BASE_URL=https://status.example.com \
  -e STATUS_CACHE_TTL_SECS=300 \
  -e DRS_ADMIN_TOKEN=your-secret-token \
  ghcr.io/okeyamy/drs-verify:latest

For persistent receipt storage, mount a host or named volume and point STORE_DIR at it:

docker run -d \
  --name drs-verify \
  -p 8080:8080 \
  -e LISTEN_ADDR=:8080 \
  -e STORE_DIR=/var/lib/drs \
  -v drs-verify-data:/var/lib/drs \
  ghcr.io/okeyamy/drs-verify:latest

Build from source

cd drs-verify
CGO_ENABLED=0 GOOS=linux go build -o drs-verify ./cmd/server
./drs-verify
# drs-verify listening on :8080

Health checks

# Liveness
curl http://localhost:8080/healthz
# {"status":"ok"}

# Readiness (includes cache state)
curl http://localhost:8080/readyz
# {"status":"ready"}

Configure your load balancer or Kubernetes probe to use /readyz — it only returns ok when the server is fully initialised.

Kubernetes deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: drs-verify
  labels:
    app: drs-verify
spec:
  replicas: 3
  selector:
    matchLabels:
      app: drs-verify
  template:
    metadata:
      labels:
        app: drs-verify
    spec:
      containers:
      - name: drs-verify
        image: ghcr.io/okeyamy/drs-verify:latest
        ports:
        - containerPort: 8080
        env:
        - name: LISTEN_ADDR
          value: ":8080"
        - name: DID_CACHE_SIZE
          value: "10000"
        - name: DID_CACHE_TTL_SECS
          value: "3600"
        - name: STATUS_LIST_BASE_URL
          value: "https://status.example.com"
        - name: STATUS_CACHE_TTL_SECS
          value: "300"
        - name: DRS_ADMIN_TOKEN
          valueFrom:
            secretKeyRef:
              name: drs-secrets
              key: admin-token
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /readyz
            port: 8080
          initialDelaySeconds: 3
          periodSeconds: 5
        resources:
          requests:
            memory: "64Mi"
            cpu: "100m"
          limits:
            memory: "128Mi"
            cpu: "500m"

Sidecar pattern

Running drs-verify as a sidecar that proxies requests to an upstream MCP server is a planned deployment mode. It is not implemented in the current release. For now, configure your MCP server to call POST /verify directly before accepting tool-call requests.

Operator Configuration

Machine-to-machine deployments (no live human in the delegation loop) use an OperatorConfig loaded at startup. This governs how the operator issues root delegations, manages key material, and handles out-of-policy requests.

Configuration file format

{
  "drs_root_type": "automated-system",
  "operator_did": "did:key:z6MkOperator...",
  "operator_key_management": "env",
  "standing_policy": {
    "allowed_tools": ["web_search", "write_file", "read_file"],
    "max_cost_usd": 100.00,
    "pii_access": false,
    "write_access": true
  },
  "renewal_rules": {
    "auto_renew": true,
    "session_ttl_hours": 8,
    "max_renewal_count": 0
  },
  "escalation": {
    "target_type": "organisation",
    "supervisor_did": "did:key:z6MkSupervisor...",
    "fallback": "deny"
  },
  "storage_tier": 2
}

Load in TypeScript:

import { parseOperatorConfig } from '@drs/sdk';
import { readFileSync } from 'fs';

const cfg = parseOperatorConfig(
  JSON.parse(readFileSync('operator-config.json', 'utf-8'))
);
// Throws DrsError: INVALID_OPERATOR_CONFIG if any field is invalid

Key management options

operator_key_managementWhere the key lives
"env"DRS_OPERATOR_KEY environment variable (base64url-encoded 32 bytes)
"file"Path in operator_key_path — raw 32-byte Ed25519 key file
"aws-kms"AWS KMS — requires DRS_KMS_KEY_ID env var
"gcp-kms"GCP Cloud KMS — requires DRS_GCP_KEY_NAME env var

Security: Never use "file" or "env" in production with keys that have regulatory significance. Use "aws-kms" or "gcp-kms" for production operator keys. See Key Management.

Root type

drs_root_typeWhen to use
"automated-system"Fully automated operator — no human consent loop
"organisation"Represents an organisation that has a defined approval process

Human-rooted delegations ("human") are not used in OperatorConfig — they require a live human consent interaction per session.

Renewal rules

FieldDescription
auto_renewIf true, the agent runtime renews the session delegation before it expires
session_ttl_hoursHow long each session delegation is valid
max_renewal_countMaximum renewals per original session. 0 = unlimited

Escalation behaviour

When an agent requests an action exceeding the standing_policy:

  1. Request is held (not rejected immediately)
  2. Notification sent to supervisor_did
  3. If supervisor approves: a new sub-DR is issued with expanded policy
  4. If supervisor does not respond within timeout: fallback applies
fallbackBehaviour
"deny"Request is rejected — safe default
"allow-degraded"Request is allowed with a degraded policy — only use when availability is more critical than strict policy enforcement

Security: "allow-degraded" should never be the default in regulated deployments. Discuss with your security team before enabling it.

Storage Tiers

DRS defines four storage tiers for delegation receipts, ordered by durability and compliance requirements.

Tier reference

TierNameBackendEnv vars requiredUse case
0In-memoryLRU cache (process lifetime)(none — default)Development, testing
1FilesystemLocal diskSTORE_DIRStandard production
3WORM + RFC 3161Filesystem + trusted timestampSTORE_DIR + TSA_URLRegulated deployments (HIPAA, EU AI Act, financial)
4BlockchainTier 3 + on-chain anchor (not yet implemented)(not yet implemented)Blockchain-native enterprise opt-in

Note on Tier 2: There is no Tier 2 in the current implementation. S3 or other object storage backends are a roadmap item.

When to use each tier

Tier 0 (In-memory): Default when STORE_DIR is not set. Receipts are lost on process restart. Use only for local development and tests.

Tier 1 (Filesystem): Set STORE_DIR to a directory path. Receipts are written as files and survive process restart. No compliance controls — suitable for production deployments where regulatory requirements do not mandate timestamping.

Tier 3 (WORM + RFC 3161): Set both STORE_DIR and TSA_URL. Every stored receipt is timestamped by an RFC 3161 Trusted Signing Authority (TSA). The TSA signs a SHA-256 hash of the stored JWT string with the current time and returns a DER timestamp token. This token is stored alongside the DR.

RFC 3161 (IETF 2001, updated by RFC 5816 in 2010 to add SHA-2 support) is the standard used here. Timestamp tokens are legally recognised under EU eIDAS and in US federal courts. TSA failure is best-effort — if the TSA is unreachable, the receipt is still stored (Tier 1 semantics) and the error is logged. Storage is never blocked by TSA availability.

Tier 4 (Blockchain): Not implemented. When built, this will be an explicit opt-in for customers who require on-chain proof and understand the gas cost implications. The default anchor mechanism is RFC 3161 (Tier 3), not blockchain.

Configuration

# Tier 0 — in-memory (default)
LISTEN_ADDR=:8080 ./drs-verify

# Tier 1 — filesystem
LISTEN_ADDR=:8080 \
  STORE_DIR=/data/drs \
  ./drs-verify

# Tier 3 — filesystem + RFC 3161 trusted timestamp
LISTEN_ADDR=:8080 \
  STORE_DIR=/data/drs \
  TSA_URL=https://freetsa.org/tsr \
  ./drs-verify

For a complete list of environment variables including STATUS_LIST_BASE_URL (required for remote revocation) and DRS_ADMIN_TOKEN (required for POST /admin/revoke), see the Configuration Reference.

TSA providers

ProviderURLCostNotes
FreeTSAhttps://freetsa.org/tsrFreeNon-commercial use; rate-limited
DigiCerthttps://timestamp.digicert.comFree (DigiCert customers)Production-grade
GlobalSignhttp://timestamp.globalsign.com/tsa/r6advanced1CommercialAATL/WebTrust certified. HTTP is correct for TSA — the response is self-verifying (signed by the TSA certificate chain).

Why not blockchain by default?

The core DRS guarantees (tamper-evident receipts, Ed25519 signatures, hash-chained custody) require zero blockchain. The only problem blockchain was solving in the v1/v2 architecture was "immutable third-party timestamp" — and RFC 3161 solves that problem better:

PropertyRFC 3161Blockchain
User pays gas feesNoYes
Latency~200 ms (typical)400 ms–12 s (typical)
Legal recognitionEU eIDAS, US federal courts, ISO 18014Unclear / jurisdiction-dependent
Requires wallet / tokenNoYes
Battle-tested20+ years4 months–10 years depending on chain
Free tierYes (FreeTSA)No

Blockchain anchoring is available as Tier 4 for customers who specifically require it — for example, blockchain-native enterprises whose compliance teams are already comfortable with on-chain evidence. It is never the default.

Key Management

Key types and requirements

Key typeRecommended storageRotation
Human root keyHardware Security Module or device Secure EnclaveNot rotated — DID is derived from key
Operator root keyHSM required for productionAnnual with overlap period
Agent session keyEphemeral — generated per session, never storedPer-session

Why agent keys must be ephemeral

Long-lived agent private keys are a liability: if the agent is compromised, all delegations signed by that key are at risk. Ephemeral keys limit the blast radius: a compromised key can only affect delegations from the current session, which expire when the session ends.

Generating keys

Development only (never in production):

pnpm exec drs keygen
# Private key: <base64url 32 bytes>
# DID:         did:key:z6Mk...

Production operator key (AWS KMS):

# Create an Ed25519 key in AWS KMS
aws kms create-key \
  --key-spec ECC_NIST_P256 \
  --key-usage SIGN_VERIFY \
  --description "DRS operator key - production"

# Set the key ID in operator config
echo "DRS_KMS_KEY_ID=<key-id>" >> .env

DID method choices

did:key (recommended): The DID is derived directly from the public key. No registry, no DNS, no trust anchor beyond the key itself. The verification key is self-contained in the DID string.

did:web: The DID is resolved by fetching a DID document from an HTTPS URL. Useful when you need to rotate keys without changing the DID (the DID document can be updated). Requires your domain's DNS and TLS to be secure — a compromised domain means a compromised DID.

Key rotation

For did:key DIDs, rotating the key means generating a new key and a new DID. The process:

  1. Generate new key and DID
  2. Update operator_did in your OperatorConfig
  3. New root delegations are issued under the new DID
  4. Old delegations (signed with the previous key) remain valid until they expire
  5. After old delegations expire, the old key can be decommissioned

Protecting signing keys

  • Never log private key material, even in debug builds
  • Never store private keys in environment variables in production (use aws-kms or gcp-kms)
  • Never include private keys in Docker images
  • Never commit keys to version control

Revocation

DRS supports two revocation mechanisms that work together:

  1. Remote Bitstring Status List — W3C standard; fetched from STATUS_LIST_BASE_URL with a configurable TTL cache (default 5 minutes). Revocations take effect within the cache window.
  2. Local revocation store — in-memory; updated immediately via POST /admin/revoke. Takes effect on the next request. Does not survive process restart.

Both are checked in Block F of verify_chain. A DR is revoked if its drs_status_list_index is marked in either source.

Revoking a delegation (immediate effect)

The local revocation store takes effect on the next verification request — no cache window:

curl -X POST http://localhost:8080/admin/revoke \
  -H "Authorization: Bearer $DRS_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status_list_index": 42}'

# Response: {"revoked":true,"status_list_index":42}

DRS_ADMIN_TOKEN must be set as an environment variable. If not set, the endpoint responds 503:

{"error": "admin endpoint not configured — set DRS_ADMIN_TOKEN"}

Note: The local revocation store is in-memory. Revocations are lost on process restart. For durable revocation that survives restarts, update the W3C Bitstring Status List served at STATUS_LIST_BASE_URL.

Revoking via the remote Bitstring Status List

To revoke durably (surviving restarts and across multiple drs-verify instances), update the W3C Bitstring Status List at the URL configured in STATUS_LIST_BASE_URL. drs-verify fetches and caches this list; the TTL is STATUS_CACHE_TTL_SECS (default 300 seconds / 5 minutes).

Reducing the cache window

For deployments where faster remote revocation propagation is needed:

STATUS_CACHE_TTL_SECS=30 ./drs-verify

Setting this too low increases load on the status list endpoint. 300 seconds is appropriate for most deployments. For emergency revocations, use POST /admin/revoke for immediate effect alongside updating the remote list.

How Block F works

for each DR in bundle:
  if DR.drs_status_list_index is set:
    if remote_status_list.is_revoked(DR.drs_status_list_index):
      return RECEIPT_REVOKED
    if local_revocation_store.is_revoked(DR.drs_status_list_index):
      return RECEIPT_REVOKED

Status list cache concurrency

The status list cache uses sync.Once internally to prevent double-fetch race conditions under concurrent load. When the cache expires and multiple goroutines arrive simultaneously, only one HTTP request is made — all others wait and reuse the result.

Reconstruct a Delegation Chain

You can reconstruct and verify any delegation chain from stored receipts without operator cooperation. All you need are the JWT strings and the DIDs.

Step 1: Retrieve the bundle

If the operator has provided the bundle file:

pnpm exec drs audit retrieve --inv-jti "inv:7h5c4d3e-..." --output evidence.json

Or assemble manually from JWT strings provided by the operator:

{
  "bundle_version": "4.0",
  "invocation": "<invocation-receipt-jwt>",
  "receipts": [
    "<root-dr-jwt>",
    "<sub-dr-jwt-1>"
  ]
}

Step 2: Verify the chain

pnpm exec drs verify evidence.json

This runs all six verification blocks locally. The public keys are resolved from the DID strings embedded in each JWT's iss field. No network calls to any operator system.

✓ Bundle verified
  Chain depth:    2
  Root principal: did:key:z6MkHuman...
  Subject:        did:key:z6MkHuman...
  Command:        /mcp/tools/call
  Policy result:  pass
  Blocks:         A✓ B✓ C✓ D✓ E✓ F✓

Step 3: Read the full audit trail

pnpm exec drs audit evidence.json

Output:

DRS Chain Audit
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Receipt 0 (root — human)
  Issued by:   did:key:z6MkHuman...
  Granted to:  did:key:z6MkAgent1...
  Command:     /mcp/tools/call
  Policy:      max_cost_usd=50, allowed_tools=[web_search,write_file]
  Valid:        2026-03-28 10:30:00 → 2026-05-28 10:30:00
  Consent:      explicit-ui-click at 2026-03-28T10:30:00Z (locale: en-GB)
  Policy hash:  sha256:a1b2c3...  (hash of text user saw)

Receipt 1 (sub-delegation)
  Issued by:   did:key:z6MkAgent1...
  Granted to:  did:key:z6MkAgent2...
  Policy:      max_cost_usd=5, allowed_tools=[web_search]
  Chain hash:  sha256:def456... ✓

Invocation
  Called by:   did:key:z6MkAgent2...
  Tool:        web_search
  Args:        {"estimated_cost_usd": 0.02, "query": "Monad TPS"}
  Budget used: $0.02 of $5.00 (sub) / $50.00 (root)
  Chain:       sha256:abc123... → sha256:def456... ✓

All 3 signatures valid. Chain intact. No revocations found.

To confirm the user saw human-readable policy (not raw JSON):

pnpm exec drs policy evidence.json --receipt 0

This shows the policy of the root DR. Cross-reference the policy_hash in the consent record with:

pnpm exec drs translate --input-json '{"allowed_tools":["web_search"],"max_cost_usd":50}' \
  | sha256sum

If the SHA-256 of the translated text matches drs_consent.policy_hash, the user saw legible information.

What you can prove

From the DRS chain alone, you can prove:

  • Who authorised the action (the iss of the root DR, with their Ed25519 signature)
  • What they authorised (the policy field at every level)
  • When they authorised it (the nbf, exp, iat fields)
  • What actually happened (the invocation receipt's args field)
  • That consent was meaningful (the drs_consent.policy_hash links to human-readable text)
  • That the chain is intact (all prev_dr_hash values verify, all signatures valid)

You cannot prove these things from server logs alone.

Independent Verification

DRS chains can be verified by anyone, without contacting the operator, without a DRS account, and without any central authority.

What you need

  • The DRS bundle (the JWT strings)
  • The drs verify CLI (from @drs/sdk) or a running drs-verify instance

What you do NOT need

  • Access to the operator's systems or databases
  • A DRS account or subscription
  • Network access to the original issuer
  • Any trusted third party to authenticate the evidence

Why this works

Each Delegation Receipt is signed with the issuer's Ed25519 private key. The issuer's public key is encoded directly in their did:key DID:

did:key:z6Mk{base58btc(multicodec_prefix + public_key_bytes)}

Anyone with the DID can derive the public key and verify the signature. No registry lookup, no HTTP request, no trust anchor beyond the public key.

Offline verification

All blocks except F (revocation) can be run offline:

pnpm exec drs verify bundle.json --offline

The --offline flag skips Block F (Bitstring Status List lookup). All cryptographic, structural, policy, and temporal checks run locally with no network calls.

Use --offline when:

  • You have no network access
  • You are verifying historical evidence (revocation status is not relevant for a past action)
  • You distrust the network path to the status list server

Full online verification

DRS_VERIFY_URL=http://your-drs-verify-instance:8080 pnpm exec drs verify bundle.json

Or spin up your own drs-verify instance and point at it:

cd drs-verify && go run ./cmd/server &
pnpm exec drs verify bundle.json

Checking signatures manually

If you want to verify a signature without the CLI tools, every DRS JWT is a standard EdDSA JWT. Any JWT library that supports alg: EdDSA can verify it:

# Decode and verify using jwt.io or any EdDSA-capable tool
# The DID in the "iss" field encodes the public key:
# did:key:z6Mk{base58(0xed01 + pub_key_bytes)}

# Extract pub key from DID:
echo "did:key:z6Mk..." | pnpm exec drs resolve-did
# {"public_key_hex": "0102030405..."}

Export Evidence for EU AI Act

DRS provides a dedicated export format for EU AI Act compliance evidence. The export covers Article 12 (record-keeping) and Article 13 (transparency) requirements.

Export command

pnpm exec drs audit export \
  --inv-jti "inv:7h5c4d3e-..." \
  --format eu-ai-act \
  --output eu-ai-act-evidence.json

What the export contains

{
  "export_format": "eu-ai-act",
  "export_timestamp": "2026-03-30T10:00:00Z",
  "article_12_evidence": {
    "delegation_chain": [
      {
        "receipt_index": 0,
        "type": "root-delegation",
        "issuer": "did:key:z6MkHuman...",
        "audience": "did:key:z6MkAgent1...",
        "issued_at": "2026-03-28T10:30:00Z",
        "valid_until": "2026-05-28T10:30:00Z",
        "policy_summary": "web_search only, max £50",
        "signature_valid": true,
        "jwt": "<root-dr-jwt>"
      }
    ],
    "invocation": {
      "type": "invocation-receipt",
      "issuer": "did:key:z6MkAgent2...",
      "tool": "web_search",
      "args_summary": "query: Monad TPS benchmarks",
      "cost_usd": 0.02,
      "signature_valid": true,
      "jwt": "<invocation-jwt>"
    },
    "chain_verification": {
      "all_signatures_valid": true,
      "chain_intact": true,
      "no_revocations": true,
      "blocks_passed": ["A", "B", "C", "D", "E", "F"]
    }
  },
  "article_13_evidence": {
    "consent_record": {
      "method": "explicit-ui-click",
      "timestamp": "2026-03-28T10:30:00Z",
      "locale": "en-GB",
      "policy_hash": "sha256:a1b2c3...",
      "policy_text_shown_to_user": "Research Agent wants permission to:\n✓  Search the web\n✗  Cannot spend more than £50.00"
    }
  }
}

Submitting the evidence

Submit eu-ai-act-evidence.json to:

  • Your internal compliance officer for review
  • The notified body conducting a conformity assessment
  • The national market surveillance authority if requested

The evidence is self-contained — no operator cooperation is needed to authenticate it. All signatures are verifiable from the public keys encoded in the DIDs.

Batch export

For a date range of invocations:

pnpm exec drs audit export \
  --subject "did:key:z6MkHuman..." \
  --date-range "2026-01-01/2026-03-31" \
  --format eu-ai-act \
  --output q1-2026-eu-ai-act.json

HIPAA Audit Evidence

For healthcare deployments handling Protected Health Information (PHI), HIPAA §164.312(b) requires audit controls that record and examine activity in information systems containing PHI.

What DRS provides for HIPAA

HIPAA RequirementDRS Mechanism
Record activity in PHI systemsInvocation Receipts — every agent action recorded and signed
Verify authorisation was granted before accessDelegation Receipts — signed proof of authorisation before invocation
Tamper-evident audit trailEd25519 signatures + prev_dr_hash chain
Independent auditabilitydid:key resolution — no central authority needed
Retention (7 years minimum)Tier 3 storage — WORM-enabled with configurable retention

Retrieving evidence for a HIPAA audit

# Retrieve all invocations by an agent that touched PHI systems
pnpm exec drs audit retrieve \
  --tool-server "did:key:z6MkPHISystem..." \
  --date-range "2026-01-01/2026-03-31" \
  --output hipaa-audit-trail.json

# Verify the entire set
pnpm exec drs verify hipaa-audit-trail.json

# Print human-readable audit trail
pnpm exec drs audit hipaa-audit-trail.json

Required storage configuration for HIPAA

PHI-touching deployments must use Tier 3 storage:

DRS_STORAGE_TIER=3
DRS_S3_WORM_POLICY=true
DRS_RETENTION_DAYS=2555    # 7 years = 365 × 7

Or in OperatorConfig:

{
  "storage_tier": 3,
  "drs_regulatory": {
    "frameworks": ["hipaa-164.312b"],
    "retention_days": 2555
  }
}

Demonstrating authorisation for BAA compliance

Under a Business Associate Agreement (BAA), you must demonstrate that agent access to PHI was:

  1. Authorised by an identified individual (the iss of the root DR, with valid signature)
  2. Within the scope of the authorisation (Block D policy compliance)
  3. Logged with a tamper-evident record (the signed invocation receipt)
# Export HIPAA evidence for a specific invocation
pnpm exec drs audit export \
  --inv-jti "inv:7h5c4d3e-..." \
  --format hipaa \
  --output hipaa-single-event.json

The output includes: delegation chain with signatures, invocation record, verification result, and retention metadata.

Development Setup

Prerequisites

Install all three language toolchains:

# Rust 1.77+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup install stable

# Go 1.22+
# Download from https://go.dev/dl/ or via your package manager

# Node.js 20+ and pnpm
# Node: https://nodejs.org or nvm
npm install -g pnpm

# wasm-pack (for WASM builds)
cargo install wasm-pack

Clone and build

git clone https://github.com/yourorg/drs
cd drs

# Rust core
cd drs-core
cargo build
cargo test
cd ..

# Go middleware
cd drs-verify
go build ./...
go test ./... -race
cd ..

# TypeScript SDK
cd drs-sdk
pnpm install
pnpm test
pnpm typecheck
cd ..

Optional: WASM build

cd drs-core
wasm-pack build --target web --features wasm
# Output: drs-core/pkg/

Run all tests

# Rust
cd drs-core && cargo test

# Go (with race detector and coverage)
cd drs-verify && go test ./... -race -coverprofile=coverage.out
go tool cover -html=coverage.out

# TypeScript
cd drs-sdk && pnpm test
cd drs-sdk && pnpm typecheck

Formatting and linting

# Rust
cd drs-core && cargo fmt && cargo clippy

# Go
cd drs-verify && gofmt -w . && go vet ./...

# TypeScript
cd drs-sdk && pnpm prettier --write .

CI enforces all formatters. cargo fmt --check, gofmt -l ., and pnpm prettier --check . must all pass.

Running drs-verify locally

cd drs-verify
go run ./cmd/server
# drs-verify listening on :8080

# In another terminal:
curl http://localhost:8080/healthz
# {"status":"ok"}

IDE setup

VS Code: Install the rust-analyzer, Go, and TypeScript extensions. The project includes workspace settings that configure formatters.

IntelliJ / GoLand: The Go module layout is standard — open drs-verify/ as a Go module.

Architecture Deep Dive

Read this before touching the crypto layer or the verification path.

Required reading

Before making changes to the core verification logic:

  1. docs/Drs_language&algorithms.md — authoritative reference for language choices and corrected algorithms
  2. docs/Drs_architecture_v2.md — the full DRS 4.0.0 specification
  3. False Positives: What We Tried — the v1 and v2 failures

Module boundaries

Each module has exactly one responsibility. Do not write code that crosses these boundaries:

ModuleResponsibilityDoes NOT do
drs-core/src/crypto/Ed25519 sign/verify, SHA-256JWT parsing, policy evaluation
drs-core/src/chain/Chain hash computation, verify_chainNetwork I/O, caching
drs-core/src/jcs/RFC 8785 canonicalisationSerialisation to non-JSON formats
drs-core/src/capability/Policy evaluation, attenuation checkCrypto operations
drs-core/src/did/did:key decode to public key bytesDID resolution with caching
drs-verify/pkg/resolver/DID resolution + LRU cacheChain verification
drs-verify/pkg/verify/verify_chain (6 blocks)DID resolution, HTTP I/O
drs-verify/pkg/middleware/HTTP request/response handlingVerification logic
drs-verify/pkg/policy/Policy field evaluationSigning, serialisation
drs-sdk/src/sdk/Issuance (sign + build JWTs)Verification
drs-sdk/src/verify/HTTP client to drs-verifyVerification logic itself
drs-sdk/src/cli/CLI command dispatchSDK logic

Data flow

Issuance (TypeScript SDK):
  Policy params → jcsSerialise(payload) → ed.signAsync → JWT string

Verification (Go, calls Rust):
  JWT string → parse header/payload → resolve_did → ed25519_verify_strict
            → check_policy_attenuation → is_revoked → VerifiedChain

DID resolution (Go):
  DID string → base58Decode → strip multicodec prefix [0xed, 0x01]
             → [32]byte public key (constant-time prefix check)

Adding a new algorithm

  1. Write it in Rust first (drs-core/src/)
  2. Write tests with official test vectors from the relevant RFC
  3. If Go middleware needs it: re-implement with the same interface (Go cannot call Rust without CGO complexity — prefer reimplementation for simple algorithms)
  4. TypeScript only calls WASM or the HTTP API — no algorithm logic in TypeScript

Security-sensitive code checklist

Before merging any change to the crypto or verification path:

  • Comparisons on key material use constant-time equality (subtle::ConstantTimeEq / crypto/subtle.ConstantTimeCompare)
  • No unwrap() in Rust library code — use Result<T, E> and propagate
  • No _ on error returns in Go production paths — check and propagate
  • Signing keys are not logged, even in debug
  • New error messages do not expose key material or sensitive internal state

Testing Standards

Every source file has a corresponding test file. Tests are not optional and are not written after the fact.

File structure

# Rust
drs-core/src/chain/verify.rs
drs-core/src/chain/verify_test.rs   ← or #[cfg(test)] module inline

# Go
drs-verify/pkg/verify/chain.go
drs-verify/pkg/verify/chain_test.go ← same package, _test.go suffix

# TypeScript
drs-sdk/src/sdk/issue.ts
drs-sdk/src/sdk/issue.test.ts

What must be tested

For every module, write tests covering:

Happy path: Expected input produces expected output.

Boundary conditions:

  • Empty input (empty chain, empty policy, empty args)
  • Maximum chain depth (10 hops)
  • Expired timestamps (now > exp)
  • Not-yet-valid timestamps (now < nbf)

Error paths: Every Err(...), error return, and DrsError throw has at least one test that triggers it.

Security properties — these are non-negotiable:

  • Signature forgery must fail Block C
  • Policy escalation must fail checkPolicyAttenuation and Block D
  • Revoked chains must fail Block F
  • Tampered DRs (modified payload) must fail Block C
  • Chain splicing (wrong prev_dr_hash) must fail Block B
  • Temporal violations (sub-DR exp > parent exp) must fail Block E

RFC test vectors

jcs_canonicalise and compute_cid must pass all official RFC 8785 test vectors. These are non-negotiable:

#![allow(unused)]
fn main() {
#[test]
fn test_jcs_rfc8785_vector_1() {
    // From RFC 8785 Appendix B
    let input = r#"{"b":1,"a":2}"#;
    let expected = r#"{"a":2,"b":1}"#;
    assert_eq!(jcs_canonicalise(input).unwrap(), expected);
}
}

Canonicalisation divergence between implementations breaks cross-implementation JWT verification. These tests are a compatibility guarantee, not just unit tests.

Running tests

# Rust — with coverage
cargo test
cargo tarpaulin --out Html   # requires cargo-tarpaulin

# Go — with race detector
go test ./... -race -coverprofile=coverage.out
go tool cover -html=coverage.out

# TypeScript
pnpm test                  # vitest run
pnpm test -- --reporter=verbose

Test naming convention

Name tests to describe the property being tested, not the implementation:

# Good
TestSignatureForgeryShouldFail
TestPolicyEscalationRejectedAtIssuance
TestExpiredReceiptFailsTemporalBlock

# Bad
TestVerifyChain
TestIssueSubDelegation
TestBlock3

When a test fails, the name should tell you what property was violated.

False Positives: What We Tried

This is a research project. Three architectural approaches were designed and discarded before the current architecture. This page is preserved as a record of what failed and why — essential context for anyone contributing to the codebase.

"Those who cannot remember the past are condemned to repeat it." — George Santayana

v1: Invented from scratch

The hypothesis: We need a new delegation chain system for AI agents.

What we built: A custom delegation chain with Ed25519 signatures and a binary Merkle tree over the chain.

The errors:

Error 1 — Reinvented the wheel: UCAN (User Controlled Authorization Network) already defines cryptographic delegation chains. We built something equivalent but worse, without the benefit of the prior art and the community that had already reviewed it.

Error 2 — Wrong data structure: We applied a binary Merkle tree to a linear delegation chain. A linear chain is not a tree. The Merkle tree added complexity without benefit, and it introduced the same last-node duplication vulnerability as Bitcoin's transaction Merkle tree (CVE-2012-2459).

Error 3 — "Ed25519 is simply secure": This is not a security model. A security model names the threats, the mechanisms that counter them, and the residual risks. "Simply secure" means nothing to an auditor.

The lesson: Check whether the problem is already solved before designing a solution. Read the existing standards. Read the CVE history of similar approaches.

v1 documents: docs/DRS_architecture_v1.md


v2: UCAN Profile (wrong version)

The hypothesis: DRS should be a UCAN Profile — extend UCAN rather than invent something new. This was the correct insight.

What we built: A UCAN 0.x Profile with JWT-based delegation tokens.

The errors:

Error 1 — Wrong spec version: We built against UCAN 0.x (JWT-based, att.nb policy field) while the actual current specification was UCAN v1.0-rc.1 (CBOR/IPLD-based, cmd/pol policy). We discovered this when we tried to validate against real UCAN implementations.

Error 2 — TypeScript for verification: Verification is a high-frequency, latency-sensitive operation. V8 (Node.js) has GC pauses of 50–1500ms. The <5ms p99 latency requirement was mathematically impossible with TypeScript on the verification path. We measured 120ms median verification time under moderate load.

Error 3 — Unbounded DID resolver cache: The cache grew without bound under agent churn. With 10,000 active agents, the cache consumed >640MB. There was no eviction policy.

Error 4 — Status list race condition: Two concurrent requests arriving when the status list cache expired could both issue HTTP GET requests to the status list server. This is a double-fetch race condition. Under load, this caused 2–10× the expected traffic on the status list endpoint.

Error 5 — O(n·m) policy check: is_attenuated_subset() iterated over all n fields in the parent policy and all m fields in the child policy. At moderate load (1,000 req/sec with 5-level chains), this produced 25 million comparisons per second.

Error 6 — Wrong canonicalisation: We used JCS on JSON for JWT signing, but UCAN v1.0 uses CBOR encoding, not JSON. The JWT payloads were valid JSON but the wrong format for the specification we were implementing against.

The lesson: Read the specification you are implementing against before writing any code. Check the encoding format. Check the version number. Validate against a reference implementation early.

v2 documents: docs/Drs_architecture_v2.md


v3/v4: OAuth 2.1 Profile (current)

The pivot: From UCAN to OAuth 2.1.

Why the pivot: The ecosystem standardised on OAuth. AT Protocol chose JWT + OAuth. MCP chose JWT + OAuth. UCAN's production adoption is near-zero (Storacha is the only known production deployment). Building on UCAN would have meant building for a standard that the target ecosystem does not use.

What changed:

  • UCAN → OAuth 2.1 + RFC 8693 as the base layer
  • TypeScript verification → Go verification server (goroutines, predictable GC)
  • Unbounded cache → golang-lru/v2 with hard cap of 10,000 entries
  • Race condition → sync.Once on status list fetch
  • O(n·m) policy check → capability index with O(1) average lookup
  • JSON/CBOR confusion → JWT throughout, JCS canonicalisation for signing

What stayed the same:

  • Ed25519 signatures (correct from the start)
  • The concept of per-step delegation receipts (correct from v2)
  • The chain hash linking mechanism (correct from v2, minus the Merkle tree)
  • The five-actor model (refined from v1)

The current architecture is not a revolution — it is the same core idea implemented correctly, on the right base layer, in the right languages.

Upstream Monitoring

DRS depends on external specifications and libraries that evolve. Upstream changes can break assumptions baked into our architecture without any change to the DRS codebase.

What to watch

SourceWhat to watchWhy it matters
ed25519-dalek (Rust)RUSTSEC advisories, 2.x API changesCore signing/verification library — RUSTSEC-2022-0093 is the reference for why we use 2.x
serde-json-canonicalizerRFC 8785 compliance updates, new test vectorsJCS divergence breaks cross-implementation JWT verification
golang-lru/v2API changes, eviction policy updatesDID resolver cache — eviction semantics affect security properties
W3C DID Core / did:keyMulticodec prefix changes, new key type supportThe [0xed, 0x01] prefix check is hard-coded — any change breaks all DID resolution
golang-jwt/jwt v5API changes, new algorithm supportJWT parsing in the Go verification server
MCP (Model Context Protocol)Middleware adapter interface changes, new transport typesX-DRS-Bundle header integration — transport changes affect bundle delivery
A2A (Agent-to-Agent Protocol)Interceptor interface changesA2A middleware integration
IETF OAuth WGRFC 8693 updates, new chain-splicing guidanceDRS is positioned as RFC 8693 mitigation #3 — spec changes affect our positioning
W3C Bitstring Status ListSpec changes to revocation formatBlock F implementation

When you notice a change

  1. Stop. Do not silently update the dependency or adapt the code.
  2. Open a GitHub issue with this format:
UPSTREAM CHANGE DETECTED

Source: <spec/crate/library name>
Version: <old version> → <new version>
What changed: <one sentence>
DRS impact: <which layer(s) and files are affected>
Recommended action: <what we need to decide>
Reference: <URL to release notes, advisory, or spec section>
  1. Wait for the maintainer to confirm before incorporating the change.
  2. Once confirmed, discuss architecture impact before writing any code.

The upstream drift lesson

v2 failed partly because it was built against UCAN 0.x while the actual specification was UCAN v1.0-rc.1. The difference between versions was not cosmetic — it was a complete change in encoding format (JSON → CBOR) and policy language (att.nbcmd/pol).

Upstream drift caught during development is a discussion. Upstream drift caught in production is a vulnerability or a broken implementation.

Subscribing to security advisories

# Watch for Rust security advisories
cargo install cargo-audit
cargo audit   # run periodically in CI

# Go vulnerability database
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...

Both cargo audit and govulncheck should run in CI on every PR that touches dependencies.

JWT Fields Reference

All DRS JWTs use the header {"alg":"EdDSA","typ":"JWT"} with keys sorted by Unicode code point (RFC 8785 JCS). The signature covers base64url(header).base64url(payload).

Delegation Receipt payload

FieldTypeRequiredConstraintsDescription
issDID stringYesValid did:key or did:webIssuer — the party granting delegation
subDID stringYesMust match root DR's sub at every hopSubject — the original resource owner; never changes through chain hops
audDID stringYesValid DIDAudience — the party receiving the delegation
drs_vstringYesMust be "4.0"DRS specification version
drs_typestringYesMust be "delegation-receipt"JWT type discriminator
cmdstringYesNon-emptyMCP command path, e.g. /mcp/tools/call
policyobjectYesSee Policy SchemaCapability constraints
nbfintegerYesUnix seconds; ≥ parent's nbf in sub-DRsNot-before — when the delegation becomes valid
expinteger or nullYesUnix seconds; ≤ parent's exp in sub-DRs when both setExpiry — null for standing delegations
iatintegerYesUnix secondsIssued-at time
jtistringYesFormat: dr: + UUID v4Unique identifier for revocation lookup
prev_dr_hashstring or nullYesFormat: sha256:{64 hex chars} or nullHash of previous DR's JWT bytes; null at chain root
drs_consentobjectWhen drs_root_type is "human"See belowHuman consent evidence
drs_root_typestringYes on root DR"human" | "organisation" | "automated-system"Trust anchor type; absent on sub-DRs
drs_regulatoryobjectNoSee belowStorage tier and retention requirements
drs_status_list_indexintegerNoNon-negativePosition in Bitstring Status List; absent if revocation not used

Invocation Receipt payload

FieldTypeRequiredConstraintsDescription
issDID stringYesMust match last DR's audIssuer — the agent making the call
subDID stringYesMust match root DR's subSubject — the original human
drs_vstringYesMust be "4.0"DRS spec version
drs_typestringYesMust be "invocation-receipt"Type discriminator
cmdstringYesMust match all DR cmd fieldsMCP command path
argsobjectYesEvaluated against all DR policiesActual invocation arguments
dr_chainstring[]YesLength = number of DRs; each sha256:{hex}Ordered hashes of every DR in the chain
tool_serverDID stringYesValid DIDDID of the tool server
iatintegerYesUnix secondsIssued-at time
jtistringYesFormat: inv: + UUID v4Unique identifier

ConsentRecord object

FieldTypeRequiredDescription
methodstringYes"explicit-ui-click" | "explicit-ui-checkbox" | "api-delegation" | "operator-policy"
timestampISO 8601 stringYesWhen the user consented
session_idstringYesSession identifier, prefixed sess:
policy_hashstringYessha256:{hex} of the human-readable policy text the user saw
localeIETF language tagYesLanguage of the consent UI (e.g. en-GB, fr-FR)

RegulatoryMetadata object

FieldTypeDescription
frameworksstring[]Regulatory frameworks: "eu-ai-act-art13", "hipaa-164.312b", "sox", "finos-tier3"
risk_levelstring"unacceptable" | "high" | "limited" | "minimal"
retention_daysintegerMinimum retention in days (0 = forever)

DRS Bundle

{
  "bundle_version": "4.0",
  "invocation": "<invocation-receipt-jwt>",
  "receipts": ["<root-dr-jwt>", "<sub-dr-jwt-1>"]
}

Transmitted as X-DRS-Bundle: base64url({bundle_json}) HTTP header.

Policy Schema

The policy object defines capability constraints on a delegation. All fields are optional — an absent field means no constraint for that dimension.

Fields

FieldTypeDefaultDescription
allowed_toolsstring[]UnrestrictedAllowlist of MCP tool names. If absent, all tools are permitted.
max_cost_usdnumberUnlimitedMaximum USD cost per invocation. Checked against args.estimated_cost_usd.
pii_accessbooleanfalseWhether access to personally identifiable information is permitted.
write_accessbooleanfalseWhether write operations are permitted.
max_callsintegerUnlimitedMaximum total invocations. Tracked by the agent runtime, not enforced by verify_chain.
allowed_resourcesstring[]UnrestrictedAllowlist of resource URIs.

Policy evaluation (Block D)

Policies are evaluated conjunctively: every policy in the chain must pass. The invocation args are checked against every DR's policy from root to the immediate sub-DR.

For each DR's policy:

PASS if:
  (policy.allowed_tools is absent) OR (args.tool ∈ policy.allowed_tools)
  AND
  (policy.max_cost_usd is absent) OR (args.estimated_cost_usd ≤ policy.max_cost_usd)
  AND
  (policy.pii_access is true) OR (args.pii_access is false or absent)
  AND
  (policy.write_access is true) OR (args.write_access is false or absent)

Attenuation rules

Sub-delegation policies must be strict subsets (attenuation) of the parent:

Parent fieldChild constraint
allowed_tools: [A, B, C]Child allowed_tools{A, B, C} — cannot add new tools
max_cost_usd: NChild max_cost_usdN — cannot increase limit
pii_access: falseChild must have pii_access: false — cannot re-enable
write_access: falseChild must have write_access: false — cannot re-enable
max_calls: NChild max_callsN — cannot increase limit

Example policies

Minimal (research agent, read-only):

{
  "allowed_tools": ["web_search", "read_file"],
  "max_cost_usd": 10.00,
  "pii_access": false,
  "write_access": false
}

Operator standing policy (automated system):

{
  "allowed_tools": ["web_search", "write_file", "read_file", "execute_code"],
  "max_cost_usd": 500.00,
  "pii_access": false,
  "write_access": true,
  "max_calls": 10000
}

Single-tool sub-delegation (tight):

{
  "allowed_tools": ["web_search"],
  "max_cost_usd": 1.00,
  "pii_access": false,
  "write_access": false,
  "max_calls": 10
}

Error Codes

SDK errors (TypeScript)

These errors are thrown by the SDK during issuance. They fire before any signing occurs — invalid chains cannot be created.

CodeThrown byDescriptionFix
MISSING_CONSENTissueRootDelegationHuman-rooted delegation issued without a consent fieldAdd consent to issueRootDelegation params
POLICY_ESCALATIONissueSubDelegationChild policy field exceeds parent constraintReduce the escalating field to be within parent bounds
TEMPORAL_BOUNDS_VIOLATIONissueSubDelegationnbf < parentNbf or exp > parentExpAdjust temporal bounds to be nested within parent bounds
INVALID_OPERATOR_CONFIGvalidateOperatorConfig, parseOperatorConfigMissing required field or invalid valueCheck the field named in the error message

Verification errors (Go — returned as HTTP 403 JSON)

These errors are returned by verify_chain when a bundle fails verification.

CodeBlockDescription
BUNDLE_INCOMPLETEABundle has no receipts or is missing the invocation receipt
ISSUER_AUDIENCE_GAPBreceipts[i].audreceipts[i+1].iss
CHAIN_HASH_MISMATCHBprev_dr_hash does not match SHA-256 of previous DR JWT bytes
DR_CHAIN_MISMATCHBinvocation.dr_chain[i] does not match SHA-256 of receipts[i] JWT bytes
INVALID_JWT_HEADERCJWT header is not {"alg":"EdDSA","typ":"JWT"}
DID_UNRESOLVABLECIssuer DID could not be resolved to an Ed25519 public key
SIGNATURE_INVALIDCEd25519 signature verification failed
SIGNATURE_MALLEABILITYCSignature rejected: S ≥ L (strict mode violation)
POLICY_VIOLATIONDInvocation args exceed a policy constraint
POLICY_ESCALATIONDSub-DR policy escalates beyond parent policy
RECEIPT_NOT_YET_VALIDEnow < receipt.nbf
RECEIPT_EXPIREDEnow > receipt.exp
TEMPORAL_BOUNDS_VIOLATIONESub-DR temporal bounds not nested within parent bounds
RECEIPT_REVOKEDFReceipt found in Bitstring Status List

HTTP 403 response body format

{
  "valid": false,
  "error": "CHAIN_HASH_MISMATCH",
  "block": "B",
  "message": "prev_dr_hash mismatch at chain index 1: expected sha256:abc123..., got sha256:def456..."
}

The message field always contains a full English sentence with diagnosis. The block field (A–F) tells you which verification stage failed.

CLI Commands

The drs CLI is included in @drs/sdk. Install globally:

pnpm add -g @drs/sdk
drs --help

Or use without global install via:

pnpm exec drs <command>

drs verify

Verify a DRS bundle against drs-verify.

drs verify <bundle-file> [options]
OptionDescription
--url <url>drs-verify base URL (default: $DRS_VERIFY_URL)
--offlineSkip Block F (revocation check) — runs all other blocks locally

Examples:

# Verify against local drs-verify
DRS_VERIFY_URL=http://localhost:8080 drs verify bundle.json

# Offline verification (no revocation check)
drs verify bundle.json --offline

# Verify and show all block results
drs verify bundle.json --verbose

Exit codes: 0 = valid, 1 = invalid, 2 = error (malformed input, server unreachable)


drs audit

Print the full human-readable audit trail for a bundle.

drs audit <bundle-file>

Shows: issuer/audience for each receipt, policy at each level, consent record, temporal bounds, chain hashes, and invocation arguments.


drs policy

Display the policy from a delegation receipt.

drs policy <bundle-file> [--receipt <index>]

--receipt 0 shows the root DR's policy. --receipt 1 shows the first sub-DR's policy. Default: shows all.


drs translate

Translate a policy JSON object to plain English.

drs translate <policy-file> [--locale <locale>]
echo '{"allowed_tools":["web_search"],"max_cost_usd":50,"pii_access":false}' \
  | drs translate --locale en-GB

Output:

Research Agent wants permission to:
✓  Search the web
✗  Cannot access personal data
✗  Cannot spend more than £50.00

Supported locales: en-GB, en-US, fr-FR, de-DE (others fall back to en-US).


drs keygen

Generate a new Ed25519 keypair.

drs keygen [--output <file>]

Without --output: prints private key (base64url) and DID to stdout.

With --output keypair.json: writes:

{
  "private_key": "<base64url 32 bytes>",
  "did": "did:key:z6Mk...",
  "created_at": "2026-03-30T10:00:00Z"
}

Security: The private key is printed to stdout or written to the output file in plaintext. Use HSM or KMS for production keys — the keygen command is for development only.

API Endpoints

The drs-verify HTTP server exposes these endpoints.

POST /verify

Verify a DRS bundle. This is the primary endpoint.

Request:

POST /verify
Content-Type: application/json
{
  "bundle_version": "4.0",
  "invocation": "<invocation-receipt-jwt>",
  "receipts": ["<root-dr-jwt>", "<sub-dr-jwt>"]
}

Body is capped at MAX_BODY_BYTES (default 1 MiB).

Response — valid chain (200):

{
  "valid": true,
  "context": {
    "root_principal": "did:key:z6MkHuman...",
    "chain_depth": 2,
    "leaf_policy": {
      "max_cost_usd": 0.10,
      "allowed_tools": ["web_search"]
    }
  }
}

Response — invalid chain (200):

/verify always returns HTTP 200. Check the valid field to determine the outcome. HTTP 403 is only returned by the MCP/A2A middleware routes, not by /verify directly.

{
  "valid": false,
  "error": {
    "code": "CHAIN_HASH_MISMATCH",
    "message": "prev_dr_hash mismatch at chain index 1",
    "suggestion": "Ensure receipts are in root-first order and were not modified after signing"
  }
}

Response — malformed input (400):

{"error": "invalid character 'x' looking for beginning of value"}

POST /mcp/* (middleware)

Implementation status: The MCP and A2A middleware routes are registered but the reverse proxy mode (DRS_UPSTREAM) and bundle-required enforcement (DRS_REQUIRE_BUNDLE) are not implemented in the current release. The endpoint currently accepts requests and returns 200 after verifying the X-DRS-Bundle header. The proxy and enforcement features are roadmap items.

When DRS_UPSTREAM is configured, drs-verify acts as a reverse proxy. All requests to /mcp/* are intercepted, the X-DRS-Bundle header is verified, and the request is proxied upstream if valid.

Request: Any MCP request with X-DRS-Bundle header:

POST /mcp/tools/call
X-DRS-Bundle: <base64url bundle>
Content-Type: application/json

On valid bundle: Proxied to $DRS_UPSTREAM/mcp/tools/call.

On invalid bundle (403):

{
  "error": "SIGNATURE_INVALID",
  "block": "C",
  "message": "Ed25519 signature verification failed for issuer did:key:z6Mk..."
}

On missing bundle (403, when DRS_REQUIRE_BUNDLE=true):

{
  "error": "BUNDLE_MISSING",
  "message": "X-DRS-Bundle header is required"
}

GET /healthz

Liveness check.

GET /healthz

Response (200):

{"status": "ok"}

Returns 503 only if the server cannot handle requests (e.g., during shutdown).


GET /readyz

Readiness check. Returns 200 when the server is fully initialised and ready to handle verification requests; 503 when not ready.

GET /readyz

Response (200 — ready):

{"status": "ready"}

Response (503 — not ready):

{"status": "not_ready", "reason": "status_list_not_fetched"}

Use /readyz for Kubernetes readiness probes. Use /healthz for liveness probes.

Note: If STATUS_LIST_BASE_URL is not configured, the status list cache is skipped and /readyz always returns 200 immediately.


POST /admin/revoke

Mark a delegation receipt as locally revoked by its status list index. Takes effect immediately — does not wait for the remote Bitstring Status List to refresh.

DRS_ADMIN_TOKEN must be set as an environment variable. If not set, the endpoint responds 503.

POST /admin/revoke
Authorization: Bearer <DRS_ADMIN_TOKEN>
Content-Type: application/json
{"status_list_index": 42}

Body is capped at 1 KiB.

Response (200):

{"revoked": true, "status_list_index": 42}

Response — admin not configured (503):

{"error": "admin endpoint not configured — set DRS_ADMIN_TOKEN"}

Response — wrong or missing token (401):

{"error": "unauthorized"}

The local revocation store is in-memory only. Revocations do not survive process restart. For durable revocation, update the W3C Bitstring Status List at your STATUS_LIST_BASE_URL endpoint.

Configuration Reference

All configuration is via environment variables. No hard-coded URLs, ports, or keys in any DRS component.

drs-verify environment variables

VariableDefaultDescription
LISTEN_ADDR:8080HTTP listen address (e.g. 0.0.0.0:8080, :443)
DID_CACHE_SIZE10000LRU DID resolver cache maximum entries. Hard cap — entries are evicted when full (~640 KB at 10 000 entries).
DID_CACHE_TTL_SECS3600DID resolver cache entry TTL in seconds.
STATUS_LIST_BASE_URLW3C Bitstring Status List endpoint base URL. Required for remote revocation (Block F).
STATUS_CACHE_TTL_SECS300Bitstring Status List cache TTL in seconds. Revocations take effect within this window.
MAX_BODY_BYTES1048576Maximum request body size in bytes for /verify (default 1 MiB).
LOG_LEVELinfoLog verbosity: debug, info, warn, or error.
DRS_ADMIN_TOKENBearer token required for POST /admin/revoke. If not set, the endpoint responds 503. No default — set explicitly to enable.
STORE_DIRBase directory for the filesystem store. Empty = Tier 0 in-memory (dev/test). Set for Tier 1 or Tier 3.
TSA_URLRFC 3161 Timestamp Authority endpoint. Enables Tier 3 trusted timestamping only when STORE_DIR is also set — if STORE_DIR is empty, TSA_URL is silently ignored and the server falls back to Tier 0 (in-memory). Providers: https://freetsa.org/tsr (free), https://timestamp.digicert.com.

drs-sdk CLI environment variables

VariableDefaultDescription
DRS_VERIFY_URLdrs-verify base URL used by drs verify and VerifyClient.

Example configurations

# Tier 0 — in-memory (development default)
LISTEN_ADDR=:8080 ./drs-verify

# Tier 1 — filesystem store
LISTEN_ADDR=:8080 \
  STORE_DIR=/data/drs \
  STATUS_LIST_BASE_URL=https://status.example.com \
  ./drs-verify

# Tier 3 — filesystem + RFC 3161 timestamp anchor (regulated deployments)
LISTEN_ADDR=:8080 \
  STORE_DIR=/data/drs \
  TSA_URL=https://freetsa.org/tsr \
  DRS_ADMIN_TOKEN=your-secret-token \
  STATUS_LIST_BASE_URL=https://status.example.com \
  ./drs-verify

Docker Compose example

version: '3.8'
services:
  drs-verify:
    image: ghcr.io/okeyamy/drs-verify:latest
    ports:
      - "8080:8080"
    environment:
      LISTEN_ADDR: ":8080"
      DID_CACHE_SIZE: "10000"
      DID_CACHE_TTL_SECS: "3600"
      STATUS_LIST_BASE_URL: "https://status.example.com"
      STATUS_CACHE_TTL_SECS: "300"
      DRS_ADMIN_TOKEN: "${DRS_ADMIN_TOKEN}"
      STORE_DIR: "/data"
      TSA_URL: "https://freetsa.org/tsr"
    volumes:
      - drs-data:/data

volumes:
  drs-data:

The published image is distroless, so container-internal shell healthcheck commands such as wget or curl are not available. Probe /healthz and /readyz from Docker, Kubernetes, or your external load balancer instead.

DRS vs Alternatives

DRS is a narrow standard solving a specific problem. Understanding what it is and is not helps you decide where it belongs in your stack.

Comparison table

SystemCore purposeIndependently verifiable?Per-step receipts?OAuth ecosystem?Production adoption
DRSDelegation chain receipts✓ Yes✓ Yes✓ YesResearch / early
OAuth 2.1User → service delegation— (no receipts)✓ YesUniversal
RFC 8693Token exchange between agents— (bearer tokens)✓ YesGrowing
UCANCapability-based delegation✓ Yes✓ Yes✗ CBOR/IPLD~1 production user
OpenTelemetryDistributed tracing✗ Operator-controlled— (spans, not receipts)AgnosticUniversal
Langfuse / ArizeLLM observability✗ Operator-controlled— (logs/evals)AgnosticGrowing
Agentic JWTJWT profile for agent identityPartial— (identity, not chains)✓ YesResearch

Why not UCAN?

UCAN is technically correct. The reason DRS uses OAuth 2.1 instead is ecosystem adoption:

  • AT Protocol chose JWT + OAuth 2.1
  • MCP (Model Context Protocol) chose JWT + OAuth 2.1
  • The LLM agent ecosystem is converging on OAuth-based token exchange

UCAN's production deployment is approximately one system (Storacha/web3.storage). Building DRS on UCAN would have meant building for a standard that the target ecosystem does not use. You cannot get enterprises to adopt an accountability standard that requires them to also adopt CBOR/IPLD and a new DID infrastructure.

DRS solves the same cryptographic problem as UCAN (independently verifiable delegation chains) but uses the token format (JWT) and authorisation protocol (OAuth 2.1) that the ecosystem already uses.

Why not OpenTelemetry?

OpenTelemetry traces are observability data. They tell you what happened from the operator's perspective, stored in operator-controlled infrastructure (Jaeger, Grafana, Datadog).

DRS receipts are authorisation proofs. They tell you what was permitted, signed by the authorising party, verifiable by anyone with the public key.

The critical difference: an attacker who compromises the operator can delete or falsify OTel traces. They cannot forge DRS receipts without the private key. For regulatory compliance ("prove what happened"), cryptographic proofs are required — logs are not sufficient.

Use OpenTelemetry for debugging and monitoring. Use DRS for compliance and audit.

Why not server logs?

Server logs are:

  • Operator-controlled — the operator can modify or delete them
  • Not cryptographically bound to the authorising party
  • Not independently verifiable — an auditor must trust the operator's infrastructure

DRS receipts are:

  • Signed by the authorising party — the operator cannot forge them
  • Verifiable by anyone — no trust in the operator's infrastructure required
  • Tamper-evident — modification breaks the Ed25519 signature

For compliance purposes, "we have logs showing what happened" is weaker than "we have cryptographic proofs signed by the authorising parties." The EU AI Act, HIPAA, and AIUC-1 requirements are moving toward the latter.

When to use each

Your goalUse
Track agent performance, latency, costsOpenTelemetry + Langfuse/Arize
Authenticate users to your serviceOAuth 2.1
Exchange tokens between agentsRFC 8693
Prove what an agent was authorised to doDRS
Meet EU AI Act Article 12/13 requirementsDRS
Meet HIPAA §164.312(b) audit controlsDRS
Get AIUC-1 certificationDRS

Roadmap

DRS is a research project. The implementation roadmap has four phases.

Phase 1 — Core protocol (current)

Status: In progress

  • ✓ DRS 4.0 specification (docs/Drs_architecture_v2.md)
  • drs-core: Rust crypto primitives, JCS canonicalisation, chain verification, WASM build target
  • drs-verify: Go verification server, MCP/A2A middleware, DID resolver with LRU cache, Bitstring Status List cache
  • drs-sdk: TypeScript SDK (issuance path), CLI tools (verify, audit, policy, translate, keygen)
  • ✓ Documentation site (this site)
  • ✓ Local revocation store with POST /admin/revoke (in-memory, immediate effect)
  • ✓ RFC 3161 trusted timestamp anchor (pkg/anchor/) — Tier 3 store with TSA client
  • did:web resolver production hardening (DNS pinning, certificate transparency)
  • ◻ Cross-implementation test suite (Rust ↔ Go ↔ TypeScript JWT interop)

Phase 2 — Production hardening (6 months)

Goal: Production-ready for early adopters and AIUC-1 certification candidates.

  • HSM key management integration (AWS KMS, GCP Cloud KMS) in drs-verify
  • Tier 3 storage (WORM S3, Azure Blob Immutable Storage)
  • AIUC-1 compliance export format and certification documentation
  • Performance benchmarks: p50/p99 latency at 10K req/sec with 5-hop chains
  • Security audit (external)

Phase 3 — Ecosystem integration (12 months)

Goal: Plug-and-play integration with the MCP and A2A ecosystems.

  • MCP server reference implementation with DRS built in (Go)
  • A2A protocol reference implementation with DRS middleware
  • Browser SDK: WASM-based verification for browser-hosted agents
  • On-chain registry: Ethereum mainnet blockchain anchor (Tier 4 — explicit opt-in for blockchain-native enterprise deployments; Ethereum is the only chain with established regulatory and legal precedent)
  • Policy language extensions: resource-level constraints, time-windows, rate limiting

Phase 4 — Standards track (18–24 months)

Goal: DRS becomes an IETF standard or an officially recognised OAuth profile.

  • IETF Internet-Draft submission (OAuth Working Group)
  • W3C Community Group proposal for the consent record format
  • FINOS AI Governance Framework alignment documentation
  • Integration with OpenID for Verifiable Credentials (OID4VC) for human identity binding

Non-goals

These are explicitly out of scope:

  • Behavioral safety (preventing LLMs from doing bad things) — model/runtime problem
  • LLM non-determinism — outside the authorisation layer
  • Prompt injection prevention — DRS records injections, does not prevent them
  • Post-compromise key recovery — operational problem
  • Agent identity (DIDs are used but not managed by DRS)

Glossary

Attenuation — The constraint that a sub-delegation's policy must be a strict subset of its parent's policy. A sub-agent can only be granted less authority than the agent that delegated to it. See Principle of Least Authority (POLA).

Bundle — The unit of transport in DRS. A JSON object containing the invocation receipt and all delegation receipts in the chain, transmitted as X-DRS-Bundle: base64url({bundle_json}).

Chain splicing — An attack where an adversary substitutes an unrelated token into a delegation chain to exceed the scope they were actually granted. CVE-2025-55241 (Azure AD, 2025) is a documented instance. DRS mitigates this with prev_dr_hash.

Delegation Receipt (DR) — A signed JWT issued by each delegator recording one hop in the delegation chain. Contains issuer DID, audience DID, command, policy constraints, temporal bounds, and a hash linking to the previous DR.

DID (Decentralised Identifier) — A URI that identifies an actor without a central registry. Format: did:method:identifier. DRS uses did:key and did:web.

did:key — A DID where the identifier encodes the public key directly: did:key:z{base58btc(multicodec_prefix + pubkey_bytes)}. No registry, no DNS. Preferred for DRS because it is self-contained and requires no network resolution.

did:web — A DID whose identifier is a domain name. Resolved by fetching a DID document from https://domain/.well-known/did.json. Requires DNS and TLS security.

DR Store — The storage backend for delegation receipts. One of five tiers from in-memory (tier 0) to on-chain (tier 4).

EdDSA / Ed25519 — The signature algorithm used in all DRS JWTs. Deterministic (no random nonce), constant-time, and immune to fault attacks. DRS uses ed25519-dalek 2.x (Rust) and golang.org/x/crypto (Go).

Invocation Receipt — A signed JWT recording an actual tool call. Contains the command, arguments, the ordered array of DR hashes (dr_chain), and the tool server's DID.

JCS (JSON Canonicalization Scheme) — RFC 8785. Defines a canonical serialisation of JSON where object keys are sorted recursively by Unicode code point with no whitespace. Used by DRS to ensure identical JWT bytes for logically equivalent objects across all implementations.

JTI (JWT ID) — Unique identifier for a JWT. DRS format: dr:uuid-v4 for delegation receipts, inv:uuid-v4 for invocation receipts.

MCP (Model Context Protocol) — Anthropic's protocol for connecting language models to external tools. DRS bundles are transmitted as X-DRS-Bundle headers on MCP requests.

Multicodec — A self-describing binary encoding prefix used in did:key. For Ed25519, the prefix is [0xed, 0x01]. DRS uses constant-time comparison to check this prefix.

POLA (Principle of Least Authority) — Each delegation grants only the authority needed for the specific task. Sub-delegations must be strictly less permissive than their parent. POLA is enforced both at issuance (SDK) and at verification (Block D).

prev_dr_hash — The field in each sub-DR that links it to its parent: "sha256:{lowercase hex of SHA-256 of parent DR JWT bytes}". Null at the chain root. Creates a tamper-evident chain — any modification to any DR changes its hash and breaks subsequent links.

RFC 8693 — IETF Token Exchange. Defines how one OAuth bearer token can be exchanged for another representing a different principal acting on behalf of the original user. DRS adds per-step receipts to close the chain splicing gap that RFC 8693 leaves open.

sub (Subject) — The JWT claim identifying the original resource owner — always the human at the root of the chain. The sub field must remain identical through every delegation hop. It is never the agent.

Verify_chain — The Go function that runs all six verification blocks (A–F) on a DRS bundle. Fail-closed: any error immediately rejects the request without continuing.