Webhook Signature Validation

To ensure the security and integrity of webhook notifications, Ripio includes an HMAC-SHA256 signature with each request. This signature allows you to verify that the webhook payload was indeed sent by Ripio and has not been tampered with during transmission.

Overview of Signature Validation

Webhook requests from Ripio will include an Http-X-Wh-Signature-256 header. This header contains the HMAC-SHA256 signature of the request payload, generated using a shared secret key provided to you by Ripio.

Validating the Signature: Step-by-Step Guide

  1. Retrieve the Signature Header: Extract the value of the Http-X-Wh-Signature-256 header from the incoming webhook request.
  2. Extract the Raw Payload: Get the raw JSON payload from the body of the webhook request. It’s crucial to use the raw, unmodified payload as it was received.
  3. Verify the Signature: Using your shared secret key, generate an HMAC-SHA256 signature for the extracted raw payload. Compare this generated signature with the signature received in the Http-X-Wh-Signature-256 header. If they match, the webhook is authentic.

Example Code (Python)

Here’s an example in Python demonstrating how to generate and verify the signature:
import hmac
import hashlib
import json

def generate_sha256_signature(payload: dict, secret: str) -> str:
    """
    Generate signature SHA256 using HMAC hex digest.

    Parameters:
    payload (dict): The payload of the webhook request.
    secret (str): The shared secret key.

    Returns:
    str: The generated HMAC-SHA256 signature, prefixed with "sha256=".
    """
    # Ensure the payload is dumped as a compact JSON string, matching how Ripio generates it.
    # Ripio's example uses json.dumps(payload) which might include spaces.
    # For consistency, ensure your server and Ripio use the exact same serialization.
    # If Ripio sends compact JSON (no spaces after separators), use:
    # payload_bytes = json.dumps(payload, separators=(',', ':')).encode("utf-8")
    # Based on the PDF's example, it seems to be standard json.dumps():
    payload_bytes = json.dumps(payload).encode("utf-8")

    hash_object = hmac.new(
        secret.encode("utf-8"),
        msg=payload_bytes,
        digestmod=hashlib.sha256
    )
    signature = "sha256=" + hash_object.hexdigest()
    return signature

def verify_signature(payload: dict, secret: str, signature_header: str) -> bool:
    """
    Verify that the payload was sent from a trusted source by validating SHA256 signature.

    Parameters:
    payload (dict): The payload of the webhook request.
    secret (str): The shared secret key.
    signature_header (str): The signature received in the `Http-X-Wh-Signature-256` header.

    Returns:
    bool: True if the signature is valid, False otherwise.
    """
    expected_signature = generate_sha256_signature(payload, secret)
    return hmac.compare_digest(expected_signature, signature_header)

# Example Usage (within a web framework like Flask)
# from flask import request, abort
#
# shared_secret = "your_shared_secret" # Replace with your actual shared secret
#
# @app.route('/webhook-receiver', methods=['POST'])
# def webhook_receiver():
#     received_signature = request.headers.get('Http-X-Wh-Signature-256')
#     payload = request.json # Assumes the payload is JSON
#
#     if not received_signature or not payload:
#         abort(400, 'Missing signature or payload')
#
#     if verify_signature(payload, shared_secret, received_signature):
#         # Process the valid webhook payload
#         print("Signature is valid. Processing payload:", payload)
#         # Add your business logic here
#         return "Webhook received and validated.", 200
#     else:
#         # Invalid signature
#         print("Invalid signature.")
#         abort(403, 'Invalid signature. Reject the request.')

Important Notes for Signature Validation

  • Exact Payload: Ensure that the payload used for generating the signature on your end is exactly as it was sent by Ripio. Any modification, even a minor one (like changes in whitespace or order of keys if not handled consistently), will result in a different signature.
  • Secure Secret Storage: Always use a secure method to store and handle your shared secret key. Do not embed it directly in your client-side code or commit it to version control.
  • Reject Invalid Requests: Reject any requests with missing or invalid signatures to prevent unauthorized access or processing of tampered data.
  • Timestamp Verification (Optional but Recommended): Consider checking the issueDatetime of the webhook event to prevent replay attacks, where an attacker resends an old, valid webhook. Define an acceptable time window for webhooks.

Webhook Event Categories

Ripio provides webhooks for two main categories of events:
  • On-Ramp Events: Notifications related to the process of converting fiat currency to cryptocurrency.
  • Off-Ramp Events: Notifications related to the process of converting cryptocurrency back to fiat currency.
Please refer to the specific pages for detailed information on event types and payload structures for each category.

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 signatures.

Python (Flask Example)

from flask import Flask, request, abort
import hmac
import hashlib
import json

app = Flask(__name__)

# Replace with your actual shared secret provided by Ripio
SHARED_SECRET = "your_ripio_shared_secret"

def generate_expected_signature(payload_bytes: bytes, secret: str) -> str:
    hash_object = hmac.new(
        secret.encode("utf-8"),
        msg=payload_bytes,
        digestmod=hashlib.sha256
    )
    return "sha256=" + hash_object.hexdigest()

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

    if not received_signature:
        print("Error: Missing Http-X-Wh-Signature-256 header")
        abort(400, 'Missing signature header')

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

    expected_signature = generate_expected_signature(raw_payload_bytes, SHARED_SECRET)

    if not hmac.compare_digest(expected_signature, received_signature):
        print(f"Error: Invalid signature. Expected: {expected_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 shared secret provided by Ripio
const SHARED_SECRET = 'your_ripio_shared_secret';

// Middleware to get raw body for signature verification
// Important: This needs to be before express.json() if you also want to use it
app.use(bodyParser.json({
  verify: (req, res, buf, encoding) => {
    if (buf && buf.length) {
      req.rawBody = buf.toString(encoding || 'utf8');
    }
  }
}));

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

  if (!receivedSignature) {
    console.error('Error: Missing Http-X-Wh-Signature-256 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');
  }

  const hmac = crypto.createHmac('sha256', SHARED_SECRET);
  const expectedSignature = 'sha256=' + hmac.update(req.rawBody).digest('hex');

  // Use crypto.timingSafeEqual for comparing signatures to prevent timing attacks
  const signaturesMatch = crypto.timingSafeEqual(
    Buffer.from(expectedSignature, 'utf8'),
    Buffer.from(receivedSignature, 'utf8')
  );

  if (!signaturesMatch) {
    console.error(`Error: Invalid signature. Expected: ${expectedSignature}, Received: ${receivedSignature}`);
    return res.status(403).send('Invalid signature');
  }

  // Signature is valid, req.body contains the parsed JSON payload (due to bodyParser.json())
  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');
});

app.listen(port, () => {
  console.log(`Webhook handler listening at http://localhost:${port}/ripio-webhook-handler`);
});
Remember to replace "your_ripio_shared_secret" with the actual secret provided by Ripio.