Signed Message Validation
Every response and every webhook from Nopan is digitally signed to guarantee authenticity and integrity.
Both are signed with the same keys. In this guide, a signed message is either of them.
By validating the signature with Nopan’s public key, you ensure that:
- The signed message came from Nopan (authenticity).
- It has not been tampered with in transit (integrity).
- It cannot later be repudiated (non-repudiation).
We highly recommend our clients to validate the signatures of responses and webhooks.
Step 1: Extract Signature Headers
Every Nopan response and webhook includes:
SignatureSignature-InputExample headers:
- Response
- Webhook
Signature: nopan_sig=:SflKxwRJSMe...abc123=:
Signature-Input: nopan_sig=("content-digest" "content-type" "content-length" "@status");keyid="nopan-key-id";created=1766678793
Signature: nopan_sig=:MIGIAkIB...=:
Signature-Input: nopan_sig=("@authority" "@method" "@request-target" "content-digest" "content-type" "content-length");keyid="nopan-key-id";created=1766678793
Take keyid from the Signature-Input header and select the key with the matching kid in Nopan's JWKS endpoint.
Do not hardcode the key id. If no key matches, fetch the JWKS again once before rejecting the signed message: Nopan may have rotated its key.
Step 2: Build the Signature Base
Just like for requests, you must reconstruct the signature base string.
Build the base from the components listed in Signature-Input, in that order. Choose the signed message you are verifying:
- Response
- Response (no body)
- Webhook
Example reconstructed base:
"content-digest": sha-512=:Base64EncodedHashOfBody==:
"content-type": application/json
"content-length": 123
"@status": 200
"@signature-params": nopan_sig=("content-digest" "content-type" "content-length" "@status");keyid="nopan-key-id";created=1766678793
For responses without a body, content-digest, content-type, and content-length are omitted.
"@status": 204
"@signature-params": nopan_sig=("@status");keyid="nopan-key-id";created=1766678793
A webhook is a request that Nopan sends to your endpoint, so it is built from request components, like in Request Signing.
Example reconstructed base:
"@authority": webhooks.example.com
"@method": POST
"@request-target": /nopan/webhook
"content-digest": sha-512=:Base64EncodedHashOfBody==:
"content-type": application/json
"content-length": 315
"@signature-params": nopan_sig=("@authority" "@method" "@request-target" "content-digest" "content-type" "content-length");keyid="nopan-key-id";created=1766678793
Hash the body as received, not a re-serialized JSON, and compare it with content-digest before verifying the signature.
Nopan signs responses and webhooks using sha-512 by default for the content-digest header.
When verifying, always read the algorithm prefix from the header value (e.g. sha-512=:...:
or sha-256=:...:) rather than assuming a fixed algorithm.
Supported algorithms: sha-256, sha-384, sha-512.
Step 3: Verify with Nopan’s Public Key
Nopan provides you with its public verification key that could be found here.
You must use this key to verify the Base64-decoded Signature against the signature base.
It is recommended to cache the public verification key and renew it at least every 24 hrs.
Examples verifying a response signature. To verify a webhook, use the webhook signature base from Step 2:
- Java
- JavaScript
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class VerifyResponse {
public static void main(String[] args) throws Exception {
// The signature base — built from the response's components.
// See understanding-signatures for the canonicalization rules.
String signatureBase = ""
+ "\"content-digest\": sha-512=:abc123==:\n"
+ "\"content-type\": application/json\n"
+ "\"content-length\": 123\n"
+ "\"@status\": 200\n"
+ "\"@signature-params\": nopan_sig=(\"content-digest\" \"content-type\" \"content-length\" \"@status\");keyid=\"nopan-key-id\";created=1766678793";
// The verbatim value of the response's Signature header.
String signatureHeader = "nopan_sig=:SflKxwRJSMe...abc123=:";
// Strip the nopan_sig=: prefix and trailing : to get the raw base64 signature.
String signatureValue = signatureHeader
.replaceFirst("^nopan_sig=:", "")
.replaceFirst(":$", "");
// Load Nopan's public key (cached from the JWKS endpoint).
String pem = Files.readString(Paths.get("nopan_public_key.pem"));
byte[] keyBytes = Base64.getDecoder().decode(pem
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", ""));
PublicKey publicKey = KeyFactory.getInstance("EC")
.generatePublic(new X509EncodedKeySpec(keyBytes));
// Nopan signs responses with P-521, so use SHA512withECDSA.
Signature verifier = Signature.getInstance("SHA512withECDSA");
verifier.initVerify(publicKey);
verifier.update(signatureBase.getBytes(StandardCharsets.UTF_8));
boolean valid = verifier.verify(Base64.getDecoder().decode(signatureValue));
System.out.println("Signature valid: " + valid);
}
}
const fs = require('fs');
const { createVerify } = require('crypto');
// The signature base — built from the response's components.
const signatureBase =
'"content-digest": sha-512=:abc123==:\n' +
'"content-type": application/json\n' +
'"content-length": 123\n' +
'"@status": 200\n' +
'"@signature-params": nopan_sig=("content-digest" "content-type" "content-length" "@status");keyid="nopan-key-id";created=1766678793';
// The verbatim value of the response's Signature header.
const signatureHeader = 'nopan_sig=:SflKxwRJSMe...abc123=:';
// Strip the nopan_sig=: prefix and trailing : to get the raw base64 signature.
const signatureValue = signatureHeader
.replace(/^nopan_sig=:/, '')
.replace(/:$/, '');
// Load Nopan's public key (cached from the JWKS endpoint).
const publicKeyPem = fs.readFileSync('nopan_public_key.pem', 'utf8');
// Nopan signs responses with P-521, so use 'SHA512'.
const verifier = createVerify('SHA512');
verifier.update(signatureBase);
verifier.end();
const valid = verifier.verify(publicKeyPem, Buffer.from(signatureValue, 'base64'));
console.log(`Signature valid: ${valid}`);
Step 4: Handle Validation Results
If the verification succeeds, you can trust the signed message.
If it fails, treat the signed message as invalid and do not process it.
- Reject unsigned or invalidly signed responses and webhooks.
- Log validation failures with enough detail for debugging.
- Check that your signature base construction matches canonicalization rules.
Error Handling
When validation fails, apply the following best practices:
Never process financial data or trigger business logic on unverified responses or webhooks.