Alert Signing (RSA)

This section describes the algorithm for signing and verifying alerts (callback messages) using RSA keys.

By default, the asymmetric RSA-SHA256 algorithm is used to sign alerts. The symmetric HMAC-SHA512 algorithm is available as an alternative method (see sectionAlert signature (HMAC)).

Purpose of signature

Signing alerts solves the following problems:

  • confirms that the message was sent by the HighHelp platform;

  • ensures that the data in the alert body has not been changed along the way.

The public RSA key is displayed in your personal account only with the current RSA-SHA256 algorithm. The current signature algorithm is displayed at the bottom of the windowAPICallback settings(lineCurrent algorithm: RSAorCurrent algorithm: HMAC). If the HMAC algorithm is configured for the cash register, use the sectionAlert signature (HMAC).

Obtaining a public RSA key

To verify the signature of alerts, use the public RSA key for signing alerts configured for the cash register.

How to obtain a key:

  1. Open your merchant's personal account.

  2. Go to sectionAPICallback settings.

  3. Make sure that the current algorithm is indicated at the bottom of the window:Current algorithm: RSA.

  4. Click on the download icon in the blockPublic Keyand save the key file.

  5. Configure the alert processing service to use this key in the signature verification algorithm.

With the current RSA algorithm, the window also displays the blockHMAC keyto generate an HMAC key. Generating an HMAC key does not switch the signature algorithm; To change the algorithm, contact the HighHelp manager.

Alert body normalization

Normalization is performed by recursively traversing the JSON structure and generating a list of strings:followed by sorting.

Algorithm:

  1. Traverse the JSON structure recursively.

  2. For each value, form a path in the form1: 2:…​:.

  3. For arrays, use element indexes::0, :1, …​

  4. For boolean values ​​use:true1, false0.

  5. Sort all lines alphabetically.

  6. Connect the lines via;.

Example of source data:

{
  "amount": 100,
  "status": "success",
  "is_paid": true,
  "data": {
    "id": 123,
    "is_active": false
  }
}

Normalization result:

amount:100;data:id:123;data:is_active:0;is_paid:1;status:success

Example implementation of normalization (Python3)

def parse_json(prefix, obj, result): """ JSON- : . """ if isinstance(obj, dict): for key, value in obj.items(): if isinstance(key, bool): key = int(key) new_prefix = f"{prefix}:{key}" if prefix else str(key) parse_json(new_prefix, value, result) elif isinstance(obj, list): for index, item in enumerate(obj): if isinstance(item, bool): item = int(item) new_prefix = f"{prefix}:{index}" parse_json(new_prefix, item, result) else: if isinstance(obj, bool): obj = int(obj) if obj is None: obj = "" result.append(f"{prefix}:{obj}") def normalize_message(payload: dict) -> str: """ JSON ( : : ;). """ items: list[str] = [] parse_json("", payload, items) items.sort() return ";".join(items)

Normalization requirements

When implementing the normalization algorithm, consider the following requirements:

  • Boolean values:are converted to an integer representation (true1, false0).

  • Null values:are converted to an empty string"".

  • Numbers:use standard string representation without localization (thousand separators, local formats). Do not add leading zeros.

  • Arrays:the order of the elements is maintained in the original sequence. Element indices are added to the path as:0, :1, :2, …​

  • Objects:after all pairs have been formed:Sorts alphabetically by complete string.

  • Character encoding:use UTF-8 for encoding before using Base64Url. Do not change the case of characters.

  • Base64Url:encode the string in Base64Url format (RFC 4648): replace+on-, /on_; alignment characters=do not delete.

  • Spaces and formatting:do not add or remove spaces in values. Use the exact values ​​from the JSON structure.

Signature generation algorithm

The signature is generated on the HighHelp side using the following algorithm:

  1. Normalization of the JSON body of an alert with a functionnormalize_message.

  2. Encoding a normalized string into Base64Url.

  3. Concatenation of the resulting string andtimestamp(line).

  4. Calculate the SHA-256 hash from the resulting string.

  5. Signing a hash with a private RSA key using the RSA-SHA256 scheme.

  6. Encoding the signature in Base64Url.

  7. Passing signature and timestamp in HTTP notification headers.

In your integration, repeat steps 1-​6 and verify the signature using the cash register's public key.

Signature verification on the merchant side

To verify the signature, the alert handler performs the following checks:

  • the notification body is present and contains valid JSON;

  • headers are presentx-access-token, x-access-timestamp, x-access-signature;

  • meaningx-access-tokencontains the public RSA key of the cash register in Base64Url;

  • the signature is correct.

Example response codes:

  • 409— empty notification body, invalid JSON, missing required headers, incorrect formatx-access-token(base64url decode / key parsing error), incorrect formatx-access-signature(base64url decode error);

  • 403— signature mismatch;

  • 200— the signature is correct.

To verify the signature, follow these steps:

  1. Get the JSON body of the alert and header values:

    • x-access-token— public RSA key of the cash register in Base64Url;

    • x-access-timestamp— time stamp;

    • x-access-signature- signature;

    • x-access-merchant-id— cash register identifier for selecting the correct public key.

  2. Normalize the alert body to a stringnormalizedaccording to the described algorithm.

  3. EncodenormalizedBase64Url.

  4. Construct the stringmessage = encoded + timestamp.

  5. Compute SHA-256 frommessage.

  6. Decode signature from Base64Url.

  7. Verify the signature using the public RSA key.

Signature verification example (Python3)

import base64 import binascii from Crypto.Hash import SHA256 from Crypto.PublicKey import RSA from Crypto.Signature.pkcs1_15 import PKCS115_SigScheme def base64url_decode(data: str) -> bytes: normalized = str(data).strip().replace("-", "+").replace("_", "/") if not normalized: raise ValueError("Invalid base64url string") padding = "=" * (-len(normalized) % 4) try: return base64.b64decode(f"{normalized}{padding}", validate=True) except binascii.Error as exc: raise ValueError("Invalid base64url string") from exc def verify_rsa_callback_signature( payload: dict, signature_b64url: str, public_key_pem: bytes, timestamp: int, ) -> bool: """ RSA-SHA256. """ try: # normalized = normalize_message(payload) normalized_bytes = normalized.encode("utf-8") encoded_base64url = base64.urlsafe_b64encode(normalized_bytes).decode("utf-8") concatenated_with_ts = f"{encoded_base64url}{timestamp}" message = concatenated_with_ts.encode("utf-8") # public_key = RSA.import_key(public_key_pem) verifier = PKCS115_SigScheme(public_key) signature = base64url_decode(signature_b64url) # verifier.verify(SHA256.new(message), signature) return True except Exception: return False

Security Recommendations

  • Save the public key of the cash register in the application configuration.

  • Update the public key when the keys in your personal account change.

  • Check idempotency of alerts by fieldsproject_id, payment_id, status, sub_status.

    The indempotency check ensures that reprocessing the same alert does not change the final result. Maintain a combination of these fields to prevent duplicate transactions.
  • Check the valid time window fortimestamp.

Signature verification form

Use the form below to verify that the RSA-SHA256 signature is generated correctly for requests to the HighHelp API.

Processing of the entered data is performed locally in the browser; data is not transferred to the server.

Performing a test

  1. Paste the JSON body of the request into the first field.

  2. Paste the RSA private key (PEM).

  3. Specify a timestamp in Unix timestamp format.

  4. Insert signaturex-access-signature, which needs to be checked.

  5. Click the buttonCheck signature.

The form will display the step-by-step signature generation process and the calculated signature. If the entered signature does not match, use the calculated value as the correct one.

Sample test data

The following test data can be used to verify the signature:

JSON request body:

{"general":{"project_id":"test-project-123"},"payment":{"amount":100000,"currency":"USD"}}

Private RSA key (PEM):

-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----

Timestamp: 1716299720

Received signature: signature-to-verify

After pressing the buttonCheck signaturethe form will display:

  1. Normalized data representation.

  2. base64url(normalized).

  3. message = base64url(normalized) + timestamp.

  4. Computed signature (x-access-signature).

  5. Signature comparison result.