Skip to main content

Understanding Signatures

Nopan requires every request and response to be digitally signed.
This ensures three critical security guarantees:

  • Authenticity - only the holder of the private key could have created the signature.
  • Integrity - any modification of the request or response payload invalidates the signature.
  • Non-repudiation - a party cannot later deny having sent a signed message.

This guide explains the concepts behind signatures so that you can better understand how they are built and verified.


What is a Digital Signature?

A digital signature is a cryptographic proof attached to an HTTP message.
It is created by applying a private key to a canonicalized message string (the signature base) and sending it along with http headers.
The recipient verifies it with the matching public key.

  • Private key : Used only by the client, must be kept secret.
  • Public key : Shared with Nopan to allow verification.
  • Signature base : Canonical representation of important HTTP fields.
  • Signature : Base64-encoded output of signing the base with your private key.
Important

The signature doesn’t change the content - it proves who sent it and that it hasn’t been tampered with.

Anatomy of a Signature

A signature in Nopan is represented by two HTTP headers:

Header
Purpose
Signature
Contains the cryptographic signature value (Base64-encoded string).
Signature-Input
Describes how the signature was constructed (components, key, timestamp).
info

We follow the RFC-9421 specification recommendations for HTTP Message Signatures. For more details, refer to this document.

Example

Signature: nopan_sig=:lP3D...kQ==:
Signature-Input: nopan_sig=("@authority" "@method" "@request-target" "content-digest" "content-type" "content-length");keyid="your-key-id";created=1766678793
  • nopan_sig is the label for the signature instance.
  • @authority @method @request-target, etc. are the inputs.
  • keyid tells Nopan which key to use to verify the signature.
  • created is a UNIX timestamp (seconds) in UTC when the signature was created.

What is the Signature Base?

The signature base is the exact string that gets signed.
It is built by combining selected HTTP headers and pseudo-headers in a strict, ordered format, each on a new line.
It's crucial that both the client and the server construct this string in the exact same way.

  • Each component follows the format "component-name": value
  • Component names (but not values) are always lowercased
  • Each component is placed on its own line
  • Each line is concatenated with a newline character (\n), hex value 0x0a. There is no trailing newline.

Components of HTTP signature base

Component
Description
Example
@authority
The hostname of the API server, lowercased.
api.sandbox.nopan.dev (sandbox), or api.nopan.io (prod)
@method
The HTTP method in uppercase
GET, POST
@request-target
The path and query string of the API endpoint
/payments/initiate
content-digest
The Base64-encoded hash of the request body, wrapped between two colon (:) characters (e.g. sha-256=:CONTENT_DIGEST:). Supported algorithms: sha-256, sha-384, sha-512. Nopan uses sha-512 by default for response digests.
sha-256=:Ccp+TqpuiunH0mEWcSkYSINkTQffuny/vEyKLgg2DVs=: for `hello, world`
content-type
The media type of the request body, without parameters (e.g. charset), lowercased.
application/json
content-length
The size of the request body in bytes.
12 for UTF-8 encoded "hello, world"
@status
The HTTP response status (e.g. 200)
200, 404
@signature-params
List of signature inputs used to construct the signature

Normalization

There are some components that retain their meaning regardless of case. EXAMPLE.COM and example.com are semantically equivalent, but they will yield different results after applying a hash function to them.

To ensure the integration with Nopan is successful and to avoid sudden discrepancies, please apply the following normalization rules:

Components of HTTP signature base

Component
Normalization
Example
@authority
Lowercased.
"api.nopan.io" → "api.nopan.io"
@method
Use canonical uppercase.
"post" → "POST"
@request-target
Percent-encoded as an ASCII URI string, as in RFC 3986
"/payments/café?q=über" → "/payments/caf%C3%A9?q=%C3%BCber"
content-digest
None. Make sure to follow RFC 4648, Section 4 (base64 with + and / characters; and = for end padding)
"sha-256=:abc+def/==:"
content-type
Parameters stripped (everything after ";"), then lowercased.
"Application/JSON; charset=utf-8" → "application/json"
content-length
None — use the "Content-Length" header value as-is. This is always the body size in bytes, not the character count.
"13"
@status
None.
"200" (three characters)

Constructing @signature-params

All components are taken either from headers or pseudo headers, except for @signature-params. To construct it, take the name of the signature (nopan_sig), add a = sign, and concatenate signature inputs using ; as a separator.

Example:

"@signature-params": nopan_sig=("@authority" "@method" "@request-target" "content-digest" "content-type" "content-length");keyid="your-key-id";created=1766678793

Once the signature base is constructed it should look like this:

"@authority": api.nopan.io
"@method": POST
"@request-target": /payments/initiate
"content-digest": sha-256=:Base64EncodedHashOfBody==:
"content-type": application/json
"content-length": 123
"@signature-params": nopan_sig=("@authority" "@method" "@request-target" "content-digest" "content-type" "content-length");keyid="your-key-id";created=1766678793

Request vs Response Signatures

While the principle is the same, the components included differ:

Signature component usage

Component
Request
Response
@authority
@method
@request-target
content-digest
✓ if body not empty
✓ if body not empty
content-type
✓ if body not empty
✓ if body not empty
content-length
✓ if body not empty
✓ if body not empty
@status
@signature-params

This ensures both request authenticity and response authenticity.


Why Strict Canonicalization Matters

Even a minor change - like an extra space, uppercase method name, or different header order - would produce a completely different hash.
That’s why canonicalization rules are strict: both client and server must generate the exact same signature base to validate each other.

info

If your integration fails signature validation, the first thing to check is whether your signature base string is built identically to Nopan’s expectations.

Putting it All Together

Here’s how the signing process fits conceptually:

  1. Select required components (headers & pseudo-headers).
  2. Build the signature base string.
  3. Sign it with your private key.
  4. Attach the Signature and Signature-Input headers.
  5. Nopan validates using your public key.

And in reverse for responses: Nopan signs the response, and you validate using Nopan’s public key.