Events API Signature Example

Overview

This page will walk you through an example of how the Signature is computed in the HTTP request.

HTTP Request Example

Here is an HTTP request example for an event (ACHC), that uses the form content type (application/x-www-form-urlencoded):

POST /Transaction
Host: some.client.domain.com
Encryption-Type: HMAC-SHA256
Content-Length: 178
User-Agent: python-requests/2.9.1
Connection: keep-alive
Signature: DkY7o3ynLLvNvnDHraFicMP+gK/UOAL09WsNj2mQ1ww=
Accept: */*
Date: 20170504:141752UTC
Content-Type: application/x-www-form-urlencoded
User-Id: galileo
Accept-Encoding: gzip,deflate

type=ach_credit_fail&account_id=2011&amount=45&prn=155200002022&prod_id=1701&prog_id=305&return_code=R01&source=Chase+Bank&source_id=6426460&timestamp=2019-10-09+11%3A20%3A33+MST

The next section will explain how the Signature was computed, so that you can compute it and make sure it is correct (to authenticate the request).

Signature Computation Steps

  1. First, collect the following header fields into a hash map (remember to use an upper case D on User-ID):

    data = {
         "Content-Length": "178",
         "Encryption-Type": "HMAC-SHA256",
         "User-ID": "galileo",
         "Content-Type": "application/x-www-form-urlencoded",
         "Date": "20170504:141752UTC"
    }
    
  2. Add the form parameters, and sort by key, resulting in the following:

    data = {
         "Content-Length": "178",
         "Content-Type": "application/x-www-form-urlencoded",
         "Date": "20170504:141752UTC",
         "Encryption-Type": "HMAC-SHA256",
         "User-ID": "galileo",
         "account_id": "2011",
         "amount": "45",
         "prn": "155200002022",
         "prod_id": "1701",
         "prog_id": "305",
         "return_code": "R01",
         "source": "Chase Bank",
         "source_id": "6426460",
         "timestamp": "2019-10-09 11:20:33 MST",
         "type": "ach_credit_fail"
    }
    

    If you are using a dictionary/hash map in your code, be aware that some languages do not sort the keys automatically, nor honor insertion order.

  3. For each value, compute the Base64 encoding of each of the values in data (using the UTF-8 encoding of the values):

    data = {
         "Content-Length": "MTc4",
         "Content-Type": "YXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVk",
         "Date": "MjAxNzA1MDQ6MTQxNzUyVVRD",
         "Encryption-Type": "SE1BQy1TSEEyNTY=",
         "User-ID": "Z2FsaWxlbw==",
         "account_id": "MjAxMQ==",
         "amount": "NDU=",
         "prn": "MTU1MjAwMDAyMDIy",
         "prod_id": "MTcwMQ==",
         "prog_id": "MzA1",
         "return_code": "UjAx",
         "source": "Q2hhc2UgQmFuaw==",
         "source_id": "NjQyNjQ2MA==",
         "timestamp": "MjAxOS0xMC0wOSAxMToyMDozMyBNU1Q=",
         "type": "YWNoX2NyZWRpdF9mYWls"
    }
    
  4. Now, build the following string, concatenating key-value pairs with | and leaving no space between successive key-value pairs:

    Content-Length|MTc4Content-Type|YXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkDate|MjAxNzA1MDQ6MTQxNzUyVVRDEncryption-Type|SE1BQy1TSEEyNTY=User-ID|Z2FsaWxlbw==account_id|MjAxMQ==amount|NDU=prn|MTU1MjAwMDAyMDIyprod_id|MTcwMQ==prog_id|MzA1return_code|UjAxsource|Q2hhc2UgQmFuaw==source_id|NjQyNjQ2MA==timestamp|MjAxOS0xMC0wOSAxMToyMDozMyBNU1Q=type|YWNoX2NyZWRpdF9mYWls
    
  5. Finally, using the shared secret key you have set up with Galileo, and the hashing algorithm in the Encryption-Type header field, compute the signature of the string above, and Base64 encode the result. Assuming the shared secret key is mysecret (encoded in UTF-8) and the encryption type is HMAC-SHA256, you should get:

    DkY7o3ynLLvNvnDHraFicMP+gK/UOAL09WsNj2mQ1ww=
    

Python 3 Code Snippet

The code below demonstrates how to compute the signature using Python 3:

import base64
import hmac
from typing import Dict
import urllib.parse

HEADERS_TO_SIGN = (
    "Content-Length",
    "Content-Type",
    "Encryption-Type",
    "User-ID",
    "Date",
)


def normalize_header_key(header_key: str) -> str:
    """Normalize header key/name

    @param header_key: Header key, e.g. "Content-length".
    @return Normalized header key
    """
    if header_key.lower() == "user-id":
        return "User-ID"
    else:
        return header_key.title()


def compute_signature(headers: Dict[str, str], payload: Dict[str, str], secret: str) -> str:
    """Compute the Galileo HMAC-SHA256 signature of a request, using key.

    :param headers: HTTP request headers
    :param payload: HTTP request payload, parsed into a dictionary/hash map. These
           are the event parameters (url decoded if the request was received as
           application/x-www-form-urlencoded).
    :param secret: The secret key for computing the signature
    :return: HMAC, as a base64 string
    """
    # Normalize all header keys:
    normalized_headers = {normalize_header_key(key): value
                          for key, value in headers.items()}

    # Add headers to sign:
    data_to_sign = {}
    for header_key in HEADERS_TO_SIGN:
        data_to_sign[header_key] = normalized_headers[header_key]

    # Add payload parameters to sign:
    for key, value in payload.items():
        data_to_sign[key] = value

    # Compute the string to sign, in sorted key order and using the base64
    # encoding of the values:
    string_to_sign = ""
    for key in sorted(data_to_sign.keys()):
        # Note: In Python 3, base64 returns an ASCII-encoded byte array, not a string.
        # Also, the default encoding for Python 3 encode/decode is utf-8 (and ascii is
        # equivalent to utf-8 for the base64 character range); so, encoding= could be
        # omitted throughout, but is added for clarity.
        val_b64_bytes = base64.b64encode(data_to_sign[key].encode(encoding="utf-8"))
        kv_to_sign = key + "|" + val_b64_bytes.decode(encoding="ascii")
        string_to_sign += kv_to_sign

    # Compute the base64-encoded signature:
    secret_bytes = secret.encode(encoding="utf-8")
    string_to_sign_bytes = string_to_sign.encode(encoding="utf-8")
    signature_bytes = hmac.new(secret_bytes, msg=string_to_sign_bytes, digestmod="SHA256").digest()
    signature_b64_bytes = base64.b64encode(signature_bytes)
    return signature_b64_bytes.decode(encoding="ascii")

# ============================================================================


EXAMPLE_HEADERS = {
    "Host": "some.client.domain.com",
    "Encryption-Type": "HMAC-SHA256",
    "Content-Length": "178",
    "User-Agent": "python-requests/2.9.1",
    "Connection": "keep-alive",
    "Signature": "DkY7o3ynLLvNvnDHraFicMP+gK/UOAL09WsNj2mQ1ww=",
    "Accept": "*/*",
    "Date": "20170504:141752UTC",
    "Content-Type": "application/x-www-form-urlencoded",
    "User-Id": "galileo",
    "Accept-Encoding": "gzip,deflate",
}
EXAMPLE_PAYLOAD = "type=ach_credit_fail&account_id=2011&amount=45&prn=155200002022&prod_id=1701&prog_id=305&return_code=R01&source=Chase+Bank&source_id=6426460&timestamp=2019-10-09+11%3A20%3A33+MST"
EXAMPLE_SECRET = "mysecret"

# Convert payload to a Python 3 dictionary, normalized to str -> str:
parsed_payload = {key: value[0] for key, value in urllib.parse.parse_qs(EXAMPLE_PAYLOAD).items()}
signature = compute_signature(EXAMPLE_HEADERS,
                              parsed_payload,
                              EXAMPLE_SECRET)

assert signature == EXAMPLE_HEADERS["Signature"]