Delegation Receipt Standard

Research Project

DRS is a JWT-based delegation receipt standard for MCP and OAuth-oriented agent ecosystems.

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. Existing OAuth and token-exchange standards help frame that ecosystem, but the implemented DRS code here adds its own signed receipt chain so a tool server can independently verify provenance instead of trusting logs or bearer-token context alone.

This is the chain splicing vulnerability (CVE-2025-55241, demonstrated in Azure AD). DRS is designed as a receipt-layer response for that class of problem, implemented here as signed JWT receipts plus hash-chain verification.

What DRS is not

DRS isDRS is not
A receipt standard for delegation chainsA replacement for OAuth 2.1
Built on JWTs, JCS, Ed25519, MCP middleware, and DIDsA UCAN implementation or OAuth server
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

  • Builders — building on top of DRS: a React Native app, MCP server, A2A agent, Node backend, or any product that should carry signed delegation receipts. Start here if you're not sure. You never need to fork the repo.
  • Developers — using the SDK or Go middleware programmatically (lower-level patterns than the consumer guides).
  • Operators — deploying the verification server and configuring enterprise policies.
  • Auditors — reconstructing delegation chains for compliance evidence.
  • Contributors — understanding the architecture and proposing changes.

The three layers (published separately)

Each layer is an independently-installable artifact. Consumers pull from registries — no fork required.

ComponentLanguageInstallRole
drs-coreRustcargo add drs-core (or bundled inside @okeyamy/drs-sdk)Crypto primitives, JCS canonicalisation, chain verification, WASM build
drs-verifyGodocker pull ghcr.io/okeyamy/drs-verifyVerification HTTP server, Go middleware, DID resolver, status list cache
drs-sdkTypeScriptpnpm add @okeyamy/drs-sdkIssuance path, CLI, React Native / Node / browser

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 bundle using the published SDK and verifier container. You do not need to clone this repository.

New to DRS? First read You do not need to fork and Which part of DRS do I install? to map your role to the right artifact.

Prerequisites

  • Node.js 20+ and pnpm
  • Docker (for the verifier). No Go toolchain required.

1. Install the SDK

pnpm add @okeyamy/drs-sdk

2. Generate a keypair

pnpm exec drs keygen

Current output is plaintext hex:

Ed25519 keypair generated.

DID          : did:key:z6Mk...
Public key   : <hex>
Private key  : <hex>

Save the DID and private key securely.

3. Issue a root delegation

import { issueRootDelegation } from "@okeyamy/drs-sdk";

const privateKey = Uint8Array.from(Buffer.from("YOUR_PRIVATE_KEY_HEX", "hex"));
const now = Math.floor(Date.now() / 1000);

const rootDR = await issueRootDelegation({
  signingKey: privateKey,
  issuerDid: "did:key:z6MkYOUR_DID",
  subjectDid: "did:key:z6MkYOUR_DID",
  audienceDid: "did:key:z6MkAGENT_DID",
  cmd: "/mcp/tools/call",
  policy: {
    allowed_tools: ["web_search"],
    max_cost_usd: 10,
    pii_access: false,
  },
  nbf: now,
  exp: now + 3600,
  rootType: "automated-system",
});

4. Start drs-verify (from the published image)

docker run --rm -d -p 8080:8080 --name drs-verify \
  ghcr.io/okeyamy/drs-verify:latest

# Confirm it's up
curl http://localhost:8080/readyz
# {"status":"ready"}

No clone, no Go build — the image is published to GHCR from this repo's release pipeline.

5. Build and verify a bundle

import { createInvocationBundle, serialiseBundle } from "@okeyamy/drs-sdk";
import { writeFileSync } from "node:fs";

const bundle = await createInvocationBundle({
  rootReceipt: rootDR,
  signingKey: agentPrivateKey,
  issuerDid: "did:key:z6MkAGENT_DID",
  subjectDid: "did:key:z6MkYOUR_DID",
  toolServer: "did:key:z6MkTOOL_DID",
  tool: "web_search",
  args: { query: "hello", estimated_cost_usd: 0.01 },
});

writeFileSync("bundle.json", serialiseBundle(bundle));
DRS_VERIFY_URL=http://localhost:8080 pnpm exec drs verify bundle.json

Expected successful output starts with:

✓ Chain verified
  Root principal : did:key:z6Mk...
  Chain depth    : 1

Next steps

Pick your path:

What is DRS?

DRS (Delegation Receipt Standard) is a JWT-based, per-step delegation receipt standard for MCP and OAuth-oriented agent ecosystems.

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 agent delegation chain.

What DRS adds around existing standards

OAuth / token exchange ecosystems → common surrounding auth context
DRS JWT receipts                  → signed proof for each delegation step
DRS verification                  → independent chain validation at the tool boundary

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. On HTTP-terminated routes it travels as a base64url-encoded JSON object in the X-DRS-Bundle header. On pure JSON-RPC MCP flows it can travel in params._meta["X-DRS-Bundle"] with the same base64url encoding.

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 is designed to complement that ecosystem, but the implemented runtime here is JWT/JCS receipt verification
UCANCapability tokens (CBOR/IPLD)DRS uses JWT receipts and DRS-specific fields, not UCAN envelopes
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 5)

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

Server logs and bearer-token context alone 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 — JWT-based DRS for OAuth/MCP ecosystems (current implementation)

The final pivot was away from UCAN envelopes and toward a JWT/JCS/Ed25519 implementation that fits the MCP and OAuth-oriented agent ecosystem. The reason was practical: AT Protocol, MCP, and the broader deployment environment already converge on JWT-based infrastructure, while UCAN adoption remained niche.

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 (@okeyamy/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 verify bundle.json
drs audit bundle.json

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 Ed25519 signature over base64url(header).base64url(payload)

Fail condition: any signature invalid or any DID unresolvable.

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 and temporal nesting checks:

  • Each sub-DR's policy must be a subset of its parent's policy
  • Child nbf must not be earlier than parent nbf
  • Child exp must not outlive parent exp when both are set
  • 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

Fail condition: any receipt is expired or not yet valid.


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:

  1. Remote check — only if STATUS_LIST_BASE_URL is configured.
  2. Local check — query the in-memory local revocation store.

If the remote status cache is not configured, that part is skipped. Local revocation still runs.

Fail condition: any receipt is marked revoked, or a configured remote revocation check errors.

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(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)
  # Block F
  for dr in drs:
    if dr.drs_status_list_index != null:
      if remote_status_list_configured and 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 verifyimplementation-dependentGo uses crypto/ed25519
Total per request (2-hop chain)~0.8ms p99

Architecture

DRS uses a three-language stack. The three layers are peer implementations with different primary roles; the Go verifier does not call Rust at runtime.

The three layers

┌──────────────────────────────────────────────┐
│  TypeScript SDK  (@okeyamy/drs-sdk)           │
│  issuance, bundle assembly, CLI               │
│  optional HTTP verification client            │
└───────────────────┬──────────────────────────┘
                    │  optional WASM for browser/runtime use
                    │  HTTP to drs-verify
┌───────────────────▼──────────────────────────┐
│  Go verifier  (drs-verify)                    │
│  verification server, middleware, revocation  │
│  resolver cache, health/readiness, storage    │
└───────────────────┬──────────────────────────┘
                    │  shared protocol contract
                    │  conformance vectors
┌───────────────────▼──────────────────────────┐
│  Rust core  (drs-core)                        │
│  crypto primitives, JCS, chain hash, policy   │
│  reference implementation for ambiguous cases │
└──────────────────────────────────────────────┘

Why Rust for the core

Rust is the lowest-level implementation and the internal reference when a conformance vector is ambiguous. It provides:

  • ed25519-dalek 2.x for strict cryptographic operations
  • serde-json-canonicalizer for RFC 8785 JCS
  • deterministic, low-level primitives suitable for WASM export

Rust is important for protocol correctness, but it is not linked into drs-verify through CGO.

Why Go for verification

The Go service is the production verification path today. It handles:

  • verify.Chain() (Blocks A-F)
  • MCPMiddleware / A2AMiddleware
  • DID resolution with LRU caching
  • Bitstring Status List caching with sync.Once
  • health and readiness endpoints
  • storage and local revocation

Key implementation details:

  • crypto/ed25519 for signature verification
  • crypto/subtle.ConstantTimeCompare for DID multicodec prefix checks
  • CGO_ENABLED=0 go build for a single static binary

Why TypeScript for the SDK

Issuance is developer-facing and low-frequency. TypeScript provides:

  • ergonomic npm distribution: pnpm add @okeyamy/drs-sdk
  • strong typing for policies, receipts, and bundles
  • browser-friendly UI integration for consent flows
  • the CLI used for local development and testing

The SDK also includes VerifyClient, which sends bundles to a running drs-verify instance over HTTP. Local WASM verification exists as a separate, explicit capability; it is not an automatic fallback inside VerifyClient.

JCS canonicalisation

All signed JSON in DRS is canonicalised with RFC 8785 before signing. The rules are:

  • object keys sorted recursively
  • no insignificant whitespace
  • canonical JSON number formatting
// WRONG
const payload = JSON.stringify(obj);

// CORRECT
const payload = jcsSerialise(obj);

In the TypeScript SDK, jcsSerialise lives in drs-sdk/src/sdk/jcs.ts. The Rust and TypeScript outputs are checked against shared conformance vectors.

WASM build

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

The browser/WASM path is explicit: callers initialize it themselves via the loader in drs-sdk/src/wasm/loader.ts.

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

Current state: there is no dedicated EU AI Act export command in the CLI yet. Today, evidence is assembled from bundle.json, drs verify, and drs audit output.

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 / Tier 4 deployment postures with timestamping support

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, RFC 8785 JCS) and designed for OAuth-oriented ecosystems — 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_tierBackendStatus
Session0In-memoryImplemented
Ephemeral1Local filesystemImplemented
Durable2S3-compatible object storeRoadmap
Compliant3Filesystem + RFC 3161 timestampingPartial
Timestamped4Tier 3 deployment posturePartial
On-chain5Ethereum anchorRoadmap

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
  • @okeyamy/drs-sdk installed: pnpm add @okeyamy/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 '@okeyamy/drs-sdk';

const humanKey = Uint8Array.from(Buffer.from('HUMAN_PRIVATE_KEY_HEX', 'hex'));
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 '@okeyamy/drs-sdk';

const agentKey = Uint8Array.from(Buffer.from('AGENT_PRIVATE_KEY_HEX', 'hex'));

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
  • @okeyamy/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 '@okeyamy/drs-sdk';
import { writeFileSync } from 'fs';

const agentKey = Uint8Array.from(Buffer.from('SUBAGENT_PRIVATE_KEY_HEX', 'hex'));

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 begins with:

✓ Chain verified
  Root principal : did:key:z6MkHUMAN...
  Chain depth    : 2

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,
  "context": {
    "root_principal": "did:key:z6MkHUMAN...",
    "chain_depth": 2,
    "root_type": "human"
  }
}

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
  Code       : CHAIN_HASH_MISMATCH

Step 6: Print the full audit trail

pnpm exec drs audit bundle.json

This prints the current compact audit breakdown: bundle version, receipt count, key receipt fields, and the invocation's issuer / command / tool server.

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 verify bundle.json
pnpm exec drs audit bundle.json

The compliance officer does not need to contact the operator. The evidence is in the signed bundle plus the verifier output. The current drs audit command is compact rather than full forensic export, but it still exposes the key receipt and invocation fields.

You do not need to fork this repo

This page exists to remove a common confusion.

Consuming DRS means pulling from package registries. It does not mean cloning this repository into your source tree.

All three layers are published. Pick the ones that match your role and install them the way you'd install any other dependency.

LayerHow builders get itYou edit this?
drs-corecargo add drs-core (or via WASM inside @okeyamy/drs-sdk)No — unless you're contributing back
drs-verifydocker pull ghcr.io/okeyamy/drs-verify:latestNo — it's a service, you run the image
drs-sdkpnpm add @okeyamy/drs-sdkNo — regular npm dependency

You only clone the repo if you want to:

  • contribute (fix a bug, propose a feature, submit a PR)
  • build from source (e.g. for air-gapped deployments where pulling from Docker Hub / GHCR is not allowed)
  • vendor a specific commit (hash-pin for supply-chain strictness)

"But where does my code go?"

Your application lives in your own repository. DRS is a dependency. The typical shape:

your-app/
├── package.json            ← "@okeyamy/drs-sdk": "^0.1.0"
├── docker-compose.yml      ← services: your-app, drs-verify
└── src/
    ├── issue-receipt.ts    ← uses @okeyamy/drs-sdk
    └── verify-middleware.ts ← calls http://drs-verify:8080/verify

You never add drs-core/, drs-verify/, or drs-sdk/ as subdirectories of your repo.

Verification: the five-minute test

Run this on a fresh machine with Docker and Node 20:

# No `git clone` of the DRS monorepo — this stays empty.
mkdir my-drs-app && cd my-drs-app

# 1. Pull the SDK from npm.
echo '{"type":"module","dependencies":{"@okeyamy/drs-sdk":"latest"}}' > package.json
npm install

# 2. Pull the verifier from GHCR and run it.
docker run --rm -d -p 8080:8080 --name drs-v ghcr.io/okeyamy/drs-verify:latest

# 3. Prove the SDK works against the running verifier.
node -e '
  import("@okeyamy/drs-sdk").then(async (s) => {
    console.log("SDK version loaded:", typeof s.issueRootDelegation === "function" ? "ok" : "missing");
    const res = await fetch("http://localhost:8080/healthz");
    console.log("verifier healthz:", res.status, await res.json());
  });
'

# 4. Stop the verifier.
docker stop drs-v

Expected output:

SDK version loaded: ok
verifier healthz: 200 { status: 'ok' }

No clone. No fork. Nothing about this machine knows about the DRS source tree.

When you should clone

Clone if:

  1. You're changing DRS itself. Crypto bug, new feature, new test vector — clone, fix, PR. See Contributing.
  2. You need reproducible builds with a commit hash. Pin to a commit, build the Docker image yourself in your CI, push to your own registry. Your base of trust moves from GHCR to your own build.
  3. You're running an air-gapped deployment. Build drs-verify from source, vendor the SDK into your npm mirror, run the container from your private registry.

In every other case — using DRS in your product, wiring it into MCP or A2A, issuing receipts from a React Native app — you install from the registries. No fork.

Which part of DRS do I install?

DRS has three separately-published layers. Which you install depends on what you are building. This page maps common roles to the artifact(s) you need.

One-minute decision tree

What are you building?
│
├─ An AI agent / client that ACTS on behalf of a user or service
│    → Install @okeyamy/drs-sdk (issuance path)
│
├─ A tool server or gateway that ACCEPTS requests from agents
│    → Run ghcr.io/okeyamy/drs-verify (verification service)
│    → OR embed pkg/middleware in your Go server
│
├─ A human-consent UI (user clicks "Approve", you mint a root delegation)
│    → Install @okeyamy/drs-sdk
│
├─ An auditor / compliance replay tool (verify chains after the fact)
│    → Install @okeyamy/drs-sdk (uses its VerifyClient)
│    → OR point it at a running drs-verify /verify endpoint
│
└─ Rust binary / WASM polyfill
     → Install drs-core from crates.io

Mapping roles to artifacts

Role: Agent runtime (Node, browser, React Native, Deno)

Install the SDK from npm.

pnpm add @okeyamy/drs-sdk

You use it to:

  • generate keys (drs keygen or programmatically)
  • issue root delegations (when a human consents)
  • issue sub-delegations (when an agent delegates to another agent)
  • issue invocations (when the agent actually calls a tool)
  • optionally, verify bundles via VerifyClient

You do not need to run drs-verify for issuance. Issuance is all local cryptography.

Role: Tool server / MCP server / API gateway

Run the verification service. Two shapes:

Shape A — sidecar verifier (any language tool server)

Run ghcr.io/okeyamy/drs-verify:latest next to your tool server. In your server's request handler, before doing real work, call POST /verify with the incoming bundle. If result.valid is true, proceed.

┌─────────────────┐          ┌─────────────────┐
│  your tool      │  POST    │  drs-verify     │
│  server (any    │──/verify─▶ :8080 (sidecar) │
│  language)      │  ◀─json─ │                 │
└─────────────────┘          └─────────────────┘

Best for: Node, Python, Rust, Ruby, Java — anything not Go.

Shape B — embedded Go middleware

If your tool server is in Go, import the middleware package directly. Faster path (no extra hop), but Go-only.

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

mux.Handle("/tools/call", middleware.MCPMiddleware(deps, nonceStore, yourHandler))

Best for: Go MCP servers, Go A2A servers.

Install the SDK from npm, same as an agent. The difference is semantic: your app's issueRootDelegation call represents the moment a human clicked "Approve". Capture consent metadata (session ID, policy hash, timestamp) in the consent field.

See Human Consent Records.

Role: Auditor / compliance reviewer

Install the SDK and use its VerifyClient to replay past chains. You can point it at a running drs-verify or use the SDK-only in-process verifier for air-gapped replay.

pnpm add @okeyamy/drs-sdk
import { VerifyClient } from "@okeyamy/drs-sdk";

const client = new VerifyClient({ baseUrl: "https://drs-verify.internal" });
const result = await client.verify(bundle);

Role: Rust/WASM builder

Most Rust callers don't interact with drs-core directly — it's embedded inside @okeyamy/drs-sdk via WASM. But if you're building a Rust binary (for example, a CLI that issues receipts), use the crate:

[dependencies]
drs-core = "0.1"

Combining them

A typical production deployment uses all three:

┌──────────────────────┐
│ Agent (React Native) │   uses @okeyamy/drs-sdk (npm)
└──────────┬───────────┘
           │  HTTPS: X-DRS-Bundle: <base64url>
           ▼
┌──────────────────────┐
│ Tool server (Node)   │   forwards body + bundle
└──────────┬───────────┘
           │  POST /verify
           ▼
┌──────────────────────┐
│ drs-verify (Docker)  │   runs from ghcr.io/okeyamy/drs-verify
│ + Redis (replay)     │
└──────────────────────┘

None of these three boxes clones the DRS monorepo.

Integrate DRS in a React Native app

This guide covers installing and using @okeyamy/drs-sdk inside an Expo or bare React Native app. The SDK is pure TypeScript plus pure-JS cryptography (@noble/ed25519) — no native modules, no WASM glue code required on the mobile side.

Compatibility matrix

RuntimeStatus
Expo SDK 50+ (managed)Supported
Expo SDK 50+ (bare)Supported
React Native 0.73+ (community CLI)Supported
Hermes (default on RN 0.70+)Supported
JavaScriptCoreSupported

The SDK relies on:

  • crypto.getRandomValues — provided by React Native's expo-crypto or by modern RN directly. Polyfill on older targets.
  • TextEncoder / TextDecoder — provided by Hermes. On JSC, polyfill with text-encoding.
  • atob / btoa — provided by both engines.

Install

# Expo
npx expo install @okeyamy/drs-sdk

# RN community CLI
pnpm add @okeyamy/drs-sdk

If your project runs on an older RN (<0.74) or bare JSC:

pnpm add react-native-get-random-values text-encoding

Then import the polyfills once, at the top of your entry file (index.js or App.tsx):

import "react-native-get-random-values";
import "text-encoding/encoding-indexes";

Generate and persist a key

Mobile apps typically generate a per-device key on first launch and store it in the OS secure enclave / Keychain. Use expo-secure-store (Expo) or react-native-keychain (bare).

import * as SecureStore from "expo-secure-store";
import { derivePublicKey } from "@okeyamy/drs-sdk";

export async function getOrCreateAgentKey(): Promise<Uint8Array> {
  const existing = await SecureStore.getItemAsync("drs.agent_sk");
  if (existing) {
    return Uint8Array.from(Buffer.from(existing, "hex"));
  }
  // Fresh key: 32 bytes from a CSPRNG.
  const sk = new Uint8Array(32);
  globalThis.crypto.getRandomValues(sk);
  await SecureStore.setItemAsync(
    "drs.agent_sk",
    Buffer.from(sk).toString("hex"),
    { keychainAccessible: SecureStore.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY },
  );
  return sk;
}

export function didFromKey(sk: Uint8Array): string {
  const pub = derivePublicKey(sk);
  const multicodec = new Uint8Array([0xed, 0x01, ...pub]);
  return `did:key:z${base58btc(multicodec)}`;
}

base58btc is a small pure-JS helper; see the SDK tests for an inlineable implementation.

Issue an invocation when the user taps a button

import { useState } from "react";
import { Button, Text } from "react-native";
import {
  buildBundle,
  issueInvocation,
  computeChainHash,
  serialiseBundle,
} from "@okeyamy/drs-sdk";

export function CallToolButton({
  agentKey,
  agentDid,
  rootDR,
  toolServerDid,
}: Props) {
  const [result, setResult] = useState<string>("");

  async function onCall() {
    const invocation = await issueInvocation({
      signingKey: agentKey,
      issuerDid: agentDid,
      subjectDid: agentDid,
      cmd: "/mcp/tools/call",
      args: { tool: "web_search", query: "react native drs integration" },
      drChain: [computeChainHash(rootDR)],
      toolServer: toolServerDid,
    });

    const bundle = buildBundle([rootDR], invocation);
    const bundleHeader = serialiseBundle(bundle);

    const res = await fetch("https://your-tool-server.example.com/mcp/tools/call", {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "X-DRS-Bundle": bundleHeader,
      },
      body: JSON.stringify({ tool: "web_search", query: "..." }),
    });
    setResult(await res.text());
  }

  return (
    <>
      <Button onPress={onCall} title="Run tool with DRS receipt" />
      <Text>{result}</Text>
    </>
  );
}

The root delegation (rootDR) was issued when the user approved the agent — see Human Consent Records. Persist it alongside the agent key, or fetch it from your backend on app launch.

Where drs-verify runs

The verifier does not run on the phone. It is a backend service that your tool server calls (or that your tool server is wrapped by). From the React Native app's perspective, DRS is issuance-only: you sign receipts, you send them over HTTPS in the X-DRS-Bundle header, the server verifies.

A typical deployment:

┌───────────────────────┐
│ React Native app      │  @okeyamy/drs-sdk (npm)
└─────────┬─────────────┘
          │ HTTPS + X-DRS-Bundle header
          ▼
┌───────────────────────┐
│ Your tool server API  │  validates bundle (sidecar or Go middleware)
└─────────┬─────────────┘
          │ POST /verify (optional sidecar mode)
          ▼
┌───────────────────────┐
│ drs-verify (Docker)   │  ghcr.io/okeyamy/drs-verify
└───────────────────────┘

See Integrate DRS with an MCP Node server for the server side.

Troubleshooting

"crypto.getRandomValues is not a function"

You're on an older RN without a secure random source. Install and import react-native-get-random-values as shown above.

"Cannot find module '@noble/ed25519'"

This is a transitive dependency of the SDK. It should resolve automatically. If it doesn't, clear the Metro cache:

npx expo start --clear
# or for bare:
pnpm start -- --reset-cache

Signatures don't verify on the server

Double-check that drChain entries are chain hashes (sha256:...), not raw JWTs. The SDK exposes computeChainHash(jwt) for this — forgetting it is the most common mistake.

Bundle is too large for HTTP headers

Some gateways cap header size at 8 KB. If your delegation chain has many sub-delegations, consider sending the bundle as a request body field using the JSON-RPC _meta pattern instead of the X-DRS-Bundle header. Both shapes are defined in drs-source-of-truth.md.

Integrate DRS with an MCP server (Node / TypeScript)

Your MCP server runs on Node. Agents send tool-call requests with a X-DRS-Bundle header. You want the bundle verified before your business logic runs. This is the sidecar pattern.

No Go code, no forking DRS, no rebuilding containers.

Architecture

Agent (React Native, web, Node, etc.)
   │
   │  POST /tools/call
   │  X-DRS-Bundle: eyJ...
   │
   ▼
┌────────────────────────────┐       ┌───────────────────────┐
│  Your MCP server (Node)    │──────▶│  drs-verify (Docker)  │
│  1. read bundle from header│ POST  │  ghcr.io/okeyamy/     │
│  2. POST /verify           │ /verify│  drs-verify:latest    │
│  3. if valid → run tool    │       │                       │
│  4. else → 403             │◀──────│                       │
└────────────────────────────┘       └───────────────────────┘

Install the enforcement middleware

The secure default path is the reusable HTTP middleware from @drs/mcp-server. It extracts X-DRS-Bundle, sends the decoded bundle plus the actual request body to drs-verify, rejects invalid chains, rejects body-binding mismatches, and only then lets your handler run.

# On your MCP server
pnpm add @drs/mcp-server

Docker Compose for local dev

# docker-compose.yml at the root of YOUR project
services:
  mcp-server:
    build: .
    ports:
      - "3000:3000"
    environment:
      DRS_VERIFY_URL: http://drs-verify:8080
    depends_on:
      - drs-verify

  drs-verify:
    image: ghcr.io/okeyamy/drs-verify:latest
    environment:
      LISTEN_ADDR: ":8080"
      LOG_FORMAT: json
      # Optional: replay protection that survives restart and scales horizontally
      NONCE_STORE_BACKEND: redis
      REDIS_URL: redis://redis:6379/0
    depends_on:
      - redis

  redis:
    image: redis:7-alpine

Middleware for your MCP server

Express / Fastify / raw http.Server — the pattern is the same.

// drs-middleware.ts
import { createDrsHttpMiddleware } from "@drs/mcp-server";

const VERIFY_URL = process.env.DRS_VERIFY_URL ?? "http://localhost:8080";

const drs = createDrsHttpMiddleware({ verifyUrl: VERIFY_URL });

export async function drsVerify(req, res, next) {
  const result = await drs(
    {
      headers: req.headers,
      body: req.body,
    },
    (verifiedReq) => {
      req.drs = verifiedReq.drs;
      next();
    },
  );

  if (!result.ok) {
    return res.status(result.status).json({ drs_error: result.error });
  }
}

Wiring it in Express

import express from "express";
import { drsVerify } from "./drs-middleware.js";

const app = express();
app.use(express.json());

app.post("/tools/call", drsVerify, async (req, res) => {
  // req.drs is set — it contains RootPrincipal, LeafPolicy, etc.
  const { tool, ...args } = req.body;

  // Enforce policy at the tool layer. `drs-verify` has already checked
  // attenuation; here you enforce execution-time limits.
  const maxCost = req.drs.leaf_policy?.max_cost_usd;
  if (maxCost != null && args.estimated_cost_usd > maxCost) {
    return res.status(403).json({ error: "Exceeds policy.max_cost_usd" });
  }

  const result = await runTool(tool, args);
  res.json(result);
});

app.listen(3000);

Wiring it in Fastify

import Fastify from "fastify";
import { drsVerify } from "./drs-middleware.js";

const app = Fastify();

app.post(
  "/tools/call",
  {
    preHandler: async (req, reply) => {
      // Adapt the Express-shaped middleware to Fastify.
      const next = () => {};
      const expressRes = {
        status: (n: number) => ({ json: (x: unknown) => reply.code(n).send(x) }),
      };
      await drsVerify(req as any, expressRes as any, next);
    },
  },
  async (req) => {
    return { ok: true, drs: (req as any).drs };
  },
);

app.listen({ port: 3000 });

Performance notes

  • drs-verify handles DID resolution caching, nonce replay checking, and revocation lookups in one round-trip. Typical /verify latency against a local container is 5–15 ms (single-digit when caches are warm).
  • If the 5–15 ms hop matters, switch to the embedded Go middleware pattern — but that forces your tool server to be in Go.

Request-binding behavior

createDrsHttpMiddleware passes the actual parsed request body to /verify. The verifier compares that body with the signed invocation.args using JCS. If they differ, the middleware rejects the request before your handler runs.

Integrate DRS with an A2A agent (Node / TypeScript)

Agent-to-Agent (A2A) differs from MCP in shape — both agents sit at equal standing and exchange tasks — but the DRS integration story is the same: the caller attaches a receipt bundle, the receiver verifies it before acting.

This guide covers the receiver side in Node. The caller side is the same as React Native issuance and MCP Node integration — you issue an invocation with issueInvocation.

Architecture

Agent A (initiator)                  Agent B (receiver)
     │
     │ POST /a2a/task
     │ X-DRS-Bundle: eyJ...
     ▼
┌──────────────────┐         ┌─────────────────────┐
│ Agent B (Node)   │────────▶│ drs-verify sidecar  │
│ 1. extract hdr   │  POST   │ ghcr.io/okeyamy/... │
│ 2. /verify       │ /verify │                     │
│ 3. if valid →    │◀────────│                     │
│    run task      │         └─────────────────────┘
└──────────────────┘

Agent B is structurally the same as an MCP tool server — both verify an inbound bundle before executing. If you've already set up the MCP integration the code here is almost identical.

Install

pnpm add @drs/mcp-server

The actual cryptographic verification happens in the drs-verify container. The Node package gives your receiver a secure enforcement point that rejects invalid chains and body-binding mismatches before task execution.

Compose with Redis for shared replay protection

If Agent B is horizontally scaled across multiple instances, you need shared replay protection or an attacker can submit the same bundle to each replica in turn.

services:
  agent-b:
    build: .
    deploy:
      replicas: 3
    environment:
      DRS_VERIFY_URL: http://drs-verify:8080

  drs-verify:
    image: ghcr.io/okeyamy/drs-verify:latest
    environment:
      NONCE_STORE_BACKEND: redis
      REDIS_URL: redis://redis:6379/0
    deploy:
      replicas: 2      # drs-verify itself can also scale — state is in Redis

  redis:
    image: redis:7-alpine

A2A middleware

// a2a-middleware.ts
import { createDrsHttpMiddleware } from "@drs/mcp-server";

const VERIFY_URL = process.env.DRS_VERIFY_URL ?? "http://localhost:8080";
const drs = createDrsHttpMiddleware({ verifyUrl: VERIFY_URL });

export async function drsA2A(req, res, next) {
  const result = await drs(
    {
      headers: req.headers,
      body: req.body,
    },
    (verifiedReq) => {
      req.drs = verifiedReq.drs;
      next();
    },
  );

  if (!result.ok) return res.status(result.status).json({ drs_error: result.error });
}

Task handler

import express from "express";
import { drsA2A } from "./a2a-middleware.js";

const app = express();
app.use(express.json({ limit: "1mb" }));

app.post("/a2a/task", drsA2A, async (req, res) => {
  // req.drs.root_principal is the original human/organisation
  // req.drs.leaf_policy is the effective policy AFTER attenuation
  const { task_type, payload } = req.body;

  // A2A-specific: enforce that the task matches what's allowed by policy.
  const allowedTools = req.drs.leaf_policy?.allowed_tools ?? [];
  if (allowedTools.length > 0 && !allowedTools.includes(task_type)) {
    return res.status(403).json({
      error: "task_type not in allowed_tools",
      allowed: allowedTools,
    });
  }

  const result = await runA2ATask(task_type, payload, {
    onBehalfOf: req.drs.root_principal,
  });
  res.json(result);
});

app.listen(3000);

JSON-RPC variant

Some A2A deployments use JSON-RPC instead of plain HTTP. The DRS spec allows the bundle to live in _meta["X-DRS-Bundle"] instead of a header.

app.post("/a2a/rpc", express.json(), async (req, res) => {
  const bundleStr = req.body?._meta?.["X-DRS-Bundle"];
  if (!bundleStr) return res.status(401).json({ error: "missing bundle" });

  const bundle = JSON.parse(
    Buffer.from(bundleStr, "base64url").toString("utf8"),
  );
  const r = await fetch(`${VERIFY_URL}/verify`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(bundle),
  });
  const result = await r.json();

  if (!result.valid) {
    return res.json({
      jsonrpc: "2.0",
      id: req.body.id,
      error: { code: -32001, message: "DRS verification failed", data: result.error },
    });
  }

  // dispatch on req.body.method ...
});

Integrate DRS with a non-MCP / non-A2A Node backend

Plenty of real-world services aren't MCP tool servers or A2A agents — they're ordinary APIs that want to enforce "this request came from an authorised delegation chain" before doing work. DRS still fits.

This guide covers three patterns for adding DRS to an existing Node backend:

  1. Express/Fastify middleware — add one app.use call.
  2. Reverse proxy in front of an unchanged backend — zero application changes.
  3. Per-route opt-in — some routes enforce DRS, others don't.

Pattern 1: one-line middleware

Same shape as the MCP Node integration. Summary:

import { drsVerify } from "./drs-middleware.js";

app.use(drsVerify);                     // enforce on every route
app.use("/admin", drsVerify);           // enforce on a subtree only

The middleware reads X-DRS-Bundle, POSTs to the sidecar verifier, and either 401s/403s or attaches req.drs and calls next(). See the MCP guide for the full implementation.

Pattern 2: reverse proxy (zero app changes)

Put drs-verify and a small proxy container in front of your existing backend. Your app gets requests as if DRS were transparent, and a header named X-DRS-Principal is added by the proxy so the app can learn who authorised the call.

Cloudflare Workers, envoy, nginx with lua, or a tiny Node proxy all work. Here's the Node version:

// proxy.ts
import http from "node:http";
import { createProxyServer } from "http-proxy";

const VERIFY_URL = "http://drs-verify:8080";
const UPSTREAM = "http://my-existing-backend:5000";

const proxy = createProxyServer({ target: UPSTREAM, changeOrigin: true });

http.createServer(async (req, res) => {
  const bundleHeader = req.headers["x-drs-bundle"];
  if (!bundleHeader) {
    res.writeHead(401, { "content-type": "application/json" });
    return res.end(JSON.stringify({ error: "missing X-DRS-Bundle" }));
  }
  const bundle = JSON.parse(
    Buffer.from(bundleHeader as string, "base64url").toString("utf8"),
  );
  const vr = await fetch(`${VERIFY_URL}/verify`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(bundle),
  });
  const result = await vr.json();
  if (!result.valid) {
    res.writeHead(403, { "content-type": "application/json" });
    return res.end(JSON.stringify(result));
  }
  // Strip the bundle (contains sensitive signatures) and add a principal header.
  delete req.headers["x-drs-bundle"];
  req.headers["x-drs-principal"] = result.context.root_principal;
  req.headers["x-drs-correlation-id"] = result.context.correlation_id ?? "";
  proxy.web(req, res);
}).listen(8443);

Deploy this alongside drs-verify and your backend:

services:
  edge:
    build: ./proxy
    ports: ["8443:8443"]
    depends_on: [drs-verify, backend]

  drs-verify:
    image: ghcr.io/okeyamy/drs-verify:latest

  backend:
    image: your-app:latest     # unchanged

The backend never learns DRS exists. It just sees X-DRS-Principal.

Pattern 3: per-route opt-in

For a mixed workload — some endpoints public, some require DRS, some require DRS + additional RBAC — make DRS enforcement explicit per route:

import { drsOptional, drsRequired } from "./drs-middleware.js";

app.get("/status", (req, res) => res.json({ ok: true })); // public

app.get("/report", drsOptional, (req, res) => {
  // If bundle present, tailor the response to that principal.
  // Otherwise return a generic report.
  res.json(generateReport(req.drs?.root_principal));
});

app.post("/admin/delete", drsRequired, (req, res) => {
  // DRS enforced. Additionally check operator role.
  const principal = req.drs.root_principal;
  if (!isOperator(principal)) return res.status(403).json({ error: "not operator" });
  deleteThing(req.body.id);
  res.status(204).end();
});

Policy enforcement at the app layer

drs-verify enforces attenuation (child policies can't escalate) but it does not enforce runtime cost or per-call counting. Do that in your app:

app.post("/llm/complete", drsRequired, async (req, res) => {
  const policy = req.drs.leaf_policy ?? {};
  const estCost = estimateCost(req.body);

  if (policy.max_cost_usd != null && estCost > policy.max_cost_usd) {
    return res.status(403).json({
      error: "exceeds policy.max_cost_usd",
      max: policy.max_cost_usd,
      estimated: estCost,
    });
  }

  const result = await callLLM(req.body);
  res.json(result);
});

Install the SDK

Requirements

  • Node.js 20+
  • pnpm

Install

pnpm add @okeyamy/drs-sdk

Repository: https://github.com/OkeyAmy/DRS

TypeScript configuration

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

Verify the install

pnpm exec drs keygen

Expected output includes:

Ed25519 keypair generated.

DID          : did:key:z6Mk...
Public key   : <hex>
Private key  : <hex>

What's in the package

The published package exports from the root entry only. Import from @okeyamy/drs-sdk, not subpaths.

If you are wiring middleware guides from this docs site, use package names and paths exactly as shown in each page. Do not switch to legacy aliases.

import {
  issueRootDelegation,
  issueSubDelegation,
  issueInvocation,
  createInvocationBundle,
  buildBundle,
  serialiseBundle,
  parseBundle,
  computeChainHash,
  checkPolicyAttenuation,
  translatePolicy,
  VerifyClient,
  initWasm,
  getWasmModule,
  isWasmReady,
} from "@okeyamy/drs-sdk";

Browser / WASM notes

The SDK includes a WASM loader, but browser/WASM verification is still an explicit integration path:

  • VerifyClient talks to a running drs-verify HTTP service
  • initWasm() / getWasmModule() are available if you wire in a WASM build
  • there is no published standalone @drs/wasm package in this repo today

Building the WASM package yourself

If you are developing locally and want to experiment with the WASM build:

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

MCP Middleware Integration

Add DRS verification to an MCP server. The Go middleware verifies the X-DRS-Bundle header before your business handler runs.

How it works

MCP client
    │  POST /mcp/tools/call
    │  X-DRS-Bundle: <base64url(JSON bundle)>
    ▼
drs-verify/pkg/middleware.MCPMiddleware
    │  decode base64url
    │  parse JSON bundle
    │  run verify.Chain (blocks A–F)
    ▼ VALID
business handler

If verification fails:

  • missing bundle: 401
  • malformed base64url/JSON: 400
  • invalid chain: 403

Go integration

If your MCP-facing server is in Go, wrap the route with middleware.MCPMiddleware or middleware.OptionalMCPMiddleware.

package main

import (
    "log"
    "net/http"
    "time"

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

func main() {
    res, err := resolver.New(10_000, time.Hour)
    if err != nil {
        log.Fatal(err)
    }

    deps := verify.Deps{
        Resolver: res,
    }

    mux := http.NewServeMux()
    // 1) Define your normal business logic handler.
    mcpBusinessHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 3) Read verification context after middleware has validated the bundle.
        ctx := middleware.GetVerificationContext(r.Context())
        if ctx == nil {
            http.Error(w, "missing verification context", http.StatusForbidden)
            return
        }

        // Example: make authorization/usage decisions with verified identity.
        // ctx.RootPrincipal, ctx.ChainDepth, ctx.LeafPolicy
        w.WriteHeader(http.StatusOK)
    })

    // 2) Wrap your business handler with MCP middleware.
    mux.Handle("/mcp/", middleware.MCPMiddleware(deps,
        mcpBusinessHandler,
    ))

    log.Fatal(http.ListenAndServe(":8080", mux))
}

Use OptionalMCPMiddleware only when DRS is advisory and your business handler can safely process requests without a bundle.

TypeScript / pure JSON-RPC integration

If your MCP traffic is pure JSON-RPC rather than HTTP-terminated, use the TypeScript wrapper packages in packages/drs-mcp-client and packages/drs-mcp-server.

  • client side: injects the bundle into params._meta["X-DRS-Bundle"]
  • server side: decodes the same base64url string and posts the decoded bundle to /verify

This is the Shape 2 transport described in docs/drs-source-of-truth.md.

Testing your integration

# Valid bundle — expect exit code 0
DRS_VERIFY_URL=http://localhost:8080 pnpm exec drs verify bundle.json

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

# Malformed bundle — expect 400
curl -X POST http://localhost:8080/mcp/tools/call \
  -H "X-DRS-Bundle: !!!not-base64url!!!" \
  -H "Content-Type: application/json" \
  -d '{"tool":"web_search","query":"test"}'

A2A Middleware Integration

DRS integrates with Agent-to-Agent (A2A) calls using the same bundle transport as HTTP-terminated MCP: the full bundle travels in the X-DRS-Bundle header as base64url-encoded JSON.

The middleware validates the bundle before your business handler executes.

Transport

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

The bundle must contain the full delegation chain from the original root to the current caller.

What happens on failure

  • missing bundle: 401
  • malformed base64url/JSON: 400
  • invalid chain: 403

Go middleware

package main

import (
    "log"
    "net/http"
    "time"

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

func main() {
    res, err := resolver.New(10_000, time.Hour)
    if err != nil {
        log.Fatal(err)
    }

    deps := verify.Deps{
        Resolver: res,
    }

    mux := http.NewServeMux()
    // 1) Define your normal A2A business handler.
    a2aBusinessHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 3) Read verification context after middleware validation.
        ctx := middleware.GetVerificationContext(r.Context())
        if ctx == nil {
            http.Error(w, "missing verification context", http.StatusForbidden)
            return
        }

        // Example: use ctx.RootPrincipal / ctx.ChainDepth / ctx.LeafPolicy.
        w.WriteHeader(http.StatusOK)
    })

    // 2) Wrap your business handler with A2A middleware.
    mux.Handle("/a2a/", middleware.A2AMiddleware(deps,
        a2aBusinessHandler,
    ))

    log.Fatal(http.ListenAndServe(":8080", mux))
}

Use OptionalA2AMiddleware only when DRS is advisory and your business handler can safely process requests without a bundle.

Chain depth in multi-agent A2A topologies

In multi-agent A2A deployments, an orchestrator may dispatch to multiple worker agents. Each worker carries the full chain to itself:

Human → Orchestrator DR → Worker DR → Invocation

That worker bundle includes:

  • receipts[0]: root delegation
  • receipts[1]: orchestrator → worker sub-delegation
  • invocation: worker invocation receipt

The orchestrator issues the worker's sub-delegation before dispatch. The worker issues the invocation receipt when calling the next service.

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.

Important: compute the hash from the exact rendered text shown to the user after localization and formatting.

Generating the human-readable text

Use the SDK's translatePolicy function:

import { translatePolicy } from '@okeyamy/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 '@okeyamy/drs-sdk';

// Hash the text the user saw — not the policy JSON
const policyHash = computeChainHash(humanText);
// "sha256:a1b2c3..."
import { issueRootDelegation, computeChainHash, translatePolicy } from '@okeyamy/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 using the exact same text for policy_hash
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.

Practical model:

  • SDK issuance checks protect you before signing invalid receipts.
  • Verifier checks protect downstream services from invalid external bundles.

Example: narrowing authority

import { issueSubDelegation } from '@okeyamy/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": 1
}

Load in TypeScript:

import { parseOperatorConfig } from '@okeyamy/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"Configuration value reserved for an external AWS KMS signer integration
"gcp-kms"Configuration value reserved for an external GCP Cloud KMS signer integration

Security: Never use "file" or "env" in production with keys that have regulatory significance unless your deployment wraps signing in a separately reviewed secrets boundary.

Implementation note: these values are accepted by the configuration model only. This repository does not currently implement KMS-backed signing. Treat KMS/HSM signing as an external integration or production-hardening task, not as built-in runtime support.

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 tier field

storage_tier records the operator's intended receipt-retention posture. The configuration schema accepts 0 through 5 so operator files can use the full DRS vocabulary, but not every tier is implemented by the current verifier.

ValueCurrent verifier behavior
0In-memory store when STORE_DIR is unset
1Local filesystem store when STORE_DIR is set
2Roadmap only — no S3-compatible object-store backend in this release
3Filesystem store plus RFC 3161 timestamp attempt when STORE_DIR and TSA_URL are set; WORM must be supplied by deployment infrastructure
4Same backend as Tier 3, with timestamp verification/reporting requested by callers
5Roadmap only — no Ethereum anchoring backend in this release

See Storage Tiers for the canonical status table.

Storage Tiers

DRS uses a six-tier storage model. The canonical reference lives in docs/storage-tiers.md; this page summarizes it and highlights what is actually implemented today.

Tier reference

TierNameBackendEnv varsStatus
0SessionIn-memory(none)Implemented
1EphemeralLocal filesystemSTORE_DIRImplemented
2DurableS3-compatible object storeroadmapNot implemented
3CompliantFilesystem + RFC 3161 timestampingSTORE_DIR + TSA_URLPartially implemented
4TimestampedTier 3 deployment posture with timestamp retrieval/reportingSTORE_DIR + TSA_URLPartially implemented
5On-chainTier 3 + Ethereum anchorroadmapNot implemented

What is actually implemented today

Tier 0: default when STORE_DIR is unset. Receipts are lost on restart.

Tier 1: receipts are written to the local filesystem and survive restart.

Tier 2: documented target only. There is no S3-compatible store in the current codebase.

Tier 3: when TSA_URL is set, drs-verify stores the receipt and attempts RFC 3161 timestamping. This is best-effort:

  • the receipt is still stored if the TSA is unavailable
  • the timestamp is stored alongside the receipt when available
  • WORM semantics are not enforced by the current filesystem backend

Tier 4: same backend as Tier 3. Today this is a reporting / operator posture rather than a separate storage engine.

Tier 5: Ethereum anchoring is a roadmap item, not a delivered feature.

Configuration

# Tier 0 — session / in-memory
LISTEN_ADDR=:8080 ./drs-verify

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

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

For the full canonical model, caveats, and tier semantics, see Canonical Storage Tiers.

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/KMS or equivalent external signer 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 keys:

Use a KMS/HSM-backed signer or another reviewed external signing service for operator keys with regulatory significance. The current repository parses operator_key_management values such as "aws-kms" and "gcp-kms" in configuration, but it does not include built-in KMS signing code. Do not assume that setting those values alone moves signing out of local process memory.

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
  • Do not treat aws-kms or gcp-kms config values as built-in signing support until your deployment supplies and tests that signer integration
  • 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: Obtain the bundle

If the operator has already provided bundle.json, use that directly.

Or assemble a bundle manually from JWT strings:

{
  "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 verifies the chain through drs-verify. The verifier reads the issuer DIDs from the JWTs and resolves did:key locally from the DID bytes.

Step 3: Read the audit trail

pnpm exec drs audit evidence.json

Current drs audit output is intentionally compact. It prints bundle version, receipt count, the main fields from each receipt, and the invocation's issuer, command, and tool server.

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

The CLI does not read policies out of a bundle by receipt index. Instead, extract the root receipt payload or save its policy object to a separate JSON file, then run:

pnpm exec drs policy root-policy.json

Use your application-side consent records to relate the translated policy text back to the stored policy_hash.

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 @okeyamy/drs-sdk
  • access to a drs-verify instance you trust, including one you run yourself

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.

Verification

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

Or run your own verifier and point the CLI at it:

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

Signature model

Each DRS JWT is an EdDSA JWT. The issuer DID encodes the Ed25519 public key:

did:key:z6Mk{base58btc(0xed01 + public_key_bytes)}

That lets any verifier derive the public key from the DID without contacting the original operator.

Export Evidence for EU AI Act

There is no dedicated drs audit export command in the current CLI. If you need EU AI Act evidence today, assemble it from three artifacts:

  1. the bundle JSON itself
  2. the drs verify result
  3. the drs audit output

Current workflow

pnpm exec drs verify bundle.json > verify.txt
pnpm exec drs audit bundle.json > audit.txt
cp bundle.json eu-ai-act-bundle.json

What you can include today

  • the signed delegation chain (bundle.json)
  • verifier output proving whether the chain is valid
  • the audit trail showing issuer, audience, command, expiry, and tool server
  • any external policy/consent records your application stored alongside the DRS flow

What is not automated yet

The repo does not currently ship:

  • drs audit export
  • EU AI Act-specific JSON schemas
  • batch export by date range or subject

Those remain documentation and tooling work for a future release.

HIPAA Audit Evidence

For healthcare deployments handling PHI, DRS can provide tamper-evident proof of authorization and invocation activity. The current tooling is useful, but it is not yet a dedicated HIPAA export pipeline.

What DRS can show today

HIPAA concernCurrent DRS evidence
Record activity in PHI systemssigned invocation receipt
Verify authorization before accesssigned delegation chain
Tamper evidenceEd25519 signatures + prev_dr_hash chain
Independent verificationdid:key-based signature checks

Current evidence workflow

pnpm exec drs verify bundle.json > verify.txt
pnpm exec drs audit bundle.json > audit.txt

Archive these outputs together with the original bundle.json.

Storage caveat

The canonical storage model points regulated deployments toward Tier 3 / Tier 4 postures, but the current implementation does not enforce WORM semantics on the filesystem backend. RFC 3161 timestamping is available when TSA_URL is set, and TSA failures are best-effort.

See:

What is not implemented

The current repo does not ship:

  • drs audit retrieve
  • drs audit export --format hipaa
  • a HIPAA-specific export schema

If you need HIPAA packaging today, build it from the raw bundle plus verifier and audit outputs.

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/OkeyAmy/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-source-of-truth.md — current implementation contract
  3. False Positives: What We Tried — the v1 and v2 failures

docs/Drs_architecture_v2.md is still useful, but as a historical prior-working-path document rather than the live implementation spec.

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) → sign → JWT string

Verification (Go):
  JWT string → parse header/payload → resolve_did → crypto/ed25519.Verify
            → check_policy_attenuation → revocation checks → VerificationResult

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. Add or extend shared conformance vectors when the protocol surface changes
  3. Port it to Go if the verifier needs the same rule on the hot path
  4. Keep TypeScript logic aligned with the conformance contract

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: JWT-based DRS for OAuth/MCP ecosystems (current)

The pivot: From UCAN to JWT-based DRS aligned with the OAuth/MCP ecosystem.

Why the pivot: The ecosystem standardised on JWT-based infrastructure around OAuth and MCP. 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 envelopes/CBOR assumptions → DRS JWT receipts with RFC 8785 JCS canonicalisation
  • 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 @okeyamy/drs-sdk.

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

Or run it without a global install:

pnpm exec drs <command>

drs verify

Verify a bundle against a running drs-verify service.

drs verify [--include-timestamps] <bundle.json>

The CLI reads the verifier base URL from DRS_VERIFY_URL. If unset, it uses http://localhost:8080.

Examples:

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

# Ask the server to retrieve and verify RFC 3161 timestamp tokens
drs verify --include-timestamps bundle.json

Exit codes: 0 = valid, 1 = invalid or command error.


drs audit

Print a human-readable audit trail for a bundle file.

drs audit <bundle.json>

Current output includes:

  • bundle version
  • receipt count
  • iss, aud, cmd, exp for each receipt
  • iss, cmd, tool_server for the invocation

It does not currently export regulatory evidence packages or retrieve bundles by invocation ID.


drs policy

Translate a policy JSON file or a JSON document with a top-level policy field.

drs policy <receipt.json>

The command does not support --receipt. If you want the policy from a bundle, extract one receipt payload first or save the policy to its own JSON file.


drs translate

Translate a policy JSON object to plain English.

drs translate <policy.json>

drs keygen

Generate a new Ed25519 keypair for development or testing.

drs keygen

Current output:

Ed25519 keypair generated.

DID          : did:key:z6Mk...
Public key   : <hex>
Private key  : <hex>

Security: the private key is printed in plaintext hex. Do not commit it. Use a proper KMS or HSM for production keys.

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

Optional: request-body binding check

POST /verify accepts an optional body field in the JSON request — the parsed request body the tool server received from its client. When present, drs-verify canonicalises both the body and invocation.args using RFC 8785 (JCS) and reports the relationship in result.binding:

binding valueMeaning
"match"Body canonically equals invocation.args. The body is bound to what was signed.
"mismatch"Chain verified but body diverges from args. Likely tampering between signing and execution.
"invalid_body"Body was included but could not be parsed as JSON.
(field absent)Body was not sent; no binding check ran.

result.valid stays cryptographic truth (chain + policy + signature). binding is a distinct signal; the tool server decides what to do with "mismatch". A common pattern:

if (!result.valid) return reject(result.error);
if (result.binding === "mismatch") return reject({ code: "BINDING_MISMATCH" });
// proceed to execute the tool against the verified body

Example request:

POST /verify
{
  "bundle_version": "4.0",
  "invocation": "<invocation-receipt-jwt>",
  "receipts": ["<root-dr-jwt>", "<sub-dr-jwt>"],
  "body": { "tool": "approve_payment", "transaction_id": "T1" }
}

Example response with binding match:

{
  "valid": true,
  "context": { ... },
  "binding": "match"
}

What drs-verify does NOT do

drs-verify is a verification service only. It does not proxy, transform, or execute MCP/A2A traffic. Tool servers own their own endpoints and call POST /verify on a local drs-verify instance for each request. See examples/drs-expense-agent/src/tool-server.ts for the canonical tool-server pattern, or import github.com/drs-protocol/drs-verify/pkg/middleware for in-process Go integrations.


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.


GET /metrics

Prometheus exposition endpoint.

GET /metrics

The endpoint is unauthenticated and exempt from the built-in rate limiter so monitoring systems can scrape it reliably. In production, expose /metrics only to your monitoring network through your reverse proxy, firewall, service mesh, or Kubernetes NetworkPolicy.


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

By default, local revocation is in-memory and affects only the current drs-verify process. Set REVOCATION_STORE_PATH to enable the file-backed local revocation store; successful /admin/revoke calls are appended and fsynced so they survive process restart on that instance.

For multi-instance or cross-region durability, update the W3C Bitstring Status List at your STATUS_LIST_BASE_URL endpoint. The file-backed local store is not a distributed revocation backend.

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.
LOG_FORMATtextLog format: text or json. Use json for log aggregation.
SERVER_IDENTITYThis verifier's DID or server identifier. When set, /verify rejects invocations whose tool_server does not match. Empty disables destination binding.
DRS_ADMIN_TOKENBearer token required for POST /admin/revoke. If not set, the endpoint responds 503. No default — set explicitly to enable.
REVOCATION_STORE_PATHOptional file path for durable local /admin/revoke state. Empty uses in-memory local revocation only.
NONCE_STORE_BACKENDmemoryReplay-protection backend: memory for single-process deployments, redis for restart-safe and multi-replica deployments.
REDIS_URLRequired when NONCE_STORE_BACKEND=redis. Supports redis:// and rediss:// URLs.
TRUST_PROXYfalseWhen true, rate limiting uses the rightmost X-Forwarded-For entry. Enable only behind a trusted reverse proxy.
RATE_LIMIT_PER_IP100Sustained requests per second per client IP.
RATE_LIMIT_GLOBAL1000Sustained requests per second across all clients.
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.
TSA_ROOT_CERT_PEMOptional PEM root pool for RFC 3161 timestamp verification. Empty uses system roots.
METRICS_ADDRListen address for the separate Prometheus /metrics endpoint (e.g. :9090 for dev, 127.0.0.1:9090 for production). Empty disables the metrics endpoint. Served on its own listener so it can be firewalled independently of the main API port.

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}"
      REVOCATION_STORE_PATH: "/data/revoked.log"
      NONCE_STORE_BACKEND: "memory"
      SERVER_IDENTITY: "did:key:z6MkToolServer..."
      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 a JWT-based design aligned with the OAuth ecosystem 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 JWT receipts, JCS canonicalization, and DRS-specific fields in a shape that fits the surrounding OAuth/MCP ecosystem.

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 still a research project, but several items previously listed as future work are now implemented.

Phase 1 — Core protocol

Status: Mostly complete

  • drs-core: Rust crypto primitives, JCS canonicalisation, chain hash, policy primitives
  • drs-verify: Go verification server, MCP/A2A middleware, DID resolver cache, revocation, local revoke endpoint
  • drs-sdk: TypeScript issuance SDK and CLI (verify, audit, policy, translate, keygen)
  • ✓ Shared conformance suite across Rust, Go, and TypeScript
  • ✓ RFC 3161 timestamping support
  • did:web resolver SSRF hardening and circuit breaker

Phase 2 — Production hardening

  • secure-by-default Node HTTP enforcement middleware (@drs/mcp-server)
  • HSM / KMS integration in the verifier
  • durable object-store backend (Tier 2 roadmap)
  • stronger retention / immutability story for regulated deployments
  • external security review
  • repeatable performance benchmarks
  • workspace-level release and CI orchestration

Phase 3 — Ecosystem integration

  • richer MCP/A2A integration guidance and examples built around reusable middleware
  • browser-focused verification flows using the WASM build
  • stronger TypeScript packages for pure JSON-RPC MCP transport
  • Ethereum anchoring as explicit Tier 5 opt-in
  • richer policy language extensions

Phase 4 — Standards track

  • standards-track documentation and draft work
  • external governance / interoperability alignment
  • stronger regulatory reference material once implementation catches up

Non-goals

  • behavioral safety or prompt injection prevention
  • model determinism
  • post-compromise key recovery
  • DID lifecycle management outside DRS itself

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 is designed to address the same chain-splicing problem space, but this repository does not itself implement RFC 8693 token-exchange flows.

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.