Webhook signature verification

Webhooks are used to communicate events in the system as status changes or important notifications. The data contained in a Webhook call might sometimes hold sensitive data. To ensure that the Webhook originated from PayFirmly, all our Webhooks are digitally signed.

Below you find a detailed description of the process to verify if a Webhook call originated from PayFirmly:

Public key

To be able to verify Webhooks signature, you first need to download our public key:

Payground Payground public key
Live Live public key

Finding the signature

The signature can be found on the X-signature HTTP header, encoded to Base64.

Verifying the signature

To verify the signature, you need from the Webhook Request: the body, the X-signature header and the public key available above.

Code examples

Java

java
import java.nio.file.*; import java.security.*; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; public class Webhook { public static void main(String[] args) throws Exception { String body = "TheRequestBodyGoesHere"; // the body from the Webhook request String signature = "TheRequestXSignatureGoesHere"; // the X-Signature request header String publicKeyPath = "path/to/the/downloaded/key/webhookPublicKey.pem"; // the path to the key // pem to publicKey String publicKeyPEM = (new String(Files.readAllBytes(Paths.get(publicKeyPath)))) .replace("-----BEGIN PUBLIC KEY-----", "") .replace("-----END PUBLIC KEY-----", ""); byte[] encoded = Base64.getMimeDecoder().decode(publicKeyPEM); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(encoded)); // verify Signature signer = Signature.getInstance("SHA1withRSA"); signer.initVerify(publicKey); signer.update(body.getBytes()); if (signer.verify(java.util.Base64.getDecoder().decode(signature))) { System.out.println("Signature verified"); } else { System.out.println("ERROR: Signature not valid!"); } } }

NodeJS

javascript
const fs = require('fs'), path = require('path'), crypto = require('crypto'), verify = crypto.createVerify('SHA1'); let body = 'TheRequestBodyGoesHere'; // the body from the Webhook request let signature = 'TheRequestXSignatureGoesHere'; // the X-Signature request header let publicKeyPath = 'path/to/the/downloaded/key/webhookPublicKey.pem'; // the path to the key let publicKey = fs.readFileSync(publicKeyPath); // read verify.update(body); // sets the message to be verified console.log(verify.verify(publicKey, signature, 'base64')); // test it

PHP

php
$body = 'TheRequestBodyGoesHere'; // the body from the Webhook request $xSignature = 'TheRequestXSignatureGoesHere'; // the X-Signature request header $publicKeyPath = 'path/to/the/downloaded/key/webhookPublicKey.pem'; // the path to the key $base64DecodedXSignature = base64_decode($xSignature); // X-Signature is base64 encoded $publicKey = openssl_pkey_get_public(file_get_contents($publicKeyPath)); echo openssl_verify($body, $base64DecodedXSignature, $publicKey); // test it

Python 3

python
import base64 from OpenSSL import crypto public_key_path = 'path/to/the/downloaded/key/webhookPublicKey.pem' # the path to the key def verify(message, signature, public_key_path): try: key = crypto.load_certificate(crypto.FILETYPE_PEM, open(key_path, 'rb').read()) crypto.verify(key, base64.urlsafe_b64decode(xsignature), message, 'sha1') # when successful, this call returns None return True except: # when unsuccessful, python raises an Error return False body = 'TheRequestBodyGoesHere' # the body from the Webhook request xsignature = 'TheRequestXSignatureGoesHere' # the X-Signature from the Webhook request print(verify(body, xsignature, public_key_path)) # test it

Ruby

ruby
require 'openssl' require 'base64' public_key_path = 'path/to/the/downloaded/key/webhookPublicKey.pem' # the path to the key body = 'TheRequestBodyGoesHere' # the body from the Webhook request signature = 'TheRequestXSignatureGoesHere' # the X-Signature request header key = OpenSSL::X509::Certificate.new File.read public_key_path # creates the certificate object from path print(key.public_key.verify(OpenSSL::Digest::SHA1.new, Base64.decode64(signature), body)) # test it

C#

This example uses the .pem file. If you first convert the .pem file to XML both the PemToXML and GetXmlRsaKey functions will not be necessary.

c#
using System; using System.IO; using System.Security.Cryptography; using System.Text; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.OpenSsl; using Org.BouncyCastle.Security; namespace ConsoleApp { class Program { static void Main(string[] args) { var Body = @"TheRequestBodyGoesHere"; // the body from the Webhook request var Signature = "TheRequestXSignatureGoesHere"; // the X-Signature request header var PublicKeyPath = @"path/to/the/downloaded/key/webhookPublicKey.pem"; // the path to the key byte[] body = Encoding.UTF8.GetBytes(Body); byte[] signature = Convert.FromBase64String(Signature); // X-Signature is base64 encoded RSACryptoServiceProvider RSAVerifier = new RSACryptoServiceProvider(); RSAVerifier.FromXmlString(PemToXML(PublicKeyPath)); // Pem to XML if (RSAVerifier.VerifyData(body, "SHA1", signature)) { Console.WriteLine("Signature verified"); } else { Console.WriteLine("ERROR: Signature not valid!"); } Console.ReadLine(); } private static string PemToXML(string PublicKeyPath) { StreamReader PubKeyReader = File.OpenText(PublicKeyPath); string pem = PubKeyReader.ReadToEnd(); return GetXmlRsaKey(pem, obj => { var publicKey = (RsaKeyParameters)obj; return DotNetUtilities.ToRSA(publicKey); }, rsa => rsa.ToXmlString(false)); } private static string GetXmlRsaKey(string pem, Func<object, RSA> getRsa, Func<RSA, string> getKey) { using (var ms = new MemoryStream()) using (var sw = new StreamWriter(ms)) using (var sr = new StreamReader(ms)) { sw.Write(pem); sw.Flush(); ms.Position = 0; PemReader pr = new PemReader(sr); object keyPair = pr.ReadObject(); using (RSA rsa = getRsa(keyPair)) { var xml = getKey(rsa); return xml; } } } } }