Skip to main content

How to sign

Signature Rules

  1. Sort parameters by ASCII code in ascending order (dictionary order)
  2. Connect parameter names and values with "="
  3. Connect parameter pairs with "&"
  4. Sign using SHA256withRSA algorithm
  5. Encode the signature result with Base64

Signature Example

Simple Parameters

// Original parameters
{
"amount": "100",
"currency": "USDT",
"nonce": "202402241530",
"outTradeNo": "TEST123456",
"timestamp": "1708752612"
}

// Sort by dictionary order and concatenate
amount=100&currency=USDT&nonce=202402241530&outTradeNo=TEST123456&timestamp=1708752612

// Sign with merchant private key to get Base64 encoded signature result
sign=Base64(SHA256withRSA(merchant_private_key, sorted_string))

Complex Parameters with Nested Objects

// Original parameters with nested object
{
"amount": "0.01",
"payChannel": "payway",
"currency": "USD",
"currencyId": "USD",
"extra": {
"channel_pay_type": "cards"
}
}

// Step 1: Convert nested objects to JSON string
{
"amount": "0.01",
"payChannel": "payway",
"currency": "USD",
"currencyId": "USD",
"extra": "{\"channel_pay_type\":\"cards\"}"
}

// Step 2: Sort by dictionary order and concatenate
amount=0.01&currency=USD&currencyId=USD&extra={"channel_pay_type":"cards"}&payChannel=payway

// Step 3: Sign with merchant private key
sign=Base64(SHA256withRSA(merchant_private_key, sorted_string))

Important Note: When the extra field contains multiple parameters, the keys within the extra object must also be sorted by ASCII order before JSON serialization.

Example with Multiple Extra Parameters

// Original parameters
{
"payChannel": "payChannelName",
"amount": "1.5",
"currency": "USDT",
"currencyId": "USDT",
"timestamp": "1757913914",
"payAddress": "+855-xxxxxxxx",
"outTradeNo": "78988784565456",
"extra": {
"channel_pay_type": "card",
"description": "edison",
"attach": "edison"
}
}

// Step 1: Sort keys within extra object by ASCII order
// attach -> channel_pay_type -> description
// Then convert to JSON string
{
"payChannel": "payChannelName",
"amount": "1.5",
"currency": "USDT",
"currencyId": "USDT",
"timestamp": "1757913914",
"payAddress": "+855-xxxxxxxx",
"outTradeNo": "78988784565456",
"extra": "{\"attach\":\"edison\",\"channel_pay_type\":\"card\",\"description\":\"edison\"}"
}

// Step 2: Sort all parameters by ASCII order and concatenate
amount=1.5&currency=USDT&currencyId=USDT&extra={"attach":"edison","channel_pay_type":"card","description":"edison"}&outTradeNo=78988784565456&payAddress=+855-xxxxxxxx&payChannel=payChannelName&timestamp=1757913914

// Step 3: Sign with merchant private key
sign=Base64(SHA256withRSA(merchant_private_key, sorted_string))

Real-world Example

// Original parameters
{
"payChannel": "payChannelName",
"amount": "20",
"currency": "USDH",
"currencyId": "USDH",
"timestamp": "1754981843",
"timeExpire": "900",
"extra": {
"channel_pay_type": "cards"
}
}

// After sorting and concatenation
amount=20&currency=USDH&currencyId=USDH&extra={"channel_pay_type":"cards"}&outTradeNo=1757313174350770800&payChannel=payChannelName&timeExpire=900&timestamp=1754981843

// Final signed request
{
"payChannel": "payChannelName",
"sign": "i4vN6MpFF1fe1KeEUpUreNMSpk7ac9MWclrDJvgptUJ4eyQXF3vbmSfgEZZBqQoz9aEom3EkaEW9iLbGFhY2vzK8oqr9NRcDEOmjNzwnwJHZp+L6NzKVgc/2piRCMpH0sUH/vTJpn0fqJX1xMvucaclVQB/dMWXT4NgoRujdjXk=",
"outTradeNo": "1757313174350770800",
"amount": "20",
"currency": "USDH",
"currencyId": "USDH",
"timestamp": "1754981843",
"timeExpire": "900",
"payAddress": "",
"extra": {
"channel_pay_type": "cards"
}
}

golang sdk

package service

import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"sort"
"strings"
"time"
)

type SignSevice struct {
Appid string
PrivateKey string
PublicKey string
}

func NewSignSevice() *SignSevice {
return &SignSevice{}
}

func (p *SignSevice) parsePrivateKey(prikey string) (*rsa.PrivateKey, error) {
block, _ := pem.Decode([]byte(prikey))
if block == nil || block.Type != "PRIVATE KEY" {
return nil, errors.New("failed to parse private key")
}

privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parse private key failed: %v", err)
}

// Type assertion to *rsa.PrivateKey
rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("private key type is not rsa")
}
return rsaPrivateKey, nil
}

func (p *SignSevice) formatPublicKey(publicKey string) string {
const lineLength = 64
var b strings.Builder
b.WriteString("-----BEGIN PUBLIC KEY-----\n")

for len(publicKey) > 0 {
chunk := publicKey
if len(chunk) > lineLength {
chunk = publicKey[:lineLength]
publicKey = publicKey[lineLength:]
} else {
publicKey = ""
}
b.WriteString(chunk)
b.WriteByte('\n')
}
b.WriteString("-----END PUBLIC KEY-----\n")
return b.String()
}

func (p *SignSevice) parsePublicKey(key string) (*rsa.PublicKey, error) {
formattedPubKey := p.formatPublicKey(key)
block, _ := pem.Decode([]byte(formattedPubKey))
if block == nil {
return nil, errors.New("public key is invalid")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parse public key failed: %v", err)
}
rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return nil, errors.New("public key type is not rsa")
}
return rsaPub, nil
}

// Sort and concatenate parameters
func (p *SignSevice) sortAndConcatenate(params map[string]interface{}) string {
var keys []string
for key := range params {
if key != "sign" && params[key] != "" && params[key] != nil {
keys = append(keys, key)
}
}
// Sort by ASCII
sort.Strings(keys)
// Concatenate fields
var sb strings.Builder
for _, key := range keys {
sb.WriteString(key + "=")
strVal := ""
strVal = fmt.Sprintf("%v&", params[key])
sb.WriteString(strVal)
}
return strings.TrimRight(sb.String(), "&")
}

// Generate signature
func (p *SignSevice) GenerateSignature(prikey string, params map[string]any) (string, error) {
privateKey, err := p.parsePrivateKey(prikey)
if err != nil {
return "", err
}
// Concatenate parameters
concatenatedParams := p.sortAndConcatenate(params)
// SHA256 hash
hash := sha256.New()
hash.Write([]byte(concatenatedParams))
hashBytes := hash.Sum(nil)
// Sign
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashBytes)
if err != nil {
return "", err
}
// Convert to Base64 encoding
return base64.StdEncoding.EncodeToString(signature), nil
}

Multi-Language Implementation

Handling Nested Objects

When dealing with complex parameters containing nested objects (like the extra field), different programming languages need to handle JSON serialization consistently.

JavaScript/Node.js Example

const crypto = require('crypto');

function handleNestedObjects(params) {
const processedParams = {};

for (const [key, value] of Object.entries(params)) {
if (key === 'sign') continue;

if (typeof value === 'object' && value !== null) {
// Convert nested objects to JSON string
processedParams[key] = JSON.stringify(value);
} else {
processedParams[key] = String(value);
}
}

return processedParams;
}

function generateSignature(params, privateKey) {
// Step 1: Handle nested objects
const processedParams = handleNestedObjects(params);

// Step 2: Sort and concatenate
const sortedKeys = Object.keys(processedParams).sort();
const queryString = sortedKeys
.map(key => `${key}=${processedParams[key]}`)
.join('&');

// Step 3: Sign with RSA-SHA256
const sign = crypto.createSign('RSA-SHA256');
sign.update(queryString);
return sign.sign(privateKey, 'base64');
}

// Usage example
const params = {
"amount": "0.01",
"payChannel": "payway",
"currency": "USD",
"currencyId": "USD",
"extra": {
"channel_pay_type": "cards",
"attach": "custom_data",
}
};

const signature = generateSignature(params, privateKey);

Python Example

import json
import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from collections import OrderedDict

def handle_nested_objects(params):
processed_params = {}

for key, value in params.items():
if key == 'sign':
continue

if isinstance(value, dict):
# Sort keys within nested objects by ASCII order before JSON serialization
sorted_dict = OrderedDict(sorted(value.items()))
processed_params[key] = json.dumps(sorted_dict, separators=(',', ':'))
else:
processed_params[key] = str(value)

return processed_params

def generate_signature(params, private_key_pem):
# Step 1: Handle nested objects
processed_params = handle_nested_objects(params)

# Step 2: Sort and concatenate
sorted_keys = sorted(processed_params.keys())
query_string = '&'.join([f"{key}={processed_params[key]}" for key in sorted_keys])

# Step 3: Load private key and sign
private_key = serialization.load_pem_private_key(
private_key_pem.encode(),
password=None
)

signature = private_key.sign(
query_string.encode(),
padding.PKCS1v15(),
hashes.SHA256()
)

return base64.b64encode(signature).decode()

# Usage example
params = {
"amount": "0.01",
"payChannel": "payway",
"currency": "USD",
"currencyId": "USD",
"extra": {
"attach": "custom_data",
"channel_pay_type": "cards"
}
}

signature = generate_signature(params, private_key_pem)

Java Example

import com.fasterxml.jackson.databind.ObjectMapper;
import java.security.PrivateKey;
import java.security.Signature;
import java.util.*;
import java.util.stream.Collectors;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class SignatureUtil {

private static final ObjectMapper objectMapper = new ObjectMapper();

public static Map<String, String> handleNestedObjects(Map<String, Object> params) {
Map<String, String> processedParams = new HashMap<>();

for (Map.Entry<String, Object> entry : params.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();

if ("sign".equals(key)) {
continue;
}

if (value instanceof Map || value instanceof List) {
try {
// Convert nested objects to JSON string
processedParams.put(key, objectMapper.writeValueAsString(value));
} catch (Exception e) {
throw new RuntimeException("Failed to serialize nested object", e);
}
} else {
processedParams.put(key, String.valueOf(value));
}
}

return processedParams;
}

public static String generateSignature(Map<String, Object> params, PrivateKey privateKey) {
try {
// Step 1: Handle nested objects
Map<String, String> processedParams = handleNestedObjects(params);

// Step 2: Sort and concatenate
String queryString = processedParams.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining("&"));

// Step 3: Sign with RSA-SHA256
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(queryString.getBytes(StandardCharsets.UTF_8));

byte[] signatureBytes = signature.sign();
return Base64.getEncoder().encodeToString(signatureBytes);

} catch (Exception e) {
throw new RuntimeException("Failed to generate signature", e);
}
}
}

Key Points for Cross-Language Compatibility

  1. JSON Serialization: Always use compact JSON format (no spaces) when converting nested objects to strings
  2. Parameter Sorting: Sort parameters by key name using ASCII/lexicographic order
  3. Encoding: Use UTF-8 encoding for all string operations
  4. Signature Algorithm: Use SHA256withRSA (PKCS#1 v1.5 padding)
  5. Base64 Encoding: Use standard Base64 encoding for the final signature

Common Pitfalls to Avoid

  • Inconsistent JSON formatting: Different languages may produce different JSON strings for the same object
  • Parameter filtering: Always exclude the sign parameter and empty/null values
  • Character encoding: Ensure UTF-8 encoding is used consistently
  • Key sorting: Use case-sensitive ASCII sorting, not locale-specific sorting