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}")

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.collections4.list.TreeList;
import org.apache.commons.lang3.StringUtils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.*;

public class AchVccSignUtils {

    public static String apiSign(String timestamp, String method, String requestUrl, String body, String secretkey) throws NoSuchAlgorithmException, InvalidKeyException {
        String signVal = "";
        if("POST".equalsIgnoreCase(method)) {
            String content = timestamp + method.toUpperCase() + getPath(requestUrl) + getJsonBody(body);
            System.out.println("POST signString:" + content);
            Base64.Encoder base = Base64.getEncoder();
            signVal = base.encodeToString(sha256(content.getBytes(StandardCharsets.UTF_8), secretkey.getBytes(StandardCharsets.UTF_8)));
        }else {
            String content = "";
            if(!StringUtils.isEmpty(body) && !"null".equalsIgnoreCase(body) && !"{}".equals(body)) {
                content = timestamp + method.toUpperCase() + getPath(requestUrl) + orderGetSignStr((Map) JSONObject.parseObject(body));
            }else {
                content = timestamp + method.toUpperCase() + getPath(requestUrl);
            }
            System.out.println("GET signString:" + content);
            Base64.Encoder base = Base64.getEncoder();
            signVal = base.encodeToString(sha256(content.getBytes(StandardCharsets.UTF_8), secretkey.getBytes(StandardCharsets.UTF_8)));
        }

        return signVal;
    }

    public static byte[] sha256(byte[] message, byte[] secret) throws NoSuchAlgorithmException, InvalidKeyException {
        Mac sha256_HMAC = Mac.getInstance("HmacSha256");
        SecretKeySpec secretKey = new SecretKeySpec(secret, "HmacSha256");
        sha256_HMAC.init(secretKey);
        return sha256_HMAC.doFinal(message);
    }


    private static String getPath(String requestUrl) {
        String path = getURI(requestUrl).getPath();

        Map map = parsePath(requestUrl);
        map = removeEmptyKeys(map);
        if (map.isEmpty()) {
            return path;
        }
        map = (Map) sortObject(map);

        if (map.isEmpty()) {
            return path;
        } else {
            StringBuffer httpUlr = new StringBuffer();
            StringBuffer sb = new StringBuffer("?");
            Iterator<Map.Entry> iterator = map.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry<String, Object> entry = iterator.next();
                sb.append(entry.getKey() + "=" + entry.getValue() + "&");
            }
            return httpUlr.append(path).append(sb.substring(0, sb.length() - 1)).toString();
        }
    }

    private static String getJsonBody(String body) {

        Map map = null;

        if (isJson(body)) {
            map = JSONObject.parseObject(body, Map.class);
        } else {
            map = new HashMap();
        }

        if (map == null || map.isEmpty()) {
            return "";
        }
        map = removeEmptyKeys(map);
        map = (Map) sortObject(map);
        return JSON.toJSONString(map);
    }


    private static URI getURI(String requestUrl) {
        URL url = null;
        URI uri = null;
        try {
            url = new URL(requestUrl);
            uri = url.toURI();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return uri;
    }


    private static String orderGetSignStr(Map<String, String> params) {
        String signStr = "?";

        if (params != null) {
            params = removeEmptyKeys(params);

            params = (Map) sortObject(params);
            for(String k:params.keySet()){
                signStr += k+"="+params.get(k)+"&";
            }
        }
        return signStr.substring(0,signStr.length()-1);
    }


    private static Map<String, String> parsePath(String requestUrl) {
        URI uri = getURI(requestUrl);
        String path = uri.getPath();

        // 获取请求参数
        String query = uri.getQuery();
        if (query != null) {
            Map<String, String> params = new HashMap<>();
            String[] keyValuePairs = query.split("&");
            for (String pair : keyValuePairs) {
                String[] keyValuePair = pair.split("=");
                String key = keyValuePair[0];
                String value = keyValuePair.length > 1 ? keyValuePair[1] : null;
                params.put(key, value);
            }
            System.out.println("Parameters: " + params);
            return params;
        } else {
            System.out.println("No parameters found.");
            return new HashMap<>();
        }
    }

    private static boolean isJson(String jsonString) {
        if (jsonString == null || jsonString.equals("")) {
            return false;
        }
        try {
            JSON.parse(jsonString);
            return true;
        } catch (JSONException e) {
            return false;
        }
    }

    private static Map removeEmptyKeys(Map map) {
        if (map.isEmpty()) {
            return map;
        }
        Map retMap = new HashMap();
        Iterator<Map.Entry> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, Object> entry = iterator.next();
            if (entry.getValue() != null && !entry.getValue().equals("")) {
                retMap.put(entry.getKey(), entry.getValue());
            }
        }
        return retMap;
    }

    private static Object sortObject(Object obj) {
        if (obj instanceof Map) {
            return sortMap((Map) obj);
        } else if (obj instanceof List) {
            sortList((List) obj);
            return obj;
        }
        return null;
    }

    private static Map sortMap(Map map) {
        if (map.isEmpty()) {
            return null;
        }
        SortedMap<String, Object> sortedMap = new TreeMap<>(removeEmptyKeys(map));
        for (String sortKey : sortedMap.keySet()) {
            if (sortedMap.get(sortKey) instanceof Map) {
                sortedMap.put(sortKey, sortMap((Map) sortedMap.get(sortKey)));
            } else if (sortedMap.get(sortKey) instanceof List) {
                sortedMap.put(sortKey, sortList((List) sortedMap.get(sortKey)));
            }
        }
        return sortedMap;
    }

    private static List sortList(List list) {
        if (list.isEmpty()) {
            return null;
        }
        List objectList = new ArrayList();

        List intList = new ArrayList();
        List floatList = new ArrayList();
        List stringList = new ArrayList();

        List jsonArray = new ArrayList();
        for (Object obj : list) {
            if (obj instanceof Map || obj instanceof List) {
                jsonArray.add(obj);
            } else if (obj instanceof Integer) {
                intList.add(obj);
            } else if (obj instanceof BigDecimal) {
                floatList.add(obj);
            } else if (obj instanceof String) {
                stringList.add(obj);
            } else {
                intList.add(obj);
            }
        }

        Collections.sort(intList);
        Collections.sort(floatList);
        Collections.sort(stringList);

        objectList.addAll(intList);
        objectList.addAll(floatList);
        objectList.addAll(stringList);
        objectList.addAll(jsonArray);

        list.clear();
        list.addAll(objectList);


        List retList = new TreeList();

        for (Object obj : list) {
            if (obj instanceof Map) {
                retList.add(sortMap((Map) obj));
            } else if (obj instanceof List) {
                retList.add(sortList((List) obj));
            } else {
                retList.add(obj);
            }
        }
        return retList;
    }

    public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
        String timestamp = String.valueOf(System.currentTimeMillis());
        String sign = apiSign(timestamp, "POST", "XXXXXXXX", "JSONbody", "secretKey");
        System.out.println("ach-access-sign:"+ sign);
    }
}