About The Sign

  • ach-access-sign request header
  • The request header "ach-access-sign" is generated by encrypting the concatenation of timestamp + method + requestPath + body using the HMAC SHA256 method with a SecretKey. The result is then encoded using Base-64.
  • In the requestPath, the parameters follow the same rules as the body. The values in a list are sorted in the following order: integers, floats/doubles, strings, and then lists and objects. Within each type, such as integers or doubles, sorting is done in ascending dictionary order. Sorting between lists and objects is based on their position in the array. Substructures like objects and lists within the main structure are recursively sorted using the same rules. Null values, empty strings (''), empty lists ([]), and empty dictionaries ({}) are excluded before generating the signature.

Sign String Example

  • Example: { {“x”: 1, “y”: 2}, 1, 3, 2, -4, 1.1, “xxxxx”, “yyyy”, “jscx”, 0, “sss”,{“z”:2,”x”:1,”a”:””}}
  • After sorting: { -4,0,1,2,3,1.1,”jscx”,”sss”,”xxxxx”,”yyy”,{“x”: 1, “y”: 2},{“x”: 1, “z”: 2}}

Please note the following guidelines:

  • Please note that, in general, the sorting of data in lists during transmission should not be related to the content being transmitted.
    In the case where both the path and body contain parameters, each should be sorted individually. Then, concatenate them in the order of timestamp + method + requestPath + body for signing.
    Here's an example:
    Timestamp: 1538054050234
    HTTP Method: GET
    Path: /api/v1/crypto/order?order_no=sdf23&token=ETH
    Body: Empty
    The signature content would be generated as follows:
    "1538054050234" + "GET" + "/api/v1/crypto/order?token=ETH&order_no=sdf23"
  • For the clarification you provided:
    The timestamp value should match the "ach-access-timestamp" request header and follow the ISO format, representing Unix time in milliseconds with a 13-digit timestamp. For example: 1538054050231.
    The method refers to the HTTP request method and should be in uppercase letters. For instance: GET/POST.
    The requestPath represents the API endpoint path and is case-sensitive. If the URL already ends with a '/', it should still be included in the signing process. For example: /api/v1/crypto/order.
  • The body refers to the string representation of the request payload. If the request does not have a body (typically for GET requests), the body can be omitted in the signing process. When present, the body should be sorted in dictionary order, and its internal structure should also follow the dictionary ordering. Empty values are excluded and not considered in the signing process.
    For example: '1538054051230' + 'GET' + '/api/v1/crypto/token/price' + body
    Both the secretKey and apiKey are case-sensitive.
    The HMAC SHA256 algorithm is used with a secret key to sign the hashed string.
    The resulting signature is encoded in Base64 format.

Sign Example

import base64
import hmac
import json
import requests
from datetime import datetime
from typing import Dict, Any, List, Union, Type


# 签名
class SignatureUtility:
    def generate_signature(self, secret_key: str, message: str) -> str:
        """Generate the HMAC SHA256 signature for a given message."""
        signature = hmac.new(
            secret_key.encode(), message.encode(), digestmod="sha256"
        ).digest()
        return base64.b64encode(signature).decode()

    def verify_signature(
        self, secret_key: str, message: str, received_signature: str
    ) -> bool:
        """Verify the received signature against the computed one."""
        computed_signature = self.generate_signature(secret_key, message)
        return hmac.compare_digest(computed_signature, received_signature)

    def clean_and_sort_dict(
        self, data: Union[Dict[Any, Union[Dict, Any]], List[Union[Dict, Any]]]
    ) -> Union[Dict[Any, Union[Dict, Any]], List[Union[Dict, Any]]]:
        if isinstance(data, dict):
            sorted_dict = {}
            for key, value in sorted(data.items()):
                if isinstance(value, (dict, list)):
                    value = self.clean_and_sort_dict(value)

                # Checking for non-empty values, including non-empty lists and non-empty dictionaries
                if value or value == 0:
                    sorted_dict[key] = value
            return (
                sorted_dict if sorted_dict else None
            )  # Return None if the dictionary is empty
        elif isinstance(data, list):
            int_list = sorted([item for item in data if isinstance(item, int)])
            float_list = sorted([item for item in data if isinstance(item, float)])
            str_list = sorted([item for item in data if isinstance(item, str)])
            complex_data_types = [
                item for item in data if isinstance(item, (dict, list))
            ]

            sorted_complex_data = [
                self.clean_and_sort_dict(item) for item in complex_data_types
            ]
            sorted_complex_data = [
                item for item in sorted_complex_data if item
            ]  # Filter out None values

            result = int_list + float_list + str_list + sorted_complex_data
            return result if result else None  # Return None if the list is empty





#
def post_request(URL: str, body: Type):
    sign_utility = SignatureUtility()
    query_string = ""
    method = "POST"
    #
    # 请求签名
    body_str = ""
    timestamp = str(int(datetime.utcnow().timestamp() * 1000))
    req_cleaned_body = sign_utility.clean_and_sort_dict(body)
    if req_cleaned_body is not None:
        body_str = json.dumps(
            req_cleaned_body,
            sort_keys=True,
            separators=(",", ":"),
            ensure_ascii=False,
        )
    message = f"{timestamp}{method}{URL}{query_string}{body_str}"
    print(message)
    sign = sign_utility.generate_signature(secret_key=SECRET_KEY, message=message)
    print(sign)
    # POST REQUEST
    headers = {
        "ach-access-key": API_KEY,
        "ach-access-sign": sign,
        "ach-access-timestamp": timestamp,
    }
    response = requests.post(url=HOST + URL, headers=headers, json=body)
    return response.text



API_KEY = "service000-local-apikey"
SECRET_KEY = "service000-local-secretkey"
HOST = "http://127.0.0.1:8072"

#
if __name__ == "__main__":
    url = "/open/api/card/create"
    body = {
        "callbackUrl": "http://baidu.com",
        "cardHolder": {
            "address": {
                "city": "string",
                "country": "string",
                "state": "string",
                "street": "string",
                "zipCode": "string"
            },
            "firstName": "string",
            "lastName": "string"
        },
        "customerId": "user_id_123",
        "deposit": "100",
        "orderNo": "12165456165441",
        "tagNameList": [
            "string"
        ],
        "vid": "vab_069af8a792ad"
    }
    response = post_request(url, body)
    print(f"response: {response}")