Amwal Tech logoDocs

Troubleshooting Webhook Signature Failures

Amwal signs every incoming webhook dispatch using RSA-PSS with SHA-256 and sends the base64-encoded signature in the X-Signature HTTP header. If your server rejects valid webhook events or signature verification returns false, follow this troubleshooting guide to resolve the failure.


1. The #1 Cause: Hashing Parsed JSON Instead of Raw Request Body

Rendering diagram...

Critical Implementation Rule

Never serialize a parsed JavaScript object (JSON.stringify(req.body)) or Python dict back into JSON to verify the signature! Key ordering, whitespace formatting, and float precision differ from Amwal's original payload stream, causing signature verification to fail. You must verify the signature against the raw unparsed byte stream.


2. Common Causes & Solutions

Failure CauseDiagnostic StepFix
Parsing middleware ran before verificationCheck if express.json() or body-parser parsed the request before your verifier ran.Use the verify callback in express.json({ verify: (req, res, buf) => req.rawBody = buf }) or express.raw().
Using wrong RSA Public KeyCheck if the Public Key in your .env matches the one returned during endpoint registration.Webhook public keys are provisioned via POST /api/create-webhook-and-apikey/. Store api_key.public_key securely as AMWAL_PUBLIC_KEY.
Incorrect RSA padding modeVerify whether RSA_PKCS1_PSS_PADDING is configured with saltLength: 32 bytes.In Node.js, pass padding: crypto.constants.RSA_PKCS1_PSS_PADDING and saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST. In Python, use padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.DIGEST_LENGTH).
Whitespace in headerHeader string contains trailing carriage returns or spaces.Run signature.trim() before passing it to base64 decoding.
Environment MismatchSandbox webhook dispatch verified against Production public key.Maintain distinct environment variables for Sandbox (AMWAL_PUBLIC_KEY_SANDBOX) and Production (AMWAL_PUBLIC_KEY_PROD).

3. Verified Verification Implementations

Node.js / Express

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

const app = express();

// 1. Capture raw request 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;

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. Verify RSA-PSS SHA-256 signature
  const verifier = crypto.createVerify('RSA-SHA256');
  verifier.update(req.rawBody);

  const isValid = verifier.verify(
    {
      key: AMWAL_PUBLIC_KEY,
      padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
      saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
    },
    signature,
    'base64'
  );

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

  // 3. Process valid event
  const payload = req.body;
  console.log(`Verified webhook event: ${payload.event_type}`);
  res.status(200).send('OK');
});

Python (FastAPI / Cryptography)

import base64
import os
from fastapi import FastAPI, Request, HTTPException
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_public_key

app = FastAPI()

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

@app.post("/webhook")
async def webhook_handler(request: Request):
    signature = request.headers.get("x-signature", "")
    if not signature:
        raise HTTPException(status_code=400, detail="Missing X-Signature header")

    # Read raw body bytes
    raw_body = await request.body()

    try:
        signature_bytes = base64.b64decode(signature)
        public_key.verify(
            signature_bytes,
            raw_body,
            padding.PSS(
                mgf=padding.MGF1(hashes.SHA256()),
                salt_length=padding.PSS.DIGEST_LENGTH
            ),
            hashes.SHA256()
        )
    except Exception:
        raise HTTPException(status_code=403, detail="Invalid signature")

    payload = await request.json()
    print(f"Processed event: {payload.get('event_type')}")
    return {"status": "ok"}

4. Manual Debugging with OpenSSL CLI

You can isolate code issues by testing a captured raw payload with OpenSSL:

# 1. Base64 decode the received X-Signature header
echo "BASE64_X_SIGNATURE_STRING" | base64 --decode > signature.bin

# 2. Verify against the raw saved JSON payload
openssl dgst -sha256 \
  -sigopt rsa_padding_mode:pss \
  -sigopt rsa_pss_saltlen:digest \
  -verify amwal_public_key.pem \
  -signature signature.bin \
  raw_payload.json

On this page