HTTP Request Signing
Secure API requests using cryptographic signatures.
Overview
API requests are signed using a client credential (a private key) with key-bound access tokens.
This provides:
- Strong authentication - Cryptographic proof of request origin
- Non-repudiation - Verifiable proof that cannot be forged
- Key binding - Access tokens bound to specific credentials
Most requests in require a http signature in a Detached JWS format.
Creating a Detached JWS
To create the Detached JWS signature for your HTTP request:
Step 1: Build the JWS Header
{
"alg": "ES256K",
"kid": "0x...",
"typ": "gnap-binding-jwsd",
"htm": "POST",
"uri": "https://auth.narval.xyz/v1/auth/offer",
"created": 1722461078706,
"ath": "8qSuB6Rj3fP2SC3Ddm7GfzCsMel4Pknptm1oRHwXs-8"
}Header Fields
| Field | Type | Description |
|---|---|---|
alg | string | Signature algorithm: ES256K, ES256, RS256, or Ed25519 |
kid | string | Key ID of the key being presented |
typ | string | Always gnap-binding-jwsd |
htm | string | HTTP method: POST, PUT, GET, DELETE |
uri | string | Full URL you're calling |
created | number | Unix timestamp in milliseconds |
ath | string | Base64url-encoded SHA256 hash of access token, if present |
Creating the ath value
- Take the
accessTokenfrom your authorization request - Calculate SHA256 hash
- Base64url encode the hash
const ath = base64url(sha256(accessToken))Step 2: Canonicalize the Header
Canonicalize the header according to RFC8785 to ensure consistent ordering:
Canonicalization Algorithm:- Check if input is a number and handle special cases for
NaNandInfinity - If input is
nullor not an object, convert to JSON string - If object has a
toJSONmethod, canonicalize the result - For arrays:
- Iterate through elements
- Convert
undefinedor symbols tonull - Canonicalize each element
- For objects:
- Sort keys alphabetically
- Filter out
undefinedand symbols - Canonicalize each key-value pair
- Return canonicalized string
Canonicalize Function
function canonicalize(object: any): string {
if (typeof object === 'number' && isNaN(object)) {
throw new Error('NaN is not allowed')
}
if (typeof object === 'number' && !isFinite(object)) {
throw new Error('Infinity is not allowed')
}
if (object === null || typeof object !== 'object') {
return JSON.stringify(object)
}
if (object.toJSON instanceof Function) {
return canonicalize(object.toJSON())
}
if (Array.isArray(object)) {
const values = object.reduce((t, cv, ci) => {
const comma = ci === 0 ? '' : ','
const value = cv === undefined || typeof cv === 'symbol' ? null : cv
return `${t}${comma}${canonicalize(value)}`
}, '')
return `[${values}]`
}
const values = Object.keys(object)
.sort()
.reduce((t, cv) => {
if (object[cv] === undefined || typeof object[cv] === 'symbol') {
return t
}
const comma = t.length === 0 ? '' : ','
return `${t}${comma}${canonicalize(cv)}:${canonicalize(object[cv])}`
}, '')
return `{${values}}`
}Step 3: Encode the Header
Base64url encode the canonicalized header:
const encodedHeader = base64url(canonicalizeHeader(header))Step 4: Prepare the Request Body
- Canonicalize the entire HTTP request body using the same canonicalization steps
- Calculate SHA256 hash of the canonicalized body
- If no body exists, use an empty payload
- Base64url encode the body hash
const canonicalBody = canonicalize(requestBody)
const bodyHash = sha256(canonicalBody || '')
const encodedPayload = base64url(bodyHash)Step 5: Sign the JWS
- Create signature input:
${encodedHeader}.${encodedPayload} - Sign using the key and algorithm referenced in the header
const signatureInput = `${encodedHeader}.${encodedPayload}`
const signature = sign(signatureInput, privateKey, algorithm)
const encodedSignature = base64url(signature)Step 6: Complete the JWS
Assemble the final detached JWS:
const detachedJWS = `${encodedHeader}.${encodedPayload}.${encodedSignature}`This value goes in the Detached-JWS header.

