Skip to content

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

FieldTypeDescription
algstringSignature algorithm: ES256K, ES256, RS256, or Ed25519
kidstringKey ID of the key being presented
typstringAlways gnap-binding-jwsd
htmstringHTTP method: POST, PUT, GET, DELETE
uristringFull URL you're calling
creatednumberUnix timestamp in milliseconds
athstringBase64url-encoded SHA256 hash of access token, if present

Creating the ath value

  1. Take the accessToken from your authorization request
  2. Calculate SHA256 hash
  3. 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:
  1. Check if input is a number and handle special cases for NaN and Infinity
  2. If input is null or not an object, convert to JSON string
  3. If object has a toJSON method, canonicalize the result
  4. For arrays:
    • Iterate through elements
    • Convert undefined or symbols to null
    • Canonicalize each element
  5. For objects:
    • Sort keys alphabetically
    • Filter out undefined and symbols
    • Canonicalize each key-value pair
  6. 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

  1. Canonicalize the entire HTTP request body using the same canonicalization steps
  2. Calculate SHA256 hash of the canonicalized body
    • If no body exists, use an empty payload
  3. Base64url encode the body hash
const canonicalBody = canonicalize(requestBody)
const bodyHash = sha256(canonicalBody || '')
const encodedPayload = base64url(bodyHash)

Step 5: Sign the JWS

  1. Create signature input: ${encodedHeader}.${encodedPayload}
  2. 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.