Skip to main content

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).
tip

We highly recommend our clients to validate the signatures of responses and webhooks.

Step 1: Extract Signature Headers​

Every Nopan response and webhook includes:

Header
Purpose
Signature
The cryptographic signature value (Base64-encoded).
Signature-Input
Describes how the signature was built (components, key, timestamp).

Example headers:

Signature: nopan_sig=:SflKxwRJSMe...abc123=:
Signature-Input: nopan_sig=("content-digest" "content-type" "content-length" "@status");keyid="nopan-key-id";created=1766678793
Key identification

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:

Component
Description
Example
content-digest
Base64-encoded hash of body, wrapped in colons. Nopan uses SHA-512 by default.
sha-512=:abc123=:
content-type
Media type of the body, without parameters (e.g. charset), lowercased.
application/json
content-length
Size of body in bytes.
123
@status
The HTTP status code.
200
@signature-params
List of inputs used to build the signature.
See Signature-Input

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
Hash the raw body

Hash the body as received, not a re-serialized JSON, and compare it with content-digest before verifying the signature.

Content-Digest hash algorithm

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.

info

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:

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);
}
}

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:

Scenario
Recommended Action
Missing signature headers
Reject the signed message. Treat as untrusted.
Signature does not verify
Reject and log. Check canonicalization and use of correct public key.
Expired or revoked key
Reject. Ensure you have the latest Nopan public key.
warning

Never process financial data or trigger business logic on unverified responses or webhooks.