Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Request Binding – ERC-8128
Skip to content

Request Binding

Request binding determines which parts of the HTTP request are cryptographically protected by the signature.

Request-Bound (Default)

For request-bound signatures, the library automatically selects components based on the request:

ConditionComponents Signed
All requests@authority, @method, @path
Has query string+ @query
Has body+ content-digest

Example for POST /orders?market=ETH with body:

Signature-Input: eth=("@authority" "@method" "@path" "@query" "content-digest");...

Why These Components?

@authority (Host)

Prevents the signature from being used on a different server.

Without @authority:

Attacker captures: POST https://api.example.com/transfer
Replays to: POST https://malicious.com/transfer ❌ (would work!)

With @authority:

Signature binds to api.example.com
Replay to malicious.com → verification fails ✓

@method

Prevents changing the HTTP method.

Without @method:

Signed: GET /resource
Attacker changes to: POST /resource ❌ (could create data!)

@path

Prevents using the signature on a different endpoint.

Without @path:

Signed: POST /api/v1/orders
Attacker uses: POST /api/v1/admin/delete ❌ (different action!)

@query

Prevents tampering with query parameters.

Without @query:

Signed: GET /search?q=cats
Attacker changes: GET /search?q=dogs&admin=true ❌

content-digest

Prevents tampering with the request body.

Without content-digest:

Signed: POST /transfer with body {"amount": "10"}
Attacker changes: POST /transfer with body {"amount": "1000000"} ❌

Content-Digest Header

For body integrity, ERC-8128 uses the Content-Digest header (RFC 9530):

Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:

The signature covers this header, and the server verifies that the header matches the actual body bytes.

Automatic Handling

When signing a request-bound request with a body, the Content-Digest header is computed and added automatically.

// Body present → Content-Digest added automatically
const signedRequest = await signRequest(
  'https://api.example.com/data',
  { method: 'POST', body: '{"value": 42}' },
  signer
)
// Content-Digest: sha-256=:hash: is added

Verification

During verification, the server checks that the Content-Digest header matches the actual body bytes. A mismatch indicates tampering.

const result = await verifyRequest({
  request,
  verifyMessage,
  nonceStore
})
 
// If content-digest is in components:
// 1. Header must exist
// 2. Hash must match body bytes
// Otherwise: result.reason === "digest_mismatch" or "digest_required"

Class-Bound Signatures

A class-bound signature authorizes a class of requests rather than a single concrete request. Instead of binding the signature to all request components (method, path, query, body), only the explicitly specified components are signed. Any request that matches the signed components is authorized by the same signature.

This is a signer-side decision: the signer chooses to produce a less specific signature. However, it is entirely up to the verifier whether to accept class-bound signatures. The ERC-8128 baseline requires every compliant verifier to accept request-bound signatures, but class-bound acceptance is optional — verifiers MAY reject them or require additional conditions.

Why Use Class-Bound Signatures

From the signer's perspective:
  • Reduced overhead — A single signature can authorize multiple requests (e.g., any endpoint on a domain), avoiding the cost of signing every request individually.
  • Performance — In high-frequency scenarios, computing and transmitting a new signature per request adds latency. A class-bound signature can be reused across requests that fall within the same class.
  • Broad authorization patterns — Useful for cases like "authorize any GET request to this API" without specifying each endpoint.
From the verifier's perspective:
  • Simpler validation for low-risk operations — For idempotent read endpoints, verifying that the request comes from a specific address on a specific domain may be sufficient without checking the exact path or query.
  • Reduced verification overhead — Fewer components to reconstruct and verify can reduce CPU cost at scale, especially when combined with replayable signatures where verification results can be cached.
  • Flexible authorization models — Enables patterns where a single proof authorizes access to a set of resources rather than a single endpoint.

Signing Class-Bound Requests

When using binding: 'class-bound', you must explicitly specify which components to sign via the components array. The library always includes @authority (required by the spec).

const signedRequest = await signRequest(
  request,
  signer,
  {
    binding: 'class-bound',
    components: ['@authority'],  // Only the domain
    replay: 'replayable',        // Often combined for performance
  }
)

Common Patterns

ComponentsAuthorization ScopeUse Case
['@authority']Any request to this domainDomain-level access tokens
['@authority', '@method']Any request with this method on this domain"Authorize all GETs"
['@authority', '@path']Any request to this specific endpointEndpoint access without method restriction
['@authority', '@method', '@path']Specific method + endpoint, any query/bodyRead endpoints where query params don't affect authorization

Security Tradeoffs

Each omitted component widens the class of requests the signature authorizes:

Omitted ComponentRiskWhen Acceptable
@methodSignature valid for GET, POST, DELETE, etc. on the same targetAll methods on the target have equivalent authorization
@pathSignature valid for any endpoint on the domainAll endpoints share the same access level
@queryQuery parameters can be changed freelyQuery params don't affect authorization or data access
content-digestRequest body can be modifiedNo body, or body content doesn't affect authorization

Adding Extra Components

Sign additional headers alongside the default set:

const signedRequest = await signRequest(
  request,
  signer,
  { components: ['x-idempotency-key', 'x-request-id'] }
)
 
// Signature covers: @authority + @method + @path + @query + content-digest + x-idempotency-key + x-request-id

Server Enforcement with additionalRequestBoundComponents and classBoundPolicies

Verification behavior is controlled by two policy inputs:

  • additionalRequestBoundComponents adds required components in addition to the default request-bound set.
  • classBoundPolicies opts into class-bound signatures by listing acceptable component sets.

Default Behavior (no policy fields)

When neither field is set, the verifier enforces the full request-bound check. This means the signature must cover the complete set of components that the ERC-8128 spec requires for the specific request:

  • @authority, @method, @path — always required
  • @query — required if the request has a query string
  • content-digest — required if the request has a body

If the signature omits any of these (given the request shape), verification fails with not_request_bound. This is the strictest mode and the default — it rejects all class-bound signatures.

// Default: enforces full request-bound check
const result = await verifyRequest({
  request,
  verifyMessage,
  nonceStore
})
// A class-bound signature (e.g. only @authority) would be rejected

Adding Extra Request-Bound Components

Use additionalRequestBoundComponents to require additional components alongside the default set:

const result = await verifyRequest({
  request,
  verifyMessage,
  nonceStore,
  policy: {
  additionalRequestBoundComponents: ['x-api-version', 'x-idempotency-key'],
  },
})

The signature must now cover the request-bound set plus x-api-version and x-idempotency-key. Order does not matter; the verifier checks set inclusion.

Accepting Class-Bound Signatures

Use classBoundPolicies to opt in to class-bound signatures. Each policy list is an acceptable component set (order does not matter). If any policy is covered by the signed components, the signature can be accepted:

const result = await verifyRequest({
  request,
  verifyMessage,
  nonceStore,
  policy: {
  classBoundPolicies: [
    ['@authority', '@method'],
    ['@authority', '@path'],
  ],
  },
})

Request-bound signatures are always accepted when valid, even if classBoundPolicies is set. The verifier also requires @authority in every class-bound policy (it is added if missing).

Best Practices

  1. Use request-bound as the default — it's the safest option
  2. Sign idempotency keys — if your API uses them
  3. Be explicit about requirements — configure additionalRequestBoundComponents and classBoundPolicies on the server
  4. Audit class-bound usage — ensure you understand what's not being signed