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:
| Condition | Components 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 addedVerification
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.
- 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
| Components | Authorization Scope | Use Case |
|---|---|---|
['@authority'] | Any request to this domain | Domain-level access tokens |
['@authority', '@method'] | Any request with this method on this domain | "Authorize all GETs" |
['@authority', '@path'] | Any request to this specific endpoint | Endpoint access without method restriction |
['@authority', '@method', '@path'] | Specific method + endpoint, any query/body | Read endpoints where query params don't affect authorization |
Security Tradeoffs
Each omitted component widens the class of requests the signature authorizes:
| Omitted Component | Risk | When Acceptable |
|---|---|---|
@method | Signature valid for GET, POST, DELETE, etc. on the same target | All methods on the target have equivalent authorization |
@path | Signature valid for any endpoint on the domain | All endpoints share the same access level |
@query | Query parameters can be changed freely | Query params don't affect authorization or data access |
content-digest | Request body can be modified | No 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-idServer Enforcement with additionalRequestBoundComponents and classBoundPolicies
Verification behavior is controlled by two policy inputs:
additionalRequestBoundComponentsadds required components in addition to the default request-bound set.classBoundPoliciesopts 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 stringcontent-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 rejectedAdding 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
- Use request-bound as the default — it's the safest option
- Sign idempotency keys — if your API uses them
- Be explicit about requirements — configure
additionalRequestBoundComponentsandclassBoundPolicieson the server - Audit class-bound usage — ensure you understand what's not being signed