Skip to main content

Webhook Signature Validation

To ensure the security and integrity of webhook notifications, Ripio signs the request payload using the ECDSA (Elliptic Curve Digital Signature Algorithm) with the P-256 curve. This allows you to verify that the webhook payload was sent by Ripio and has not been tampered with during transmission.

Overview of Signature Validation

Webhook requests from Ripio will include an X-Signature-Ecdsa-Sha256 header. This header contains the ECDSA signature of the request payload. You will need to use Ripio’s public key to verify this signature.

Validating the Signature: Step-by-Step Guide

  1. Retrieve the Signature Header: Extract the value of the X-Signature-Ecdsa-Sha256 header from the incoming webhook request. The signature is Base64 encoded.
  2. Extract the Raw Payload: Get the raw JSON payload from the body of the webhook request. It is crucial to use the raw, unmodified payload as it was received.
  3. Verify the Signature: Using Ripio’s public key for the P-256 curve, verify the signature against the raw payload. If the signature is valid, the webhook is authentic.

Important Notes for Signature Validation

  • Exact Payload: Ensure that the payload used for verification is exactly as it was sent by Ripio. Any modification will cause the verification to fail. The JSON payload must be serialized into a compact string, with no whitespace after separators (e.g., {"key":"value"} instead of {"key": "value"}).
  • Secure Public Key Storage: Store Ripio’s public key securely.
  • Reject Invalid Requests: Always reject requests with missing or invalid signatures.

Webhook Events

For details on specific webhook events and their payload structures, please refer to the following pages:

Configuring Your Webhook Endpoint

To receive webhook notifications, you need to configure a publicly accessible HTTPS URL endpoint in your Ripio partner settings. Ripio will send POST requests to this URL with a JSON payload. Key considerations for your endpoint:
  • HTTPS: Your endpoint URL must use HTTPS.
  • Respond Quickly: Your endpoint should acknowledge receipt of the webhook by returning a 2xx HTTP status code (e.g., 200 OK or 202 Accepted) as quickly as possible, ideally within 10 seconds as per Ripio’s guidelines.
  • Asynchronous Processing: For any time-consuming processing of the webhook data, perform it asynchronously (e.g., using a message queue) to ensure your endpoint responds promptly.
  • Idempotency: Design your webhook handler to be idempotent. This means that processing the same event multiple times should not result in duplicated actions or inconsistent state, as network issues or retries might cause a webhook to be delivered more than once.
For specific instructions on configuring your webhook URL within your Ripio account, please reach out to the Ripio technical team.

Basic Endpoint Implementation Examples

Here are basic examples of how you might set up an endpoint to receive webhooks and validate ECDSA signatures.

Python (Flask Example)

from flask import Flask, request, abort
import json
import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.exceptions import InvalidSignature

app = Flask(__name__)

# Replace with your actual public key provided by Ripio
RIPIO_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----"""

def verify_signature(payload_bytes: bytes, signature_header: str, public_key_pem: str) -> bool:
    """Verifies the ECDSA signature of the raw payload."""
    try:
        public_key = ec.from_pem_public_key(public_key_pem.encode("utf-8"))
        signature = base64.b64decode(signature_header)
        public_key.verify(signature, payload_bytes, ec.ECDSA(hashes.SHA256()))
        return True
    except (InvalidSignature, TypeError, ValueError):
        return False

@app.route('/ripio-webhook-handler', methods=['POST'])
def ripio_webhook_handler():
    received_signature = request.headers.get('X-Signature-Ecdsa-Sha256')
    
    # Get raw body bytes for signature verification
    raw_payload_bytes = request.get_data()

    if not received_signature:
        print("Error: Missing X-Signature-Ecdsa-Sha256 header")
        abort(400, 'Missing signature header')

    if not raw_payload_bytes:
        print("Error: Missing payload")
        abort(400, 'Missing payload')

    if not verify_signature(raw_payload_bytes, received_signature, RIPIO_PUBLIC_KEY):
        print(f"Error: Invalid signature. Received: {received_signature}")
        abort(403, 'Invalid signature')

    # If signature is valid, parse the JSON payload for processing
    try:
        payload_json = json.loads(raw_payload_bytes.decode('utf-8'))
    except json.JSONDecodeError:
        print("Error: Could not decode JSON payload")
        abort(400, 'Invalid JSON payload')

    print("Webhook received and signature validated!")
    print("Event Type:", payload_json.get("eventType"))
    print("Payload:", payload_json)

    # TODO: Add your asynchronous business logic here
    # (e.g., add to a queue for processing)

    return "Webhook processed successfully", 200

if __name__ == '__main__':
    # For development only. Use a proper WSGI server in production.
    app.run(port=5000, debug=True)

Node.js (Express Example)

const express = require('express');
const crypto = require('crypto');
const bodyParser = require('body-parser');

const app = express();
const port = 3000;

// Replace with your actual public key provided by Ripio
const RIPIO_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----`;

// Middleware to get raw body for signature verification
app.use(bodyParser.json({
  verify: (req, res, buf) => {
    if (buf && buf.length) {
      req.rawBody = buf; // Keep it as a buffer
    }
  }
}));

app.post('/ripio-webhook-handler', (req, res) => {
  // Headers are lowercased by some servers/frameworks
  const receivedSignature = req.headers['x-signature-ecdsa-sha256']; 

  if (!receivedSignature) {
    console.error('Error: Missing X-Signature-Ecdsa-Sha256 header');
    return res.status(400).send('Missing signature header');
  }

  if (!req.rawBody) {
    console.error('Error: Missing raw body payload');
    return res.status(400).send('Missing payload');
  }

  try {
    const verify = crypto.createVerify('sha256');
    verify.update(req.rawBody);
    
    const signatureBuffer = Buffer.from(receivedSignature, 'base64');
    
    const isSignatureValid = verify.verify(RIPIO_PUBLIC_KEY, signatureBuffer);

    if (!isSignatureValid) {
      console.error(`Error: Invalid signature.`);
      return res.status(403).send('Invalid signature');
    }

    // Signature is valid, req.body contains the parsed JSON payload
    const payload = req.body; 
    console.log('Webhook received and signature validated!');
    console.log('Event Type:', payload.eventType);
    console.log('Payload:', payload);

    // TODO: Add your asynchronous business logic here
    // (e.g., add to a message queue for processing)

    res.status(200).send('Webhook processed successfully');
  } catch (error) {
    console.error('An error occurred during signature verification:', error);
    res.status(500).send('Internal server error');
  }
});

app.listen(port, () => {
  console.log(`Webhook handler listening at http://localhost:${port}/ripio-webhook-handler`);
});