Amwal Tech logoDocs

Webhook Signature Verification

To guarantee authenticity and prevent man-in-the-middle tampering, Amwal signs every outgoing webhook dispatch using RSA-PSS with SHA-256. Merchants must verify the X-Signature header against the raw incoming request bytes before trusting or executing any payment events.


Cryptographic Specification

ParameterSpecificationDescription
AlgorithmRSA-PSSProbabilistic Signature Scheme (PKCS#1 v2.1).
Hashing AlgorithmSHA-256256-bit secure hash function.
Padding ModeRSA_PKCS1_PSS_PADDINGPSS padding mode with MGF1 mask generation.
Salt Length32 bytes (Digest length)Matches the SHA-256 output digest size (32 bytes).
Signature EncodingBase64Standard Base64 ASCII string passed in the X-Signature header.
Verification Keyapi_key.public_keyRSA Public Key (PEM format) received during Webhook Setup.

Webhook Delivery Headers

Every incoming webhook request contains the following authentication headers:

HeaderDescription
X-SignatureBase64-encoded RSA-PSS SHA-256 cryptographic signature.
X-Api-KeyWebhook API Key identifier prefix.
X-Amwal-KeyDelivery tracking key for idempotency checking.
Content-Typeapplication/json

Critical Rule: Verify Against Raw Body Bytes

Never Re-Serialize JSON Before Verification

Cryptographic hashing operates on the exact byte sequence sent over the wire. Re-serializing a parsed JavaScript object (JSON.stringify(req.body)) will alter whitespace, key ordering, and floating-point representations, causing verification to fail.

Always capture and verify against the raw unparsed request Buffer / byte array.


Inputs

Verification requires three values: the RSA public key, the webhook payload, and the Base64-encoded signature. Replace the placeholders below with your actual values:

const publicKeyPem = `-----BEGIN PUBLIC KEY-----
YOUR_AMWAL_PUBLIC_KEY_PEM_HERE
-----END PUBLIC KEY-----`;

const payload = {
  "amount": "29.00000",
  "client_first_name": "fname ",
  "client_last_name": "lname",
  "payment_link_id": "9493559e-yyyyyyb",
  "payment_option": "Pay In Full",
  "status": "success",
  "transaction_id": "ccdaaba6-xxxxx5"
};

const signatureBase64 = "amwal - signature";

Verification Implementations

Node.js / Express

import crypto from 'node:crypto';
import express from 'express';

const app = express();

// 1. Capture the raw body Buffer before JSON parsing
app.use(express.json({
  verify: (req, _res, buf) => {
    req.rawBody = buf;
  }
}));

const AMWAL_PUBLIC_KEY = process.env.AMWAL_PUBLIC_KEY || `-----BEGIN PUBLIC KEY-----
YOUR_AMWAL_PUBLIC_KEY_PEM_HERE
-----END PUBLIC KEY-----`;

function verifyAmwalSignature(rawBodyBuffer, signatureBase64) {
  try {
    const verifier = crypto.createVerify('RSA-SHA256');
    verifier.update(rawBodyBuffer);

    return verifier.verify(
      {
        key: AMWAL_PUBLIC_KEY,
        padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
        saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
      },
      signatureBase64,
      'base64'
    );
  } catch (err) {
    console.error('Signature verification threw an exception:', err);
    return false;
  }
}

app.post('/webhook', (req, res) => {
  const signature = req.headers['x-signature'];

  if (!signature || !req.rawBody) {
    return res.status(400).send('Missing X-Signature header or request body');
  }

  // 2. Perform verification
  const isValid = verifyAmwalSignature(req.rawBody, signature);

  if (!isValid) {
    console.warn('Rejected incoming webhook: Invalid RSA-PSS signature');
    return res.status(403).send('Invalid signature');
  }

  // 3. Process verified event
  const payload = req.body;
  console.log(`Verified webhook event: ${payload.event_type} (ID: ${payload.data?.id})`);

  res.status(200).send('OK');
});

Python (Cryptography)

import base64
import os
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_public_key

PUBLIC_KEY_PEM = os.getenv("AMWAL_PUBLIC_KEY").encode("utf-8")
public_key = load_pem_public_key(PUBLIC_KEY_PEM)

def verify_amwal_signature(raw_body_bytes: bytes, signature_base64: str) -> bool:
    try:
        signature_bytes = base64.b64decode(signature_base64)
        public_key.verify(
            signature_bytes,
            raw_body_bytes,
            padding.PSS(
                mgf=padding.MGF1(hashes.SHA256()),
                salt_length=padding.PSS.DIGEST_LENGTH
            ),
            hashes.SHA256()
        )
        return True
    except Exception as e:
        print(f"Signature mismatch: {e}")
        return False

CLI / OpenSSL Verification

For manual testing, debugging, or incident response, you can verify a captured raw webhook payload using the OpenSSL CLI:

# 1. Save signature to base64-decoded binary file
echo "BASE64_SIGNATURE_STRING_HERE" | base64 --decode > signature.bin

# 2. Verify signature against the raw payload file using RSA-PSS SHA-256
openssl dgst -sha256 \
  -sigopt rsa_padding_mode:pss \
  -sigopt rsa_pss_saltlen:digest \
  -verify amwal_public_key.pem \
  -signature signature.bin \
  raw_payload.json

Output on success:

Verified OK

Troubleshooting Common Verification Errors

SymptomCauseSolution
Verification fails on all valid payloadsUsing req.body parsed JSON instead of raw Buffer.Capture raw body buffer before JSON middleware runs.
Signature decoding errorWhitespace or newlines inside X-Signature header string.Trim whitespace (signature.trim()) before Base64 decoding.
Invalid public key formatMissing -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY----- headers.Ensure public key PEM format includes exact standard delimiters.
Mismatch between environmentsSandbox webhooks verified using Production Public Key (or vice-versa).Isolate AMWAL_PUBLIC_KEY_SANDBOX and AMWAL_PUBLIC_KEY_PROD environment variables.

On this page