API Signature Generation

# Signature Rules

All parameter names and values are included in the signature, with Key (parameter name) sorted

  1. Sort all parameter Keys (parameter names) according to ASCII order.
  2. Concatenate each parameter name and its value in the order of sorted Keys, then use the RSA algorithm to calculate the signature string from the concatenated string.

# Example Explanation

  • Example Parameters
Parameter Name Parameter Value
merchantCode S820190712000002
orderNum T1231511321515
orderAmount 999.56
callback https://xxx/yyy
timestamp 1745377181
  • Parameter Format

Formatting rules description:

  1. Extract all keys from the incoming key-value pair parameters (Map) and convert them into a string array sorted in lexicographical ascending order.
  2. Iterate through the sorted key array and concatenate each key and its value in the form of “key=value&” into a string sequentially.
  3. Remove the extra “&” at the end of the concatenated string to obtain the formatted parameter string.
  4. The above rules are encapsulated in TopPaySignUtil#paramFormat, which returns the following source when called:

source = callback=https://xxx/yyy&merchantCode=S820190712000002&orderAmount=999.56&orderNum=T1231511321515&timestamp=1745377181

  • Calculate Signature
  1. Use the key pair configured in your TopPay merchant backend.
  2. Use your Private Key and the formatted source string as inputs to call TopPaySignUtil#sign, obtaining the final signature string:

Sign = Jv0AMYiVSL/V8NxuBf8ZfHn7UyHO8TsU8Xkh2sqa0hbpKH1HSPampNXxzBn5PvJoytb8zPkHuQAMveTuBV5Ye8Qu+n8aw69 ...

# Code Example

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.RuntimeException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class TopPaySignUtil {

    private static final String RSA = "RSA";
    private static final int MAX_ENCRYPT_BLOCK = 245;  
    private static final int MAX_DECRYPT_BLOCK = 256;   
    private static final String RSA_ECB_PKCS1_PADDING = "RSA/ECB/PKCS1Padding";

    public static void main(String[] args) throws NoSuchAlgorithmException {
        Map<String,String> params = new HashMap<>() ;
        params.put("merchantCode", "S820190712000002") ;
        params.put("orderAmount", "999.56") ;
        params.put("orderNum", "T1231511321515") ;
        params.put("callback", "https://xxx/yyy") ;
        params.put("timestamp", "1745377181") ;

        String source = paramFormat(params);
        System.out.println("source: " + source);

        KeyPairGenerator keyGen = KeyPairGenerator.getInstance(RSA);
        keyGen.initialize(2048);
        KeyPair pair = keyGen.generateKeyPair();

        String pubKeyBase64 = Base64.getEncoder().encodeToString(pair.getPublic().getEncoded());
        String priKeyBase64 = Base64.getEncoder().encodeToString(pair.getPrivate().getEncoded());

        System.out.println("public key: " + pubKeyBase64);
        System.out.println("private key: " + priKeyBase64);

        String sign = sign(priKeyBase64, source);
        System.out.println("sign: " + sign);

        boolean verify = verify(pubKeyBase64, source, sign);
        System.out.println("verify: " + verify);
    }

    /**
     * This code is for reference only, and needs to be considered in actual use
     * 1.HttpClient Initialize participation
     * 2.HttpResponse Need to close it in time after use
     */
    public static String doPost(String url, String json) throws IOException {
        HttpClient client = new DefaultHttpClient();
        HttpPost post = new HttpPost(url);
        StringEntity s = new StringEntity(json);
        s.setContentEncoding("UTF-8");
        s.setContentType("application/json");
        post.setEntity(s);
        HttpResponse res = client.execute(post);
        if (res.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
            return EntityUtils.toString(res.getEntity());
        }
        return null;
    }
    
    //Parameter formatting
    private static String paramFormat(Map<String, String> param) {
        String[] keys = param.keySet().toArray(new String[0]);
        Arrays.sort(keys); //Must sort first
        StringBuilder builder = new StringBuilder();
        for (String key : keys) {
            String value = param.get(key);
            if (value != null && !value.trim().isEmpty()) { //null and empty strings need to be filtered
                builder.append(key).append("=").append(value).append("&");
            }
        }
        builder.deleteCharAt(builder.length()-1) ;
        return builder.toString();
    }

    /**
     * Calculate the signature
     * @param priKeyBase64 Base64 encoded private key
     * @param source The source data used to calculate the signature
     * @return RSA signature string
     */
    public static String sign(String priKeyBase64, String source) {
        return encrypt(source, stringToPrivateKey(priKeyBase64)) ;
    }

    /**
     * Verify the signature
     * @param pubKeyBase64 Base64 encoded public key
     * @param source The source data used to calculate the signature
     * @param sign A signature that needs to be verified
     * @return RSA signature string
     */
    public static boolean verify(String pubKeyBase64, String source, String sign) {
        return source.equals(decrypt(sign, stringToPublicKey(pubKeyBase64)));
    }

    // RSA private key encryption
    private static String encrypt(String plainData, PrivateKey privateKey) {
        Cipher cipher = null;  
        try {
            cipher = Cipher.getInstance(RSA_ECB_PKCS1_PADDING);
            cipher.init(Cipher.ENCRYPT_MODE, privateKey);

            byte[] dataBytes = plainData.getBytes(StandardCharsets.UTF_8);
            int inputLen = dataBytes.length;
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            int offSet = 0;
            
            while (inputLen - offSet > 0) {
                int blockSize = Math.min(inputLen - offSet, MAX_ENCRYPT_BLOCK);
                byte[] encryptedBlock = cipher.doFinal(dataBytes, offSet, blockSize);
                out.write(encryptedBlock);
                offSet += blockSize;
            }
            return Base64.getEncoder().encodeToString(out.toByteArray());
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | IOException | InvalidKeyException |
                 IllegalBlockSizeException | BadPaddingException e) {
            throw new RuntimeException("encrypt error.");
        }

    }

    // RSA public key decryption
    private static String decrypt(String encryptedData, PublicKey publicKey) {
        Cipher cipher = null;
        try {
            cipher = Cipher.getInstance(RSA_ECB_PKCS1_PADDING);
            cipher.init(Cipher.DECRYPT_MODE, publicKey);

            byte[] dataBytes = Base64.getDecoder().decode(encryptedData);
            int inputLen = dataBytes.length;
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            int offSet = 0;
            
            while (inputLen - offSet > 0) {
                int blockSize = Math.min(inputLen - offSet, MAX_DECRYPT_BLOCK);
                byte[] decryptedBlock = cipher.doFinal(dataBytes, offSet, blockSize);
                out.write(decryptedBlock);
                offSet += blockSize;
            }
            return new String(out.toByteArray(), StandardCharsets.UTF_8);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException |
                 IOException | InvalidKeyException e) {
            throw new RuntimeException("decrypt error.");
        }

    }


    // Recover the PublicKey from the Base64 string
    public static PublicKey stringToPublicKey(String publicKeyStr) {
        byte[] keyBytes = Base64.getDecoder().decode(publicKeyStr);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        try {
            KeyFactory keyFactory = KeyFactory.getInstance(RSA);
            return keyFactory.generatePublic(keySpec);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw new RuntimeException("generatePublicKey error.");
        }
    }


    // Recover the PrivateKey from the Base64 string
    public static PrivateKey stringToPrivateKey(String privateKeyStr) {
        byte[] keyBytes = Base64.getDecoder().decode(privateKeyStr);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        try {
            KeyFactory keyFactory = KeyFactory.getInstance(RSA);
            return keyFactory.generatePrivate(keySpec);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw new RuntimeException("generatePrivateKey error.");
        }
    }
}


<?php
class TopPaySignUtil
{
    const MAX_ENCRYPT_BLOCK = 245;
    const MAX_DECRYPT_BLOCK = 256;

    public static function main()
    {

        $params = [
            'merchantCode' => 'S820190712000002',
            'orderAmount' => '999.56',
            'orderNum' => 'T1231511321515',
            'callback' => 'https://xxx/yyy',
            'timestamp' => '1745377181'
        ];

        $source = self::paramFormat($params);
        // Generate RSA keypair (same as Java KeyPairGenerator)
        $config = [
            "private_key_bits" => 2048,
            "private_key_type" => OPENSSL_KEYTYPE_RSA,
            // You may need to specify your openssl.cnf path
//            'config' => 'D:/phpstudy_pro/Extensions/php/php7.3.4nts/extras/ssl/openssl.cnf',
        ];

        $res = openssl_pkey_new($config);
        openssl_pkey_export($res, $priKeyPem);
        $pubKeyPem = openssl_pkey_get_details($res)['key'];

        if (!openssl_pkey_export($res, $priKeyPem, null, $config)) {
            echo "openssl_pkey_export error: " . openssl_error_string();
        }
        // Convert to DER (same as Java PublicKey.getEncoded() / PrivateKey.getEncoded())
        $priDer = self::pemToDer($priKeyPem);
        $pubDer = self::pemToDer($pubKeyPem);

        $priKeyBase64 = base64_encode($priDer);
        $pubKeyBase64 = base64_encode($pubDer);
        echo "public key: $pubKeyBase64<br>";
        echo "private key: $priKeyBase64<br>";
        // Private key encryption
        $sign = self::sign($priKeyBase64, $source);
        echo "source: $source<br>";
        echo "sign: $sign<br>";
        // Public key decryption
        $verify = self::verify($pubKeyBase64, $source, $sign);
        echo "verify: " . ($verify ? "true" : "false") . "<br>";
    }

    // ========= Java: paramFormat ===========
    public static function paramFormat($param)
    {
        ksort($param);
        $result = '';
        foreach ($param as $key => $value) {
            if ($value !== null && trim($value) !== '') {
                $result .= $key . "=" . $value . "&";
            }
        }
        return rtrim($result, '&');
    }

    // ============ DER / PEM Conversion ============
    public static function pemToDer($pem)
    {
        return base64_decode(
            preg_replace('/-----(BEGIN|END) (PUBLIC|PRIVATE) KEY-----/', '', trim($pem))
        );
    }

    private static function derToPrivateKey($der)
    {
        $pem = "-----BEGIN PRIVATE KEY-----\n" .
            chunk_split(base64_encode($der), 64, "\n") . "\n" .
            "-----END PRIVATE KEY-----";
        return openssl_pkey_get_private($pem);
    }

    private static function derToPublicKey($der)
    {
        $pem = "-----BEGIN PUBLIC KEY-----\n" .
            chunk_split(base64_encode($der), 64, "\n") . "\n" .
            "-----END PUBLIC KEY-----";
        return opensssl_pkey_get_public($pem);
    }

    // ============ Java: Private Key Encryption ============
    public static function sign($priKeyBase64, $source)
    {
        $privateKey = self::derToPrivateKey(base64_decode($priKeyBase64));
        return self::encryptByPrivateKey($source, $privateKey);
    }

    // ============ Java: Public Key Decryption ============
    public static function verify($pubKeyBase64, $source, $sign)
    {
        $publicKey = self::derToPublicKey(base64_decode($pubKeyBase64));
        $decrypt = self::decryptByPublicKey($sign, $publicKey);
        return $decrypt === $source;
    }

    // ============ Private key encryption (same as Java) ============
    private static function encryptByPrivateKey($data, $key)
    {
        $encrypted = '';
        $chunks = str_split($data, self::MAX_ENCRYPT_BLOCK);

        foreach ($chunks as $chunk) {
            openssl_private_encrypt($chunk, $encryptedChunk, $key, OPENSSL_PKCS1_PADDING);
            $encrypted .= $encryptedChunk;
        }

        return base64_encode($encrypted);
    }

    // ============ Public key decryption (same as Java) ============
    private static function decryptByPublicKey($data, $key)
    {
        $data = base64_decode($data);
        $chunks = str_split($data, self::MAX_DECRYPT_BLOCK);

        $decrypted = '';
        foreach ($chunks as $chunk) {
            openssl_public_decrypt($chunk, $decryptedChunk, $key, OPENSSL_PKCS1_PADDING);
            $decrypted .= $decryptedChunk;
        }

        return $decrypted;
    }

    public static function doPost($url, $json)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array(
            'Content-Type: application/json',
            'Content-Length: ' . strlen($json)
        ));
        // Debug: do not verify peer SSL certificate
//        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        // Debug: do not verify host name in SSL certificate
//        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        $response = curl_exec($ch);
        curl_close($ch);
        return $response;
    }
}

TopPaySignUtil::main();
?>

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using Newtonsoft.Json;
using System.IO;
using Org.BouncyCastle.Crypto.Encodings;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Pkcs;       // PrivateKeyInfoFactory / SubjectPublicKeyInfoFactory
using Org.BouncyCastle.X509;       // SubjectPublicKeyInfoFactory

namespace Demo;

class TopPaySignUtil
{
    private const int MAX_ENCRYPT_BLOCK = 245; 
    private const int MAX_DECRYPT_BLOCK = 256;

    public static async Task Main()
    {
        Dictionary<string, string> paramsDict = new Dictionary<string, string>
        {
            { "merchantCode", "S820190712000002" },
            { "orderAmount", "999.56" },
            { "orderNum", "T1231511321515" },
            { "callback", "https://xxx/yyy" },
            { "timestamp", "1745377181" }
        };

        string source = ParamFormat(paramsDict);
        Console.WriteLine($"source: {source}");

        // ----------- Generate RSA key pair -----------
        var rsa = new Org.BouncyCastle.Crypto.Generators.RsaKeyPairGenerator();
        rsa.Init(new Org.BouncyCastle.Crypto.KeyGenerationParameters(new SecureRandom(), 2048));
        var keyPair = rsa.GenerateKeyPair();

        string priKeyBase64 = Convert.ToBase64String(
            PrivateKeyInfoFactory.CreatePrivateKeyInfo(keyPair.Private).GetEncoded()
        );

        string pubKeyBase64 = Convert.ToBase64String(
            SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(keyPair.Public).GetEncoded()
        );

        Console.WriteLine($"public key: {pubKeyBase64}");
        Console.WriteLine($"private key: {priKeyBase64}");

        // ----------- Private key encryption (Java/sign compatible) -----------
        string sign = Sign(priKeyBase64, source);
        Console.WriteLine($"sign: {sign}");

        // ----------- Public key decryption (Java/verify compatible) -----------
        bool verify = Verify(pubKeyBase64, source, sign);
        Console.WriteLine($"verify: {verify}");
    }

    // ---------------------- Format request parameters ----------------------
    public static string ParamFormat(Dictionary<String, String> param)
    {
        var sortedKeys = param.Keys.OrderBy(k => k).ToList();
        StringBuilder builder = new StringBuilder();

        foreach (var key in sortedKeys)
        {
            if (!string.IsNullOrWhiteSpace(param[key]))
                builder.Append($"{key}={param[key]}&");
        }

        if (builder.Length > 0)
            builder.Length--;

        return builder.ToString();
    }

    // ---------------------- Sign (encrypt with private key) ----------------------
    public static string Sign(string priKeyBase64, string source)
    {
        byte[] data = Encoding.UTF8.GetBytes(source);
        AsymmetricKeyParameter privateKey = PrivateKeyFactory.CreateKey(
            Convert.FromBase64String(priKeyBase64)
        );

        byte[] encrypted = RsaPrivateEncrypt(privateKey, data);
        return Convert.ToBase64String(encrypted);
    }

    // ---------------------- Verify (decrypt with public key) ----------------------
    public static bool Verify(string pubKeyBase64, string source, string sign)
    {
        AsymmetricKeyParameter publicKey = PublicKeyFactory.CreateKey(
            Convert.FromBase64String(pubKeyBase64)
        );

        byte[] encrypted = Convert.FromBase64String(sign);
        byte[] decrypted = RsaPublicDecrypt(publicKey, encrypted);
        string result = Encoding.UTF8.GetString(decrypted);

        return source.Equals(result);
    }

    // ------------------- Private key segmented encryption (Java compatible) -------------------
    private static byte[] RsaPrivateEncrypt(AsymmetricKeyParameter privateKey, byte[] data)
    {
        IAsymmetricBlockCipher engine = new RsaEngine();
        engine = new Pkcs1Encoding(engine);
        engine.Init(true, privateKey);

        using MemoryStream input = new MemoryStream(data);
        using MemoryStream output = new MemoryStream();

        byte[] buffer = new byte[MAX_ENCRYPT_BLOCK];
        int read;

        while ((read = input.Read(buffer, 0, MAX_ENCRYPT_BLOCK)) > 0)
        {
            byte[] block = engine.ProcessBlock(buffer, 0, read);
            output.Write(block, 0, block.Length);
        }

        return output.ToArray();
    }

    // ------------------- Public key segmented decryption (Java compatible) -------------------
    private static byte[] RsaPublicDecrypt(AsymmetricKeyParameter publicKey, byte[] data)
    {
        IAsymmetricBlockCipher engine = new RsaEngine();
        engine = new Pkcs1Encoding(engine);
        engine.Init(false, publicKey);

        using MemoryStream input = new MemoryStream(data);
        using MemoryStream output = new MemoryStream();

        byte[] buffer = new byte[MAX_DECRYPT_BLOCK];
        int read;

        while ((read = input.Read(buffer, 0, MAX_DECRYPT_BLOCK)) > 0)
        {
            byte[] block = engine.ProcessBlock(buffer, 0, read);
            output.Write(block, 0, block.Length);
        }

        return output.ToArray();
    }

    // ------------------- HTTP POST helper -------------------
    public static async Task<string> DoPost(string url, string json)
    {
        using (HttpClient client = new HttpClient())
        {
            var content = new StringContent(json, Encoding.UTF8, "application/json");
            HttpResponseMessage response = await client.PostAsync(url, content);

            if (response.IsSuccessStatusCode)
                return await response.Content.ReadAsStringAsync();

            return null;
        }
    }
}

package main
import (
    "bytes"
    "crypto/rand"
    "crypto/rsa"
    "crypto/x509"
    "encoding/base64"
    "fmt"
    "io/ioutil"
    "math/big"
    "net/http"
    "sort"
    "strings"
)

// Java RSA 2048:
// Maximum encryption block size is 245 bytes, decryption block size is fixed at 256 bytes
const (
    MAX_ENCRYPT_BLOCK = 245
)

func main() {
    params := map[string]string{
        "merchantCode": "S820190712000002",
        "orderAmount":  "999.56",
        "orderNum":     "T1231511321515",
        "callback":     "https://xxx/yyy",
        "timestamp":    "1745377181",
    }

    source := paramFormat(params)
    fmt.Println("source:", source)

    // Generate Java-compatible: X509 public key + PKCS8 private key
    privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
    publicKey := &privateKey.PublicKey

    pubDer, _ := x509.MarshalPKIXPublicKey(publicKey)
    pubKeyBase64 := base64.StdEncoding.EncodeToString(pubDer)

    priDer, _ := x509.MarshalPKCS8PrivateKey(privateKey)
    priKeyBase64 := base64.StdEncoding.EncodeToString(priDer)

    fmt.Println("public key:", pubKeyBase64)
    fmt.Println("private key:", priKeyBase64)

    signData := sign(priKeyBase64, source)
    fmt.Println("sign:", signData)

    verifyOK := verify(pubKeyBase64, source, signData)
    fmt.Println("verify:", verifyOK)
}

func paramFormat(param map[string]string) string {
    var keys []string
    for k := range param {
        keys = append(keys, k)
    }
    sort.Strings(keys)

    var b strings.Builder
    for _, key := range keys {
        v := param[key]
        if strings.TrimSpace(v) != "" {
            b.WriteString(key + "=" + v + "&")
        }
    }
    return strings.TrimRight(b.String(), "&")
}

//////////////////////////////////////////////////////////////////////
// sign (private key encryption) - Java Cipher.ENCRYPT_MODE compatible implementation
//////////////////////////////////////////////////////////////////////

func sign(privateKeyBase64 string, source string) string {
    priv := stringToPrivateKey(privateKeyBase64)
    return rsaPrivateEncrypt(priv, []byte(source))
}

func verify(pubKeyBase64 string, source string, signData string) bool {
    pub := stringToPublicKey(pubKeyBase64)
    plain := rsaPublicDecrypt(pub, signData)
    return plain == source
}

//////////////////////////////////////////////////////////////////////
// Private key encryption (Java RSA/ECB/PKCS1Padding full replication)
//////////////////////////////////////////////////////////////////////

func rsaPrivateEncrypt(priv *rsa.PrivateKey, data []byte) string {
    var buf bytes.Buffer
    k := priv.Size() // 256 bytes

    for i := 0; i < len(data); i += MAX_ENCRYPT_BLOCK {
        end := i + MAX_ENCRYPT_BLOCK
        if end > len(data) {
            end = len(data)
        }

        block := data[i:end]
        encBlock := rsaRawPrivateEncrypt(priv, block)

        // MUST be exactly 256 bytes
        if len(encBlock) != k {
            panic("encrypted block wrong size")
        }
        buf.Write(encBlock)
    }
    return base64.StdEncoding.EncodeToString(buf.Bytes())
}

//////////////////////////////////////////////////////////////////////
// Public key decryption (Java RSA/ECB/PKCS1Padding full replication)
//////////////////////////////////////////////////////////////////////

func rsaPublicDecrypt(pub *rsa.PublicKey, base64Cipher string) string {
    cipherBytes, _ := base64.StdEncoding.DecodeString(base64Cipher)
    k := pub.Size() // 256 bytes

    if len(cipherBytes)%k != 0 {
        panic("cipher size not aligned to RSA block")
    }

    var buf bytes.Buffer
    for i := 0; i < len(cipherBytes); i += k {
        block := cipherBytes[i : i+k]
        plain := rsaRawPublicDecrypt(pub, block)
        buf.Write(plain)
    }
    return buf.String()
}

//////////////////////////////////////////////////////////////////////
// Low-level math operations: PKCS1Padding + private key encryption
//////////////////////////////////////////////////////////////////////

func rsaRawPrivateEncrypt(priv *rsa.PrivateKey, data []byte) []byte {
    k := priv.Size()
    if len(data) > k-11 {
        panic("data too large for RSA")
    }

    paddingLen := k - len(data) - 3
    em := make([]byte, k)
    em[0] = 0x00
    em[1] = 0x01
    for i := 2; i < 2+paddingLen; i++ {
        em[i] = 0xFF
    }
    em[2+paddingLen] = 0x00
    copy(em[3+paddingLen:], data)

    m := new(big.Int).SetBytes(em)
    c := new(big.Int).Exp(m, priv.D, priv.N)

    // Java output is fixed at 256 bytes
    out := c.Bytes()
    if len(out) < k {
        padded := make([]byte, k)
        copy(padded[k-len(out):], out)
        out = padded
    }
    return out
}

//////////////////////////////////////////////////////////////////////
// Low-level math operations: public key decryption + remove PKCS1Padding
//////////////////////////////////////////////////////////////////////

func rsaRawPublicDecrypt(pub *rsa.PublicKey, cipher []byte) []byte {
    k := pub.Size()
    c := new(big.Int).SetBytes(cipher)
    m := new(big.Int).Exp(c, big.NewInt(int64(pub.E)), pub.N)

    out := m.Bytes()
    if len(out) < k {
        padded := make([]byte, k)
        copy(padded[k-len(out):], out)
        out = padded
    }

    em := out
    // PKCS1Padding parsing
    if em[0] != 0x00 || em[1] != 0x01 {
        panic("invalid padding header")
    }

    i := 2
    for ; i < len(em); i++ {
        if em[i] == 0x00 {
            break
        }
    }
    if i == len(em) {
        panic("padding end not found")
    }
    return em[i+1:]
}

//////////////////////////////////////////////////////////////////////
// Base64 → RSA Key parsing
//////////////////////////////////////////////////////////////////////

func stringToPublicKey(b64 string) *rsa.PublicKey {
    der, _ := base64.StdEncoding.DecodeString(b64)
    pub, err := x509.ParsePKIXPublicKey(der)
    if err != nil {
        panic(err)
    }
    return pub.(*rsa.PublicKey)
}

func stringToPrivateKey(b64 string) *rsa.PrivateKey {
    der, _ := base64.StdEncoding.DecodeString(b64)
    key, err := x509.ParsePKCS8PrivateKey(der)
    if err != nil {
        panic(err)
    }
    return key.(*rsa.PrivateKey)
}

//////////////////////////////////////////////////////////////////////
// HTTP POST wrapper
//////////////////////////////////////////////////////////////////////

func doPost(url string, json string) (string, error) {
    resp, err := http.Post(url, "application/json", strings.NewReader(json))
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }
    return string(body), nil
}

# !!!Do not use requests.post(url, json=post_json) — it double-serializes the string
import base64
import requests
from Crypto.PublicKey import RSA
from Crypto.Util.number import bytes_to_long, long_to_bytes


class TopPaySignUtil:
  MAX_ENCRYPT_BLOCK = 245
  MAX_DECRYPT_BLOCK = 256

  @staticmethod
  def main():
    params = {
      'merchantCode': 'S820190712000002',
      'orderAmount': '999.56',
      'orderNum': 'T1231511321515',
      'callback': 'https://xxx/yyy',
      'timestamp': '1745377181'
    }

    source = TopPaySignUtil.param_format(params)
    print(f"source: {source}")

    # Java compatible keys: X509 public key + PKCS8 private key (DER Base64)
    key = RSA.generate(2048)
    pub_key_base64 = base64.b64encode(key.publickey().export_key(format='DER')).decode()
    pri_key_base64 = base64.b64encode(key.export_key(format='DER', pkcs=8)).decode()

    print(f"public key: {pub_key_base64}")
    print(f"private key: {pri_key_base64}")

    sign = TopPaySignUtil.sign(pri_key_base64, source)
    print(f"sign: {sign}")

    verify = TopPaySignUtil.verify(pub_key_base64, source, sign)
    print(f"verify: {verify}")

  @staticmethod
  def do_post(url, post_json):
    # post_json is a JSON string from json.dumps(), same as Java TopPaySignUtil.doPost
    # Do not use requests.post(url, json=post_json) — it double-serializes the string
    response = requests.post(
      url,
      data=post_json.encode('utf-8'),
      headers={'Content-Type': 'application/json'},
    )
    if response.status_code == 200:
      return response.text
    return None

  @staticmethod
  def param_format(param):
    sorted_keys = sorted(param.keys())
    pairs = []
    for key in sorted_keys:
      value = param.get(key)
      if value is not None and str(value).strip():
        pairs.append(f"{key}={value}")
    return "&".join(pairs)

  @staticmethod
  def sign(pri_key_base64, source):
    private_key = TopPaySignUtil.string_to_private_key(pri_key_base64)
    return TopPaySignUtil.encrypt(source, private_key)

  @staticmethod
  def verify(pub_key_base64, source, sign):
    public_key = TopPaySignUtil.string_to_public_key(pub_key_base64)
    return source == TopPaySignUtil.decrypt(sign, public_key)

  @staticmethod
  def encrypt(plain_data, private_key):
    plain_bytes = plain_data.encode('utf-8')
    encrypted_data = b''
    for i in range(0, len(plain_bytes), TopPaySignUtil.MAX_ENCRYPT_BLOCK):
      chunk = plain_bytes[i:i + TopPaySignUtil.MAX_ENCRYPT_BLOCK]
      encrypted_data += TopPaySignUtil._rsa_private_encrypt(private_key, chunk)
    return base64.b64encode(encrypted_data).decode()

  @staticmethod
  def decrypt(encrypted_data, public_key):
    encrypted_bytes = base64.b64decode(encrypted_data)
    block_size = public_key.size_in_bytes()
    decrypted_data = b''
    for i in range(0, len(encrypted_bytes), block_size):
      chunk = encrypted_bytes[i:i + block_size]
      decrypted_data += TopPaySignUtil._rsa_public_decrypt(public_key, chunk)
    return decrypted_data.decode('utf-8')

  @staticmethod
  def string_to_public_key(public_key_str):
    return RSA.import_key(base64.b64decode(public_key_str))

  @staticmethod
  def string_to_private_key(private_key_str):
    return RSA.import_key(base64.b64decode(private_key_str))

  @staticmethod
  def _pkcs1_pad_type1(data, block_size):
    padding_len = block_size - len(data) - 3
    if padding_len < 8:
      raise ValueError('data too large for RSA block')
    return b'\x00\x01' + (b'\xff' * padding_len) + b'\x00' + data

  @staticmethod
  def _pkcs1_unpad_type1(em):
    if len(em) < 11 or em[0] != 0x00 or em[1] != 0x01:
      raise ValueError('invalid padding header')
    i = 2
    while i < len(em) and em[i] != 0x00:
      if em[i] != 0xFF:
        raise ValueError('invalid padding')
      i += 1
    if i >= len(em):
      raise ValueError('padding end not found')
    return em[i + 1:]

  @staticmethod
  def _rsa_private_encrypt(private_key, data):
    k = private_key.size_in_bytes()
    em = TopPaySignUtil._pkcs1_pad_type1(data, k)
    m = bytes_to_long(em)
    c = pow(m, private_key.d, private_key.n)
    return long_to_bytes(c, k)

  @staticmethod
  def _rsa_public_decrypt(public_key, cipher_block):
    k = public_key.size_in_bytes()
    c = bytes_to_long(cipher_block)
    m = pow(c, public_key.e, public_key.n)
    em = long_to_bytes(m, k)
    return TopPaySignUtil._pkcs1_unpad_type1(em)


if __name__ == "__main__":
  TopPaySignUtil.main()

/**
 * Fully compatible Node.js version of Java TopPaySignUtil
 * Principle: RSA private key encryption (NOT SHA256withRSA)
 */

const crypto = require('crypto');
const fs = require('fs');
const https = require('https');

class TopPaySignUtil {
    static MAX_ENCRYPT_BLOCK = 245;
    static MAX_DECRYPT_BLOCK = 256;
    static main() {
        const params = {
            merchantCode: 'S820190712000002',
            orderAmount: '999.56',
            orderNum: 'T1231511321515',
            callback: 'https://xxx/yyy',
            timestamp: Math.floor(Date.now() / 1000), // Unix timestamp in seconds
        };

        const source = this.paramFormat(params);
        console.log(`source: ${source}`);

        // === Generate RSA key pair compatible with Java ===
        const {publicKey, privateKey} = crypto.generateKeyPairSync('rsa', {
            modulusLength: 2048,
            publicKeyEncoding: {
                type: 'spki',   // Default Java public key format
                format: 'pem'
            },
            privateKeyEncoding: {
                type: 'pkcs8',  // Default Java private key format
                format: 'pem'
            }
        });

        const pubKeyPem = publicKey;
        const priKeyPem = privateKey;

        console.log(`public key (SPKI):\n${pubKeyPem}`);
        console.log(`private key (PKCS8):\n${priKeyPem}`);

        // === Sign ===
        const sign = this.sign(priKeyPem, source);
        console.log(`sign: ${sign}`);

        // === Verify ===
        const verify = this.verify(pubKeyPem, source, sign);
        console.log(`verify: ${verify}`);
    }

    // ========== Network POST request function ==========
    static doPost(url, json) {
        return new Promise((resolve, reject) => {
            const options = {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Content-Length': Buffer.byteLength(json)
                }
            };

            const req = https.request(url, options, res => {
                let data = '';
                res.on('data', chunk => data += chunk);
                res.on('end', () => {
                    if (res.statusCode === 200) {
                        resolve(data);
                    } else {
                        reject(new Error(`Request failed with status code ${res.statusCode}`));
                    }
                });
            });

            req.on('error', error => reject(error));
            req.write(json);
            req.end();
        });
    }

    // ========== Parameter formatting ==========
    static paramFormat(param) {
        const sortedKeys = Object.keys(param).sort();
        const pairs = [];
        for (const key of sortedKeys) {
            const value = param[key];
            if (value !== null && value !== undefined && value.toString().trim() !== '') {
                pairs.push(`${key}=${value}`);
            }
        }
        return pairs.join('&');
    }

    // ========== Sign (RSA private key encryption) ==========
    static sign(privateKeyPem, source) {
        return this.encrypt(source, this.stringToPrivateKey(privateKeyPem));
    }

    // ========== Verify (RSA public key decryption) ==========
    static verify(publicKeyPem, source, sign) {
        const decrypted = this.decrypt(sign, this.stringToPublicKey(publicKeyPem));
        return source === decrypted;
    }

    // ========== RSA private key encryption (equivalent to Java encrypt()) ==========
    static encrypt(plainData, privateKey) {
        const buffer = Buffer.from(plainData, 'utf8');
        const chunks = [];
        for (let offset = 0; offset < buffer.length; offset += this.MAX_ENCRYPT_BLOCK) {
            const chunk = buffer.slice(offset, offset + this.MAX_ENCRYPT_BLOCK);
            const encrypted = crypto.privateEncrypt(
                {key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING},
                chunk
            );
            chunks.push(encrypted);
        }
        return Buffer.concat(chunks).toString('base64');
    }

    // ========== RSA public key decryption (equivalent to Java decrypt()) ==========
    static decrypt(base64Data, publicKey) {
        const buffer = Buffer.from(base64Data, 'base64');
        const chunks = [];
        for (let offset = 0; offset < buffer.length; offset += this.MAX_DECRYPT_BLOCK) {
            const chunk = buffer.slice(offset, offset + this.MAX_DECRYPT_BLOCK);
            const decrypted = crypto.publicDecrypt(
                {key: publicKey, padding: crypto.constants.RSA_PKCS1_PADDING},
                chunk
            );
            chunks.push(decrypted);
        }
        return Buffer.concat(chunks).toString('utf8');
    }

    // ========== Convert PEM string to public/private key ==========
    static stringToPublicKey(publicKeyStr) {
        return crypto.createPublicKey(publicKeyStr);
    }

    static stringToPrivateKey(privateKeyStr) {
        return crypto.createPrivateKey(privateKeyStr);
    }
}
//todo RSA生成
// TopPaySignUtil.main();


//========== Do you prefer procedural programming!???? =============================



// Generate RSA key pair
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
    modulusLength: 2048, // Key length (Java commonly uses 2048)
    publicKeyEncoding: {
        type: 'spki', // Public key format (default in Java)
        format: 'pem', // Output in PEM format
    },
    privateKeyEncoding: {
        type: 'pkcs8', // Private key format (default in Java)
        format: 'pem', // Output in PEM format
    },
});

// Output to console
console.log('-----BEGIN PUBLIC KEY (SPKI, Java compatible) -----\n');
console.log(publicKey);
console.log('-----BEGIN PRIVATE KEY (PKCS8, Java compatible) -----\n');
console.log(privateKey);

// Optional: save to files
fs.writeFileSync('private_key_pkcs8.pem', privateKey);
console.log('\n✅ Generated and saved as:');
console.log('  private_key_pkcs8.pem');
fs.writeFileSync('public_key_spki.pem', publicKey);
console.log('  public_key_spki.pem');

const params = {
    merchantCode: 'S820190712000002',
    orderNum: 'T16425931668144',
    orderAmount: '888',
    callback: 'https://xxx/yyy',
    timestamp: Math.floor(Date.now() / 1000), // Unix timestamp in seconds
};

// 1️⃣ Sort keys and concatenate as key=value&key=value
function getSortedQueryString(params) {
    return Object.keys(params)
        .filter(k => params[k] !== undefined && params[k] !== null && params[k] !== "")
        .sort()
        .map(key => `${key}=${params[key]}`)
        .join('&');
}

// 2️⃣ RSA private key encryption (equivalent to Java encrypt())
function rsaPrivateEncrypt(data, privateKeyPem) {
    const buffer = Buffer.from(data, 'utf8');
    const maxBlock = 245; // Same as MAX_ENCRYPT_BLOCK in Java
    const chunks = [];
    for (let offset = 0; offset < buffer.length; offset += maxBlock) {
        const chunk = buffer.slice(offset, offset + maxBlock);
        const encrypted = crypto.privateEncrypt(
            {
                key: privateKeyPem,
                padding: crypto.constants.RSA_PKCS1_PADDING,
            },
            chunk
        );
        chunks.push(encrypted);
    }
    return Buffer.concat(chunks).toString('base64');
}

// 3️⃣ RSA public key decryption (equivalent to Java decrypt())
function rsaPublicDecrypt(base64Data, publicKeyPem) {
    const buffer = Buffer.from(base64Data, 'base64');
    const maxBlock = 256;
    const chunks = [];
    for (let offset = 0; offset < buffer.length; offset += maxBlock) {
        const chunk = buffer.slice(offset, offset + maxBlock);
        const decrypted = crypto.publicDecrypt(
            {
                key: publicKeyPem,
                padding: crypto.constants.RSA_PKCS1_PADDING,
            },
            chunk
        );
        chunks.push(decrypted);
    }
    return Buffer.concat(chunks).toString('utf8');
}

const sourceData = getSortedQueryString(params);
console.log('String to be signed:', sourceData);

// Generate Java-equivalent signature (RSA private key encryption)
const sign = rsaPrivateEncrypt(sourceData, privateKey);
console.log('Generated signature (Base64):', sign);

// Verify signature consistency
const verifyResult = rsaPublicDecrypt(sign, publicKey);
console.log('Is local verification consistent:', verifyResult === sourceData);