Notification Signatures

 

The following document provides general instructions and specific examples for implementing your own notification signature verification function. WePay signs every notification it sends to your platform’s endpoints. We include a signature in the HTTP request header so your platform can validate that the notification is intact and sent from WePay.


Keys

Please copy and paste the Keys below when it’s time for your platform to integrate.

Keys for external stage

WePay provides your platform with two keys for testing in external stage. When testing Notifications be sure to use requestbin.com, as it provides a publicly available HTTPS URL. When testing WePay Signatures, be sure to implement the signature verification function below during your integration.

Primary Public Key:

Copy
Copied
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1kZVzSNG8Jski0L2wO3I3R0p6UaPBgtuRwRtHBLOxCc+EZHkvNXCRakGpS5cI79xCJwOh4byDs2
kBroVPuiiJZkyatkGGTlJzAUg9ReIiXdUzRt5TZVJ9J7Mnbd6uHd0pR9uZDaEMEZhoO12nD3HyhzLUwAmhjNo3fjqpcDmiU/fpxipVNSOACEy73LmxgO1Vv
vlQ193cwW3Upua30S+TGxDgFs78K7g4r1tbro2Fu+/kX/dCwImdaQaENxECSydFySXN/Hl1ztdvpDUhlPOL1bnPvj2BCeUU4P9X1XQkDNr/p0J5tVPIBRzM
3E4s7lF4xtQsVRa+JsGSukDBaL54QIDAQAB
-----END PUBLIC KEY-----

Backup Public Key:

Copy
Copied
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6s0P9ViHflLV8eqX1vofow0jmVJK6CGls/9GfpRZ81wqefxzKWykYoOI1H589Qk8oj8T+BorEC8y
0CLV6rGwH9a0ot1r8h40NTIp0A8VfpB5QM6viJ/PM+XNzuuxsUC7F++5Btlvy7VsrR2NQFPdC/dPnvxQzoLputeKPQoR7nr2ksMRFaKwURJBhzpJ/cDWQUfi
i4nkOan1VgKoCJRw+0ro1qej+AojzqLWynXL6CzvnzyQDC3vPX+HX6gxxFgyUle8MIxBVAv1TM0cNvpeftWDfvctsWjhrfkj/zxN3Y4eq9QA4ZQlnqJ2G1mS
/2z6erJgbCS4prPsjvg57/jG5QIDAQAB
-----END PUBLIC KEY-----

Keys for production

WePay provides one primary key for testing in production.

Copy
Copied
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAojYsBf7vFRcMESXL7GSaHzfZx2DAFsa3UdtU797GpOni3obBkc5xRcMZJ9XkBVyQdzYuBYgStphOK
UyROSKJZyGpMvLXG4aFIidJvzJvt+OUL2ru7j2EgJhYOONnAyZ65rBfyH1fhX5Pbs8clnoD9+DoCeGPxSjEoHScdkkKg7yCRU8ZNNcIPS0frGXY4DUn9BFXIN
lIYzc7Zf1SsxTX2JmB8aYi7lFEuO03j+WFyWB044DawOI6CnmsBqd0uR1KO8FsLcm+u6LzXAYfwqBNOPzjjD61XA0iOl1wd8k/Aqz6Jmn+H7YtNwX+x+tPSQY
6YqPDky6OHiQe202+jSxo7QIDAQAB
-----END PUBLIC KEY-----

WePay Signature

This is an example of a wepay-signature, which is located in the HTTPS request header of a notification. This value is utilized throughout the General Instructions, but you will need a different value for your platform’s integration.
Copy
Copied
W3sicHJvdGVjdGVkIjoiZXlKaGJHY2lPaUpTVXpJMU5pSjkiLCJzaWduYXR1cmUiOiJHTVd0Z2s2YXk0Q20tU0xpTmhOTklmd0N4LVBXWURZSkZwZzgxcTVm
Tkp1Qml1RVhDNmszSm5uUWhHOVNGN0I4MWRCeGtuX0k5UHZFa01EXzhXb09yVmtTc0dUclRaNE1YbEZJSURMLTE5RHhUQ3lITzdxdDU0UXpoZ20tNzdaY1FU
U3dPam54M3BfYXBNWFhaMy03QzJzTUxQMW0wOU5rV3h4YWlQcHB2Wi1FdU5DU1pVb0dMRkQ4dGNudlA0M0x2b2IzOFk2V3RVTE5lSmx5Y2JnVF9Sa0RIeTZK
Ri1jallmM3hxN2ltYkZTdzFUY2tEa2VFZnNXMDNLNUx6WVZDaTk2aGkyTWlGTUtUSzNXUzVnT2xTbHEwWUw2b0tpVkJ6QUJWZFhPOWtKZW9yYWpGQzJ0S2dG
Yzl6VmlSSTh3RXI1U0N4cFJQTFFjcUFNeHhWM1IwSFEifSx7InByb3RlY3RlZCI6ImV5SmhiR2NpT2lKU1V6STFOaUo5Iiwic2lnbmF0dXJlIjoiQVA4eHVT
Zmh2aEJIS3BhbU9PRnpUMXBuelVHYlRZMERQM19yN2I0eTRfWlRWU21CSUdZQlFhYXBXeml3Q3RWT0RDbndHak44RVhfQTgxSkVoeVBEcFpyY0wyZUk0WnZ1
T29mb3BYSGFlLVMyb0VqYnVxSmx5clJMWlNwYW00Vk93REx3S2tRVHd6OEFrQ3YwN211aERVbUhlWWtNOG9aRUFtVjlrQVFpeE5DZ1VxRVptNGFvMndWbzg5
aHhHdmQ2cEY5b1l1NzFQeWtCOHc4VEZqZnVYX1lTdkxyblRuTGd1RTZvb0dzYUtTR1RjQm5JNmlvU3VoZ04tVDhzZHJtWmpreWIyM0VjUWxrTkI2MHdBSzRW
alhtNDE4cXdqY1RTaXVnelRwbGhYa2tKTTZVMlg3M2JFT3E5SERFNjFleFltV0o0NmdSbnBIX2dvVjhKQTdqeFhRIn1d

Request Body

After subscribing to a notification topic using notification_preferences, your platform will start to receive notifications. The notification request body will be sent to your platform’s URL. The request body will look like this:

Copy
Copied
{
    "event_time": 1511307578,
    "id": "aceecf2c-e432-4aac-90a3-8f3b7fbb6420",
    "resource": "notifications",
    "path": "/notifications/aceecf2c-e432-4aac-90a3-8f3b7fbb6420",
    "owner": {
        "id": "171845",
        "resource": "applications",
        "path": null
    },
    "topic": "payments.completed",
    "payload": {
        "id": "c55324d3-9d90-4c66-a1e6-c3bc13ce0f09",
        "resource": "payments",
        "path": "/payments/c55324d3-9d90-4c66-a1e6-c3bc13ce0f09",
        "owner": {
            "id": "40200612-e849-4172-8308-657cf3a0de27",
            "resource": "accounts",
            "path": "/accounts/40200612-e849-4172-8308-657cf3a0de27"
        },
        "create_time": 1511307571,
        "status": "completed",
        "amount": 4200,
        "currency": "",
        "payment_method": {
            "id": "ba7b10a4-7674-4f36-9b3a-05bd6ea890a5",
            "resource": "payment_methods",
            "path": "/payment_methods/ba7b10a4-7674-4f36-9b3a-05bd6ea890a5"
        },
        "fee_amount": 0,
        "auto_capture": true,
        "pending_reasons": null,
        "failure_reason": null,
        "txnr_app_fee": null,
        "txnr_merchant": null,
        "signature_uri": null,
        "amount_refundable": 4200,
        "custom_data": null
    }
}

General Instructions

The instructions below provide a general overview of the integration process. Check out the Java Example and Python Example for detailed instructions.

Note: WePay may send your platform multiple signatures. A notification is valid as long as at least one signature passes verification.

  1. Obtain and insert the WePay public key.
  2. Read wepay-signature in the HTTP request header and then Base64 decode it. Your work will look like this:
Copy
Copied
[  
   {  
      "protected":"eyJhbGciOiJSUzI1NiJ9",
      "signature":"GMWtgk6ay4Cm-SLiNhNNIfwCx-PWYDYJFpg81q5fNJuBiuEXC6k3JnnQ
      hG9SF7B81dBxkn_I9PvEkMD_8WoOrVkSsGTrTZ4MXlFIIDL-19DxTCyHO7qt54Qzhgm-7
      7ZcQTSwOjnx3p_apMXXZ3-7C2sMLP1m09NkWxxaiPppvZ-EuNCSZUoGLFD8tcnvP43Lvo
      b38Y6WtULNeJlycbgT_RkDHy6JF-cjYf3xq7imbFSw1TckDkeEfsW03K5LzYVCi96hi2M
      iFMKTK3WS5gOlSlq0YL6oKiVBzABVdXO9kJeorajFC2tKgFc9zViRI8wEr5SCxpRPLQc
      qAMxxV3R0HQ"
   },
   {  
      "protected":"eyJhbGciOiJSUzI1NiJ9",
      "signature":"AP8xuSfhvhBHKpamOOFzT1pnzUGbTY0DP3_r7b4y4_ZTVSmBIGYBQaap
      WziwCtVODCnwGjN8EX_A81JEhyPDpZrcL2eI4ZvuOofopXHae-S2oEjbuqJlyrRLZSpam
      4VOwDLwKkQTwz8AkCv07muhDUmHeYkM8oZEAmV9kAQixNCgUqEZm4ao2wVo89hxGvd6pF
      9oYu71PykB8w8TFjfuX_YSvLrnTnLguE6ooGsaKSGTcBnI6ioSuhgN-T8sdrmZjkyb23E
      cQlkNB60wAK4VjXm418qwjcTSiugzTplhXkkJM6U2X73bEOq9HDE61exYmWJ46gRnpH_g
      oV8JA7jxXQ"
   }
]
  1. Read the request body as a string (without any changes or processing of the string) and Base64 encode it.
  2. Parse wepay-signature as a JSON array and read the elements in it. (It should include at least one JSON object.)
  3. Base64 decode the signature value in step 4.
  4. Concatenate protected (from step 4) + "." + request body (from step 3) to get the payload.
  5. Call the RSA verify function in your server language. The function should accept the WePay public key, the payload (from step 6), and the signature (from step 4) as inputs. If verification fails, please ignore the notification.
  6. Compare the app ID in the response and your platform's app ID. If the app ID does not match your app ID, please ignore the notification.

Note: Please choose a URL safe encoding and decoding implementation in your server languages. In addition, add or remove padding (if needed) when Base64 decoding and Base64 encoding.


Java Example

Utilize the example and comments below to implement your notification signature verification function during integration. In addition, refer to the WePay Signature, Request Body, and Keys sections for values you can plug in.

Copy
Copied
import org.json.JSONArray;
import org.json.JSONObject;

import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

public class WePayVerifierSample {

   public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, 
   UnsupportedEncodingException, java.security.SignatureException {

       // Obtain and insert WePay's public key.
       String publicKeyContent = "";

       // Read the request body as a string and insert it below. 
       String requestBody = "";

       // Obtain the "wepay-signature" string from the request header, and insert it below.
       String signatureStr = "";

       // Insert your platform's app ID.
       String appId = "";

       // Check the signature.
       boolean verified = verifyWePaySignature(requestBody, signatureStr, publicKeyContent);

       // Check your platform's app ID.
       boolean checkAppId = ((JSONObject) (new JSONObject(requestBody)).get("owner")).getString("id").equals(appId);

       if (verified && checkAppId) {
           // Process the notification.

       } else {
           // Discard the notification.

       }
   }

   public static boolean verifyWePaySignature(String requestBody, String signatureHeader, String publicKeyContent) 
   throws NoSuchAlgorithmException, 
   InvalidKeySpecException, UnsupportedEncodingException, InvalidKeyException, SignatureException {

       // Prepare WePay's public key.
       publicKeyContent = publicKeyContent.replaceAll("\\n", "").replace("-----BEGIN PUBLIC KEY-----", "").
       replace("-----END PUBLIC KEY-----", "");
       KeyFactory kf = KeyFactory.getInstance("RSA");
       X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyContent));
       RSAPublicKey pubKey = (RSAPublicKey) kf.generatePublic(keySpecX509);

       // Read "wepay-signature" in the HTTP request header and Base64 decode it.
       String decodedWepaySignature = new String(Base64.getUrlDecoder().decode(signatureHeader.getBytes("UTF8")));

       // Read the request body as a string and Base64 encode it.
       byte[] requestBodyByte = Base64.getUrlEncoder().withoutPadding().encode(requestBody.getBytes("UTF8"));

       // Parse "wepay-signature" as a JSON array and read the elements in it.
       JSONArray sigArray = new JSONArray(decodedWepaySignature);

       for (int i = 0; i < sigArray.length(); i++) {

           // Read the protected header in the signature JSON array.
           String protectedHeader = ((JSONObject) sigArray.get(i)).getString("protected");
          
           // Read the signature value.
           String signature = ((JSONObject) sigArray.get(i)).getString("signature");

           // Base64 decode the signature value in the step above.
           byte[] sigToVerify = Base64.getUrlDecoder().decode(signature.getBytes("UTF8"));

           // Concat protected + "." + encoded request body to get the payload.
           byte[] payload = (protectedHeader + "." + new String(requestBodyByte)).getBytes();

           // Verify with RSA-SHA256 function.
           Signature sig = Signature.getInstance("SHA256WithRSA");
           sig.initVerify(pubKey);
           sig.update(payload);
           boolean verified = sig.verify(sigToVerify);
           if (verified) {
               return true;
           }
       }
       return false;
   }
}

Python Example

Utilize the example and comments below to implement your notification signature verification function during integration. In addition, refer to the WePay Signature, Request Body, and Keys sections for values you can plug in.

Copy
Copied
# dependency: pycryptodome(https://github.com/Legrandin/pycryptodome)
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
import json
from base64 import urlsafe_b64encode, urlsafe_b64decode


class Verifier(object):
   @staticmethod
   def base64url_decode(payload):
       l = len(payload) % 4
       if l == 2:
           payload += '=='
       elif l == 3:
           payload += '='
       elif l != 0:
           raise ValueError('Invalid base64 string')
       return urlsafe_b64decode(payload.encode('utf-8'))

   @staticmethod
   def base64_decode(data: str) -> str:
       return str(Verifier.base64url_decode(data), 'utf-8', 'ignore')

   @staticmethod
   def base64_encode(payload: str) -> str:
       if not isinstance(payload, bytes):
           payload = payload.encode('utf-8')
       encode = urlsafe_b64encode(payload)
       return encode.decode('utf-8').rstrip('=')

   def __init__(self, public_key, app_id):
       # Obtain and insert the WePay public key.
       self.public_key = RSA.import_key(public_key.encode('utf-8'))
       self.app_id = app_id

   def __verify_single_signature(self, payload, sig):
       data = SHA256.new(payload.encode('utf-8'))
       try:
           pkcs1_15.new(self.public_key).verify(data, sig)
           return True
       except Exception as e:
           print(e)
           return False

   def verify_signatures(self, request_body, wepay_signature):

       # Read "wepay-signature" in the HTTP request header and Base64 decode it.
       decoded_wepay_signature = Verifier.base64_decode(wepay_signature)

       # Read the request body as a string and Base64 encode it.
       base64_request_body = Verifier.base64_encode(request_body)

       # Parse "wepay-signature" as a JSON array and read the elements in it.
       sig_json_array = json.loads(decoded_wepay_signature)
       for sig in sig_json_array:
           protected = sig['protected']
           signature = sig['signature']

           # Base64 decode the signature value.
           decoded_signature = Verifier.base64url_decode(signature)

           # Concat protected + "." + request body to get the payload.
           payload = protected + '.' + base64_request_body

           # Call RSA to verify the function.
           verified = self.__verify_single_signature(payload, decoded_signature)
           if verified:
               return True
       return False


if __name__ == '__main__':
   public_key = ""  # Replace with WePay's public key.
   app_id = ""  # Replace with your platform's app ID.
   verifier = Verifier(public_key, app_id)

   request_body = ''  # Read the request body as a string.
   wepay_signature = ""  # Read "wepay-signature" in the HTTP request header.

   verified = verifier.verify_signatures(request_body, wepay_signature)

   # Check your platform's app ID.
   check_app_id = json.loads(request_body)['owner']['id'] == verifier.app_id

   if check_app_id and verified:
       # Process the notification.
       pass
   else:
       # Ignore the notification.
       pass

C Sharp Example

Utilize the example and comments below to implement your notification signature verification function during integration. In addition, refer to the WePay Signature, Request Body, and Keys sections for values you can plug in.

Copy
Copied
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Org.BouncyCastle.Security;

namespace WePayVerifierExample
{
    public class WePayVerifierExample
    {
        static void Main(string[] args)
        {
            // Obtain and insert WePay's public key.
            string publicKeyContent = "{WePay's public key}";

            // Read the request body as a string and insert it below.
            string requestBody = "{stringified-body}";

            // Obtain the "wepay-signature" string from the request header, and insert it below.
            string signatureStr = "{wepay-signature}";

            // Insert your platform's app ID.
            string appId = "{app-id}";

            // Check the signature.
            bool verified = VerifyWePaySignature(requestBody, signatureStr, publicKeyContent);

            // Check your platform's app ID.
            var requestAppId = JObject.Parse(requestBody)["owner"]["id"].ToString();
            bool checkAppId = appId.Equals(requestAppId);

            if (verified && checkAppId)
            {
                // Process the notification.
            }
            else
            {
                // Discard the notification.
            }
        }

        public static bool VerifyWePaySignature(string requestBody, string signatureHeader, string publicKeyContent)
        {
            // Read "wepay-signature" in the HTTP request header and Base64 decode it.
            var signatures = JsonConvert.DeserializeObject<IList<NotificationSignature>>(Base64UrlEncoder.Decode(signatureHeader));

            // Read the request body as a string and Base64 encode it.
            string requestBodyEncoded = Base64UrlEncoder.Encode(requestBody);

            foreach (var signatureObj in signatures)
            {
                // Base64 decode the signature value in the signature JSON object.
                var signatureBytes = Base64UrlEncoder.DecodeBytes(signatureObj.Signature);

                // Concat protected header + "." + encoded request body to get the payload.
                var payloadBytes = Encoding.UTF8.GetBytes($"{signatureObj.Protected}.{requestBodyEncoded}");

                // Verify with RSA-SHA256 function.
                var rsaKeyParameters = PublicKeyFactory.CreateKey(Convert.FromBase64String(publicKeyContent));
                var signer = SignerUtilities.GetSigner("SHA-256withRSA");
                signer.Init(false, rsaKeyParameters);
                signer.BlockUpdate(payloadBytes, 0, payloadBytes.Length);

                if (signer.VerifySignature(signatureBytes))
                {
                    return true;
                }
            }
            return false;
        }

        class NotificationSignature
        {
            public string Protected { get; set; }
            public string Signature { get; set; }
        }
    }
}

Debugging

Here, we provide guidance for debugging potential issues during testing.

JSON body modifications

If signature verification is failing, be sure to verify that your internal tools are properly handling the JSON request body. For instance, if you are using a logging tool like Kibana to log requests, they may add extra padding or spaces to the request body.

To resolve, use requestbin.com for successful stage testing, and ensure that your production server, when accepting requests from WePay, doesn’t add spaces or tamper with the request body.