AgentCASH.js – Callback Signature

General

In order to prevent faked backend callbacks, each callback originating from the AgentCASH backend will be extended with two security fields: signature_order and signature.

Example callback:

{
  "amount": "30.01",
  "approval_code": "111222",
  "card_brand": "mastercard",
  "card_cardholder_name": "Bob Gordon",
  "card_fingerprint": "2d597977-880d-4db8-92f0-47799439af72",
  "card_masked_pan": "550000******0012",
  "created_at": "2016-09-14T14:01:02Z",
  "currency": "EUR",
  "external_id": "ID-654321"
  "payment_id": "c2efcaf2-e222-405c-b9d4-6f9932d07f76",
  "receipt_url": "http://test.host/i/vXMM9letRr2NJ_Ej4K2e6w",
  "status": "approved",
  "type": "purchase",

  "signature_order": "payment_id,external_id,type,status,receipt_url,amount,currency,approval_code,card_brand,card_masked_pan,card_cardholder_name,card_fingerprint,created_at,signature_order,secret",
  "signature": "5884f2d86237c507ddd62cfcbc2c032020f45c362f31eb00a99f83205bbfe06a65fb427cd8f00f38cfdf812ca2235b5dce76ec8ef92578e47d9b8d2996655f64"
}

The signature is a SHA-512 hash of all object values ordered and joined (without delimiters) as signature_order specifies. A secret value is also included and the hash is calculated for the final string.

The above example intentionally shows fields sorted in the alphabetical order to demonstrate that their order for signature calculation is in fact specified by the signature_order field.

Secret from the example is "MeetTheFlintstones".

Therefore, values ordered according to the signature_order field from the example are:

[
  "c2efcaf2-e222-405c-b9d4-6f9932d07f76",       # payment_id
  "ID-654321",                                  # external_id
  "purchase",                                   # type
  "approved",                                   # status
  "http://test.host/i/vXMM9letRr2NJ_Ej4K2e6w",  # receipt_url
  "30.01",                                      # amount
  "EUR",                                        # currency
  "111222",                                     # approval_code
  "mastercard",                                 # card_brand
  "550000******0012",                           # card_masked_pan
  "Bob Gordon",                                 # card_cardholder_name
  "2d597977-880d-4db8-92f0-47799439af72",       # card_fingerprint
  "2016-09-14T14:01:02Z",                       # created_at
  "<see-above>",                                # signature_order
  "MeetTheFlintstones"                          # secret
]

Array values are joined (without delimiters) into a string, hashed and the signature becomes:

"5884f2d86237c507ddd62cfcbc2c032020f45c362f31eb00a99f83205bbfe06a65fb427cd8f00f38cfdf812ca2235b5dce76ec8ef92578e47d9b8d2996655f64"

If the calculated signature matches the one from the request, it can be trusted. Otherwise it may not originate from a trusted source.

Secret

The secret is coupled with the API key used to identify the website and it can be retrieved from the AgentCASH admin UI (Webshop > API). Only merchant admins have access to this data.

The secret should not be shared publicly with anyone.

Signature validation example

The following pseudocode illustrates the signature validation algorithm:

secret = ENV["AGENTCASH_SECRET"]  # keep in an env-var or config

json = JSON.parse(request.body)   # parse callback data

if (json["signature"]) {
  # signature is present, need to validate it first
  signature_fields = json["signature_order"].split(",")

  # append field values in the correct order
  values = []
  for (field in signature_fields) {
    if (field == "secret") {
      values.add(secret)       # add the secret
    } else {
      values.add(json[field])  # add the field value
    }
  }

  # calculate SHA-512 hash from the values
  values_string = values.join("")
  calculated_signature = sha512_digest(values_string)

  if (json["signature"] == calculated_signature) {
    # ok - we can proceed
  } else {
    # error - invalid signature
  }
} else {
  # error - missing signature
}