Webhook Security
Webhook security for i-payout involves using digital signatures to ensure the authenticity and integrity of the webhook notifications. This process involves signing the webhook payload with a private key and verifying the signature using a public key.
Example Code
Here's a practical example of how to implement webhook security using digital signatures to ensure the authenticity and integrity of your notifications.
using System;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
namespace SHA256_CSharp_Example
{
internal class Program
{
static void Main()
{
string pemPublicKey = @"MIIBITANBgkqhkiG9w0BAQEFAAOCAQ4AMIIBCQKCAQBkijC/ALAtltCudY48ONLIC7iwjaKFb7SCACzRLvU6QcPvtN0SpnbVhMDpvmQJzQFoVqDc6f4IugPo/aG5MzrBS40lArFRP29+kPpKqhYftKWXf/DusTd++l592PT/G6QUUc2Leu4N10YRLOTJyy+mY1iaQNBjx5h64Az7vpQx4fG2MPi4LB+ondRYL5B/dWniigyn5dKZ9mcffr5ji1gmyg+43I+HSJGtXjwjDJ2GDzXqu4XuSUmJunlpuGxMlYarlUrGWAqBLb0DkNc05KCtJHvRTtM2sa60u+rtcWXke2zjcs7/jxHoP8E2SsD5gueWnv4LKlXzl0dw9sC29emNAgMBAAE=";
string signature = "RozdyIRwpRO6HojlfCQ8yviO8KF2rnvFPQOdpip78wwfujfmYFlGQDnAg1OKaNC3OvdVnS4Jkmpklhn+gzYkVFTbn1x+D0QTQqwj5ph/9k3gMIK7GuQl9+0vZzTfLpkuHoVcL/qNlAnJUfq2zVVDtwppZpNAzszDSHGaT1ZKYe189+EMaGNcKC6ay1WU1yeJLOz6QqUhe6Yl8jTVDq1sqM7EHVvIcA53bH9b6vHRFEq7MMSNS6/mRzPyZHMe+adieDE6ld1EUxh0VbZXT1TVgn3hNAX0GWIladqba8DPnURVEcGoU2/WqDIiuSQ0vl90yGrXSc2wzfk7OXqtx3GngQ==";
string dataToValidate = "1719489115#myNotification.com/webhook#{'webhookId':'123'}";
// Validate the signature
bool isValid = ValidateSignature(pemPublicKey, dataToValidate, signature);
Console.WriteLine("Signature is valid: " + isValid);
}
static byte[] ConvertFromPem(string pem)
{
return Convert.FromBase64String(pem);
}
static bool ValidateSignature(string publicKey, string dataToValidate, string signature)
{
byte[] signatureBytes = Convert.FromBase64String(signature);
byte[] publicKeyBytes = ConvertFromPem(publicKey);
byte[] dataBytes = Encoding.UTF8.GetBytes(dataToValidate);
using (RSA rsa = RSA.Create())
{
rsa.ImportSubjectPublicKeyInfo(publicKeyBytes, out _);
// Verify the signature using RSA and SHA-256
return rsa.VerifyData(dataBytes, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
}
}
}
const crypto = require('crypto');
const buffer = require('buffer');
// Function to convert PEM format key to Buffer
function convertFromPem(pem) {
return Buffer.from(pem.replace(/(-----(BEGIN|END) (PUBLIC|PRIVATE) KEY-----|\n)/g, ''), 'base64');
}
// Function to verify the signature
function validateSignature(publicKey, dataToValidate, signature) {
const signatureBytes = Buffer.from(signature, 'base64');
const publicKeyBuffer = convertFromPem(publicKey);
const dataBytes = Buffer.from(dataToValidate, 'utf8');
const verify = crypto.createVerify('SHA256');
verify.update(dataBytes);
verify.end();
return verify.verify({
key: publicKeyBuffer,
format: 'der',
type: 'spki'
}, signatureBytes);
}
// Example usage
const publicKey = 'MIIBITANBgkqhkiG9w0BAQEFAAOCAQ4AMIIBCQKCAQBkijC/ALAtltCudY48ONLIC7iwjaKFb7SCACzRLvU6QcPvtN0SpnbVhMDpvmQJzQFoVqDc6f4IugPo/aG5MzrBS40lArFRP29+kPpKqhYftKWXf/DusTd++l592PT/G6QUUc2Leu4N10YRLOTJyy+mY1iaQNBjx5h64Az7vpQx4fG2MPi4LB+ondRYL5B/dWniigyn5dKZ9mcffr5ji1gmyg+43I+HSJGtXjwjDJ2GDzXqu4XuSUmJunlpuGxMlYarlUrGWAqBLb0DkNc05KCtJHvRTtM2sa60u+rtcWXke2zjcs7/jxHoP8E2SsD5gueWnv4LKlXzl0dw9sC29emNAgMBAAE=';
const dataToValidate = "1719489115#www.myNotification.com/webhook#{'webhookId':'123'}";
const signature = "RozdyIRwpRO6HojlfCQ8yviO8KF2rnvFPQOdpip78wwfujfmYFlGQDnAg1OKaNC3OvdVnS4Jkmpklhn+gzYkVFTbn1x+D0QTQqwj5ph/9k3gMIK7GuQl9+0vZzTfLpkuHoVcL/qNlAnJUfq2zVVDtwppZpNAzszDSHGaT1ZKYe189+EMaGNcKC6ay1WU1yeJLOz6QqUhe6Yl8jTVDq1sqM7EHVvIcA53bH9b6vHRFEq7MMSNS6/mRzPyZHMe+adieDE6ld1EUxh0VbZXT1TVgn3hNAX0GWIladqba8DPnURVEcGoU2/WqDIiuSQ0vl90yGrXSc2wzfk7OXqtx3GngQ==";
const isValid = validateSignature(publicKey, dataToValidate, signature);
console.log('Signature valid:', isValid);
package com.signatures;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class SHA256SignatureVerification {
private static final String PUBLIC_KEY = "<PUBLIC KEY>";
public static void main(String[] args) {
long unixTimeInSeconds = 1679925945;
String notificationUrl = "www.notificationUrl.com";
String body = "<Atcual JSON Response Body>";
String signature = "<Signature from Header>";
SignatureVerificationService signatureVerificationService = new SignatureVerificationService();
signatureVerificationService.verify(signature, unixTimeInSeconds, notificationUrl, body);
}
public boolean verify(String signatureHeader, long unixTimeInSeconds, String notificationUrl, String body) {
try {
Signature sign = Signature.getInstance("SHA256WithRSA");
sign.initVerify(loadPublicKey(PUBLIC_KEY));
String plainText = unixTimeInSeconds + "#" + notificationUrl + "#" + body;
sign.update(plainText.getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = Base64.getDecoder().decode(signatureHeader);
var isVerified = sign.verify(signatureBytes);
System.out.println("Is signature verified ? " + isVerified);
return isVerified;
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
e.printStackTrace();
}
return false;
}
private static RSAPublicKey loadPublicKey(String publicKeyStr) {
RSAPublicKey publicKey = null;
try {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyStr));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
e.printStackTrace();
}
return publicKey;
}
}
{
"Data": {
"CustomerToken": "d37f859b-6c14-4c8b-ac83-a75fb1699609",
"Username": "LoadTest28171671",
"EmailAddress": "[email protected]",
"FirstName": "FN28171671",
"LastName": "LN28171671",
"CompanyName": "I-Payout",
"PhoneNumber": "9547851236",
"CellPhoneNumber": "9547851236",
"Address": "540 Nw 104th Street",
"Address2": "",
"State": "FL",
"City": "Fort Lauderdale",
"ZipCode": "33301",
"Country": "US",
"DateOfBirth": "1994-10-24T00:10:00",
"PreferredLanguage": "EN",
"IsSuspended": false,
"IsInfoVerified": false,
"IsClosed": false,
"CreatedDate": "2024-07-26T04:57:28.017",
"CustomerID": 503293
},
"EventName": "CUSTOMER.CREATED",
"Identifier": "c98024ef-5609-4cbf-a8c6-7b8ecb1e24e6",
"DatetimeUtc": "2024-07-26T08:57:28.0377582Z"
}
Key Components
Headers
- x-timestamp: This is the current Unix timestamp (number of seconds since epoch) when the webhook notification is sent. It ensures the request's freshness and helps prevent replay attacks.
- x-signature: This is the message's signature, created using the private RSA key and the SHA256 hash of the SignatureBody. It is Base64 encoded.
SignatureBody
The format of the SignatureBody is:
timestamp + "#" + NotificationUrl + "#" + JsonBody
- timestamp: The Unix timestamp.
- NotificationUrl: The URL to which the notification is sent.
- JsonBody: The JSON payload of the webhook notification.
Signature Generation
The SignatureBody is signed using the private RSA key and SHA256 hash algorithm to create the signature. The signature is then Base64 encoded and included in the x-signature header.
Signature Verification
The signature verification process should follow these steps:
- Grant network access and allowlist specific i-payout IP addresses.
- Check if the request has a Header Signature.
- Extract key information from the header:
- x-timestamp: timestamp
- x-signature: base64 signature
- Verify that there is a maximum of 60 minutes difference between the request timestamp and the current timestamp:
- current timestamp - timestamp < 60 Minutes Max
- Extract the request's body.
- Decode the base64 signature.
- Prepare the payload content to verify the signature:
- The timestamp (as a string).
- The character.
- The webhook host (your webhook endpoint) where to send the notification request.
- The request's body.
- Verify the signature using the correct i-payout Public Key.
Public Keys
i-payout uses the POST HTTP method and JSON payloads to send requests to your webhook URL.
Environment | Key |
---|---|
Sandbox environment | @"MIIBITANBgkqhkiG9w0BAQEFAAOCAQ4AMIIBCQKCAQBkijC/ALAtltCudY48ONLIC7iwjaKFb7SCACzRLvU6QcPvtN0SpnbVhMDpvmQJzQFoVqDc6f4IugPo/aG5MzrBS40lArFRP29+kPpKqhYftKWXf/DusTd++l592PT/G6QUUc2Leu4N10YRLOTJyy+mY1iaQNBjx5h64Az7vpQx4fG2MPi4LB+ondRYL5B/dWniigyn5dKZ9mcffr5ji1gmyg+43I+HSJGtXjwjDJ2GDzXqu4XuSUmJunlpuGxMlYarlUrGWAqBLb0DkNc05KCtJHvRTtM2sa60u+rtcWXke2zjcs7/jxHoP8E2SsD5gueWnv4LKlXzl0dw9sC29emNAgMBAAE="; |
Production environment |
IP Addresses
i-payout uses the following IP addresses:
Server | IP Addresses |
---|---|
Sandbox Environment | 216.99.221.232 216.99.221.231 |
Production Environment | 216.99.221.12 216.99.221.13 216.99.220.12 216.99.220.13 |
By following this security model, you can ensure that the webhook notifications you receive are authentic and tamper-proof. This provides a secure method for keeping your system updated with real-time events.
What's next?
After understanding the security features, read more about Webhook Events and see examples of how webhooks return data.
Updated 26 days ago