目录

Overview

HTTPS Security Requirements

HTTP Method Description

Currently, the external APIs of Payment Kit involve two HTTP methods: GET and POST.

Data format

Requests and responses are in JSON format.

Character Encoding

By default, all requests of Payment Kit support UTF-8 encoding. If other encoding formats are used, problems such as signature verification failure and field parsing failure may occur.

For an API request whose HTTP method is GET, if the request URL involves parameter concatenation, it is recommended that the complete request URL after concatenation not contain special characters. If necessary, you are advised to encode special characters in the URL. Special characters in URLs include but are not limited to the following: plus sign (+), space, question mark (?), percent sign (%), number sign (#), ampersand (&), and equal sign (=).

Authentication and Authorization

Signing Rules

Signing scenario: parameter signature in the header of a request from the merchant cloud.

Signature algorithm: SHA256WithRSA/PSS or SM2.

Development procedure

  1. Filtering: Exclude the sign field from the JSON string in the request and retain only other fields.

  2. Sorting and concatenation: Sort the fields by ASCII code of key values and use & to concatenate them. In list scenarios, use the equal sign (=) to concatenate them.

    For details, see the signString method of the SignStringUtil.java class in Maven dependency (pay-java) provided by Huawei Pay in the sample code. The following is an example of the sorting and concatenation result:

    "allocationType=DELAY_ORDER_ALLOCATION&callbackUrl=https://www.xxxxxx.com/hw/pay/callback&currency=CNY&mercNo=xxxxxx&mercOrderNo=xxxxxx&totalAmount=2&tradeSummary=Magazine and newspaper"
  3. Signing: Call the given algorithm to sign the sorted string.

    SHA256WithRSA/PSS: For details, see the signByRSAWithPSS method of the RSAUtils.java class in Maven dependency (pay-java) provided by Huawei Pay in the sample code.

    SM2: For details, see the sign method of the Sm2Utils.java class in Maven dependency (pay-java).

    Reference for the signed content:

    POST request: POST https://petalpay-developer.cloud.huawei.com.cn/api/v2/aggr/preorder/create/app
    {
        "appId": "5765880207854262xxx",
        "callbackUrl": "https://www.xxx.com/hw_notify",
        "currency": "CNY",
        "mercNo": "101723200xxx",
        "mercOrderNo": "2405171027547xxx",
        "payload": "shop|h8ahnJA8xxRXVw",
        "totalAmount": 5,
        "tradeSummary": "test",
        "subOrders": [
          {
            "mercNo": "101723200xxx",
            "mercOrderNo": "abcxxx1"
          },
          {
            "mercNo": "101723201xxx",
            "mercOrderNo": "abcxxx2"
          }
        ]
    }
    Signed content: appId=5765880207854262xxx&callbackUrl=https://www.xxx.com/hw_notify&currency=CNY&mercNo=101723200xxx&mercOrderNo=2405171027547xxx&payload=shop|h8ahnJA8xxRXVw&subOrders=mercNo=101723200xxx&mercOrderNo=abcxxx1,mercNo=101723201xxx&mercOrderNo=abcxxx2&totalAmount=5&tradeSummary=test
    
    GET request: GET https://petalpay-developer.cloud.huawei.com.cn/api/v2/aggr/transactions/merc-orders/20240507000000041809599950090xxx?mercNo=101540000089&sdkVersion=1.0.0
    Signed content: /api/v2/aggr/transactions/merc-orders/20240507000000041809599950090xxx?mercNo=101540000089&sdkVersion=1.0.0

If a field is set to null, the field cannot be signed. If a field is set to an empty string, the field must be signed.

[h2]Sample Code for Signing with SHA256WithRSA/PSS in Common Languages

Go

Sample code for signing:

// Example for sorting and concatenation
import (
    "encoding/base64"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "sort"
    "strings"
)

// Generate a string to be signed using GenerateSignData.
// Step 1: Exclude the sign field.
// Step 2: Sort the fields by ASCII code of key values and use & to concatenate them.
func (s *SignatureUtils) GenerateSignData(params map[string]interface{}) string {
    return s.buildSignData(params)
}
// Recursively build a to-be-signed string using buildSignData.
func (s *SignatureUtils) buildSignData(params interface{}) string {
    switch v := params.(type) {
    case map[string]interface{}:
        return s.buildMapSignData(v)
    case []interface{}:
        return s.buildArraySignData(v)
    default:
        return fmt.Sprintf("%v", v)
    }
}
// Use buildMapSignData to process data of the Map type.
func (s *SignatureUtils) buildMapSignData(params map[string]interface{}) string {
    // Sort keys.
    var keys []string
    for k := range params {
        // Exclude the sign field.
        if k == "sign" {
            continue
        }
        keys = append(keys, k)
    }
    sort.Strings(keys)
    var parts []string
    for _, key := range keys {
        value := params[key]
        // The null value is not involved in signing.
        if value == nil {
            continue
        }
        // The empty string is involved in signing.
        strValue := s.buildSignData(value)
        parts = append(parts, fmt.Sprintf("%s=%s", key, strValue))
    }
    return strings.Join(parts, "&")
}
// Use buildArraySignData to process data of the array type.
func (s *SignatureUtils) buildArraySignData(arr []interface{}) string {
    var parts []string
    for _, item := range arr {
        if item == nil {
            continue
        }
        parts = append(parts, s.buildSignData(item))
    }
    return strings.Join(parts, ",")
}

// Signing example
package main
import (
  "crypto"
  "crypto/rand"
  "crypto/rsa"
  "crypto/sha256"
  "crypto/x509"
  "encoding/base64"
  "fmt"
  "testing"
)
func signSha256WithRsaPSS(message, privateKeyStr string) (string, error) {
  privateKey, err := getPrivateKey(privateKeyStr)
  if err != nil {
    return "", err
  }
  hasher := sha256.New()
  hasher.Write([]byte(message))
  hashedMessage := hasher.Sum(nil)
  opts := rsa.PSSOptions{
    SaltLength: rsa.PSSSaltLengthEqualsHash,
    Hash:       crypto.SHA256,
  }
  signature, err := rsa.SignPSS(rand.Reader, privateKey, crypto.SHA256, hashedMessage, &opts)
  if err != nil {
    return "", err
  }
  return base64.StdEncoding.EncodeToString(signature), nil
}

func getPrivateKey(privateKeyStr string) (*rsa.PrivateKey, error) {
  decoded, err := base64.StdEncoding.DecodeString(privateKeyStr)
  if err != nil {
    return nil, err
  }
  privateKey, err := x509.ParsePKCS8PrivateKey(decoded)
  if err != nil {
    return nil, err
  }
  rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey)
  if !ok {
  return nil, err
  }
  return rsaPrivateKey, nil
}

func TestRsaSign(t *testing.T) {
  signature, err := signSha256WithRsaPSS("Sign the sorted and concatenated content","Private key")
  fmt.Println(signature, err)
}

JavaScript

JavaScript implements signing using the jsrsasign module. If the Node .js environment has been installed, run the following command to install the jsrsasign module:

npm install jsrsasign

Sample code for signing:

// Example for sorting and concatenation
/**
 * Recursively build a to-be-signed string.
 * @param {*} data Pointer to the data.
 * @returns {string} String to be signed.
 */
buildSignData(data) {
    if (data === null || data === undefined) {
        return '';
    }
    if (typeof data === 'object' && !Array.isArray(data)) {
        return this.buildMapSignData(data);
    }
    if (Array.isArray(data)) {
        return this.buildArraySignData(data);
    }
    return String(data);
}
/**
 * Process data of the Map type.
 * @param {Object} params Parameter object.
 * @returns {string} String to be signed.
 */
buildMapSignData(params) {
    // Sort keys.
    const keys = Object.keys(params)
        .filter(key => key !== 'sign') // Exclude the sign field.
        .sort();
    const parts = [];
    for (const key of keys) {
        const value = params[key];
        // The null value is not involved in signing.
        if (value === null || value === undefined) {
            continue;
        }
        // The empty string is involved in signing.
        const strValue = this.buildSignData(value);
        parts.push(`${key}=${strValue}`);
    }
    return parts.join('&');
}
/**
 * Process data of the array type.
 * @param {Array} arr Array.
 * @returns {string} String to be signed.
 */
buildArraySignData(arr) {
    const parts = [];
    for (const item of arr) {
        if (item === null || item === undefined) {
            continue;
        }
        parts.push(this.buildSignData(item));
    }
    return parts.join(',');
}

// Signing example
const jsrsasign = require('jsrsasign');
function signSha256WithRsaPSS(plainData, privateStr) {
  let rsa = new jsrsasign.RSAKey();
  // Private key in PEM format.
  const priKey = `-----BEGIN PRIVATE KEY-----\n${privateStr}\n-----END PRIVATE KEY-----\n`;
  rsa = jsrsasign.KEYUTIL.getKey(priKey);
  const sig = new jsrsasign.KJUR.crypto.Signature({
    alg: 'SHA256withRSAandMGF1',
  });
  sig.init(rsa);
  sig.updateString(plainData);
  return jsrsasign.hextob64(sig.sign());
}
let signature = signSha256WithRsaPSS("Sign the sorted and concatenated content","Private key")
console.info(signature)

Python

Python implements signing using the cryptography module. You can run the following command to install the cryptography module:

pip install cryptography

Sample code for signing:

# Example for sorting and concatenation
def generate_sign_data(self, params):
    """
    Generate a string to be signed.
    Step 1: Exclude the sign field.
    Step 2: Sort the fields by ASCII code of key values and use & to concatenate them.
    Args:
        params: Parameter dictionary.
    Returns:
        str: String to be signed.
    """
    return self._build_sign_data(params)
def _build_sign_data(self, data):
    """
    Recursively build a to-be-signed string.
    Args:
        data: Data.
    Returns:
        str: String to be signed.
    """
    if data is None:
        return ''
    if isinstance(data, dict):
        return self._build_map_sign_data(data)
    if isinstance(data, (list, tuple)):
        return self._build_array_sign_data(data)
    return str(data)
def _build_map_sign_data(self, params):
    """
    Process data of the dictionary type.
    Args:
        params: Parameter dictionary.
    Returns:
        str: String to be signed.
    """
    # Sort the keys and exclude the sign field.
    keys = sorted([k for k in params.keys() if k != 'sign'])
    parts = []
    for key in keys:
        value = params[key]
        # The None value is not involved in signing.
        if value is None:
            continue
        # The empty string is involved in signing.
        str_value = self._build_sign_data(value)
        parts.append(f"{key}={str_value}")
    return '&'.join(parts)
def _build_array_sign_data(self, arr):
    """
    Process data of the array type.
    Args:
        arr: Array.
    Returns:
        str: String to be signed.
    """
    parts = []
    for item in arr:
        if item is None:
            continue
        parts.append(self._build_sign_data(item))
    return ','.join(parts)

# Signing example
import base64

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key

def signSha256WithRsaPSS(private_key_str, message):
    # Private key in PEM format.
    prikey_content = "-----BEGIN PRIVATE KEY-----\n" + private_key_str +"\n-----END PRIVATE KEY-----\n";
    private_key = load_pem_private_key(data=prikey_content.encode("utf-8"), password=None, backend=default_backend())

    # Use the RSA-PSS algorithm to generate a signature.
    signature = private_key.sign(
        message.encode("utf-8"),
        padding.PSS(
            mgf=padding.MGF1(hashes.SHA256()),
            salt_length=padding.PSS.DIGEST_LENGTH
        ),
        hashes.SHA256()
    )
    return base64.b64encode(signature).decode('utf-8')

message = "Sign the sorted and concatenated content"
private_key_str = "Private key"
signature = signSha256WithRsaPSS(private_key_str, message)
print(signature)

PHP

PHP implements signing using the phpseclib encryption library. If the composer environment has been installed, you can run the following command to import phpseclib:

composer require phpseclib/phpseclib

Sample code for signing:

// Example for sorting and concatenation
class SignatureUtils
{
    /**
     * Generate a string to be signed.
     * Step 1: Exclude the sign field.
     * Step 2: Sort the fields by ASCII code of key values and use & to concatenate them.
     *
     * @param array $params Parameter array.
     * @return string String to be signed.
     */
    public function generateSignData($params)
    {
        return $this->buildSignData($params);
    }
    /**
     * Recursively build a to-be-signed string.
     *
     * @param mixed $data Data.
     * @return string String to be signed.
     */
    private function buildSignData($data)
    {
        if ($data === null) {
            return '';
        }
        if (is_array($data)) {
            // Check whether the array is an associative array (Map).
            if ($this->isAssocArray($data)) {
                return $this->buildMapSignData($data);
            } else {
                return $this->buildArraySignData($data);
            }
        }
        return (string)$data;
    }
    /**
     * Check whether the array is an associative array.
     *
     * @param array $arr Array.
     * @return bool
     */
    private function isAssocArray($arr)
    {
        if (empty($arr)) {
            return false;
        }
        return array_keys($arr) !== range(0, count($arr) - 1);
    }
    /**
     * Process data of the Map type.
     *
     * @param array $params Parameter array.
     * @return string String to be signed.
     */
    private function buildMapSignData($params)
    {
        // Sort the keys and exclude the sign field.
        $keys = array_keys($params);
        $keys = array_filter($keys, function($key) {
            return $key !== 'sign';
        });
        sort($keys);
        $parts = [];
        foreach ($keys as $key) {
            $value = $params[$key];
            // The null value is not involved in signing.
            if ($value === null) {
                continue;
            }
            // The empty string is involved in signing.
            $strValue = $this->buildSignData($value);
            $parts[] = $key . '=' . $strValue;
        }
        return implode('&', $parts);
    }
    /**
     * Process data of the array type.
     *
     * @param array $arr Array.
     * @return string String to be signed.
     */
    private function buildArraySignData($arr)
    {
        $parts = [];
        foreach ($arr as $item) {
            if ($item === null) {
                continue;
            }
            $parts[] = $this->buildSignData($item);
        }
        return implode(',', $parts);
    }
    /**
     * Convert Base64 to Hex.
     *
     * @param string $base64String Base64 string.
     * @return string Hexadecimal string.
     */
    private function base64ToHex($base64String)
    {
        $bytes = base64_decode($base64String);
        return bin2hex($bytes);
    }
    /**
     * Parse a JSON string as an array.
     *
     * @param string $jsonStr JSON string.
     * @return array Array.
     */
    public function parseJSON($jsonStr)
    {
        return json_decode($jsonStr, true);
    }
    /**
     * Generate a string to be signed from the JSON string.
     *
     * @param string $jsonStr JSON string.
     * @return string String to be signed.
     */
    public function generateSignDataFromJSON($jsonStr)
    {
        $params = $this->parseJSON($jsonStr);
        return $this->generateSignData($params);
    }
    public function testExcludeSignField()
    {
        $params = [
            'appId' => '5765880207854262xxx',
            'callbackUrl' => 'https://www.xxx.com/hw_notify',
            'sign' => 'xxxxx', // The sign field should be excluded.
            'currency' => 'CNY'
        ];
        $signData = $this-> generateSignData($params);
        echo "[testExcludeSignField] Sign data without sign field: " . $signData . "\n";
    }
}

// Signing example
<?php
require_once 'vendor/autoload.php';
use phpseclib\Crypt\RSA;

function signSha256WithRsaPSS($content, $privateKeyStr) {
    try {
        $privateKey = "-----BEGIN PRIVATE KEY-----\n" . $privateKeyStr ."\n-----END PRIVATE KEY-----";
        $signer = new \phpseclib\Crypt\RSA();
        $signer->loadKey($privateKey, $password = false);
        $signer->setHash('sha256');
        $signer->setSignatureMode(RSA::SIGNATURE_PSS);
        $signer->setMGFHash('sha256');
        $signature = $signer->sign($content);

        $sign = base64_encode($signature);
        return $sign;
    } catch (Exception $e) {
        echo $e->getMessage();
    }
    return null;
}
$data = "Sign the sorted and concatenated content";
$privateKeyStr = "Private key";
echo signSha256WithRsaPSS($data, $privateKeyStr)
?>

Sample code for signing:

<?php
require_once 'vendor/autoload.php';

use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Crypt\RSA;

function signSha256WithRsaPSS($data,$rsa_key){
    $key = "-----BEGIN PRIVATE KEY-----\n" . $rsa_key . "-----END PRIVATE KEY-----";
    $private = PublicKeyLoader::load($key, $password = false);
    $private = $private->withPadding(RSA::SIGNATURE_PSS);
    return base64_encode($private->sign($data));
}

$data = "Sign the sorted and concatenated content";
$privateKeyStr = "Private key";
echo signSha256WithRsaPSS($data, $privateKeyStr);
?>

C#

C# can implement signing using the BouncyCastle library. If you have installed .NET SDK, run the following command to add BouncyCastle:

dotnet add package BouncyCastle

Sample code for signing:

// Example for sorting and concatenation
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Linq;
namespace PaySignature
{
    /// <summary>
    /// Signature utilities.
    /// </summary>
    public class SignatureUtils
    {
        /// <summary>
        /// Generate a string to be signed.
        /// Step 1: Exclude the sign field.
        /// Step 2: Sort the fields by ASCII code of key values and use & to concatenate them.
        /// </summary>
        /// <param name="params">Parameter dictionary</param>
        /// <returns>String to be signed</returns>
        public string GenerateSignData(Dictionary<string, object> parameters)
        {
            return BuildSignData(parameters);
        }
        /// <summary>
        /// Recursively build a to-be-signed string.
        /// </summary>
        /// <param name="data">Data</param>
        /// <returns>String to be signed</returns>
        private string BuildSignData(object data)
        {
            if (data == null)
            {
                return string.Empty;
            }
            if (data is Dictionary<string, object> dict)
            {
                return BuildMapSignData(dict);
            }
            if (data is List<object> list)
            {
                return BuildArraySignData(list);
            }
            return data.ToString();
        }
        /// <summary>
        /// Process data of the dictionary type.
        /// </summary>
        /// <param name="parameters">Parameter dictionary</param>
        /// <returns>String to be signed</returns>
        private string BuildMapSignData(Dictionary<string, object> parameters)
        {
            // Sort the keys and exclude the sign field.
            var keys = parameters.Keys
                .Where(k => k != "sign")
                .OrderBy(k => k, StringComparer.Ordinal)
                .ToList();
            var parts = new List<string>();
            foreach (var key in keys)
            {
                var value = parameters[key];
                // The null value is not involved in signing.
                if (value == null)
                {
                    continue;
                }
                // The empty string is involved in signing.
                var strValue = BuildSignData(value);
                parts.Add($"{key}={strValue}");
            }
            return string.Join("&", parts);
        }
        /// <summary>
        /// Process data of the array type.
        /// </summary>
        /// <param name="arr">Array</param>
        /// <returns>String to be signed</returns>
        private string BuildArraySignData(List<object> arr)
        {
            var parts = new List<string>();
            foreach (var item in arr)
            {
                if (item == null)
                {
                    continue;
                }
                parts.Add(BuildSignData(item));
            }
            return string.Join(",", parts);
        }
        /// <summary>
        /// Convert Base64 to Hex.
        /// </summary>
        /// <param name="base64String">Base64 string</param>
        /// <returns>Hexadecimal string</returns>
        private string Base64ToHex(string base64String)
        {
            var bytes = Convert.FromBase64String(base64String);
            return BitConverter.ToString(bytes).Replace("-", "").ToLower();
        }
        /// <summary>
        /// Parse a JSON string as a dictionary.
        /// </summary>
        /// <param name="jsonStr">JSON string</param>
        /// <returns>Dictionary</returns>
        public Dictionary<string, object> ParseJSON(string jsonStr)
        {
            using var document = JsonDocument.Parse(jsonStr);
            var result = new Dictionary<string, object>();
            foreach (var element in document.RootElement.EnumerateObject())
            {
                result[element.Name] = ParseJsonElement(element.Value);
            }
            return result;
        }
        /// <summary>
        /// Parse a JSON element.
        /// </summary>
        /// <param name="element">JSON element</param>
        /// <returns>Object</returns>
        private object ParseJsonElement(JsonElement element)
        {
            switch (element.ValueKind)
            {
                case JsonValueKind.String:
                    return element.GetString();
                case JsonValueKind.Number:
                    if (element.TryGetInt32(out int intValue))
                        return intValue;
                    if (element.TryGetInt64(out long longValue))
                        return longValue;
                    return element.GetDouble();
                case JsonValueKind.True:
                    return true;
                case JsonValueKind.False:
                    return false;
                case JsonValueKind.Null:
                    return null;
                case JsonValueKind.Object:
                    var dict = new Dictionary<string, object>();
                    foreach (var prop in element.EnumerateObject())
                    {
                        dict[prop.Name] = ParseJsonElement(prop.Value);
                    }
                    return dict;
                case JsonValueKind.Array:
                    var list = new List<object>();
                    foreach (var item in element.EnumerateArray())
                    {
                        list.Add(ParseJsonElement(item));
                    }
                    return list;
                default:
                    return element.ToString();
            }
        }
        /// <summary>
        /// Generate a string to be signed from the JSON string.
        /// </summary>
        /// <param name="jsonStr">JSON string</param>
        /// <returns>String to be signed</returns>
        public string GenerateSignDataFromJSON(string jsonStr)
        {
            var parameters = ParseJSON(jsonStr);
            return GenerateSignData(parameters);
        }
    }
}
// Test.
using Xunit;
using PaySignature;
namespace PaySignatureTest
{
    public class SignatureUtilsTest2
    {
        private readonly SignatureUtils _utils;
        public SignatureUtilsTest2()
        {
            _utils = new SignatureUtils();
        }
        /// <summary>
        /// Test the construction of a to-be-signed string.
        /// </summary>
        [Fact]
        public void TestComplexNestedStructure()
        {
            var goods = new List<object>
            {
                new Dictionary<string, object>
                {
                    { "goodsId", "G001" },
                    { "goodsName", "Product 1" },
                    { "quantity", 2 }
                },
                new Dictionary<string, object>
                {
                    { "goodsId", "G002" },
                    { "goodsName", "Product 2" },
                    { "quantity", 3 }
                }
            };
            var parameters = new Dictionary<string, object>
            {
                { "appId", "5765880207854262xxx" },
                { "goods", goods },
                { "totalAmount", 100 }
            };
            var signData = _utils.GenerateSignData(parameters);
            Assert.Contains("appId=5765880207854262xxx", signData);
            Assert.Contains("goods=", signData);
            Console.WriteLine($"Sign data with complex structure: {signData}");
        }
    }
}

// Signing example
using System.Text;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Digests;
class SignProgram
{
    public static void Main(string[] args)
    {
        string res = RSASign("Sign the sorted and concatenated content","Private key")
        Console.WriteLine("result: " + res);
    }
    public static string RSASign(string datastr,string privateStr)
    {
        var priKey = "-----BEGIN PRIVATE KEY-----\n"+ privateStr +"\n-----END PRIVATE KEY-----\n";
        byte[] data = Encoding.UTF8.GetBytes(datastr);
        // Create a PSS signer.
        var signer = new PssSigner(new RsaEngine(), new Sha256Digest());
        AsymmetricKeyParameter rsaPrivateKey = null;
        using (var stringReader = new StringReader(priKey))
        {
            var pemReader = new PemReader(stringReader);
            rsaPrivateKey = pemReader.ReadObject() as AsymmetricKeyParameter;
        }
        signer.Init(true, new ParametersWithRandom(rsaPrivateKey, new SecureRandom()));
        signer.BlockUpdate(data, 0, data.Length);
        byte[] signature = signer.GenerateSignature();
        return Convert.ToBase64String(signature);
    }
}

Signature Verification Rules

Signature verification scenario: The merchant needs to perform signature verification on the information from the Payment Kit server to verify that the signature information comes from the Payment Kit server. The merchant server performs signature verification on the callback notification request parameters and refund notification request parameters returned by the Payment Kit server.

Signature verification algorithm: SM2

Development procedure: Confirm the public key certificate for signature verification, confirm that the fields to be signed are converted into the "key=value" format and combined &, and use the signature verification method to verify the signature.

  1. Obtain the Huawei Pay public key. The public key certificate is the one obtained from Huawei Pay merchant platform. For details, see Downloading the Huawei Pay Certificate.

  2. Obtain the fields involved in signing, convert them into a map, sort the key-value pairs in the map, and concatenate them using &.

  3. Use the SM2 algorithm for signature verification. For details, see the verifySign method of the VerifyTools.java class in the Maven dependency (pay-java) provided by Huawei Pay in the sample code.

Due to business development needs, API fields may change. It is recommended that the signature verification function be implemented in an extensible manner. That is, perform signature verification on the request body of the callback notification and then convert it into a business object for processing. This ensures that signature verification can still be performed properly when fields change.

[h2]Sample Code for Signature Verification with SM2 in Common Languages

JavaScript

JavaScript implements signature verification using the sm-crypto module. If the Node .js environment has been installed, run the following command to install the sm-crypto module:

npm install sm-crypto

Sample code for signature verification:

const SmCrypto = require('sm-crypto');
function signSM2Verify(plainData, sign, publicStr) {
  let signHex1 = base64ToHex(sign)
  console.info("plainData: " + plainData)
  console.info("sign: " + sign)
  console.info("sign hex: " + signHex1)
  return SmCrypto.sm2.doVerifySignature(plainData, signHex1, publicStr, {
    hash: true,
    der: true
  })
}
function base64ToHex(base64String) {
  const buffer = Buffer.from(base64String, 'base64');
  return buffer.toString('hex');
}
let signSM2VerifyRes = signSM2Verify(
    "Perform signature verification on the sorted and concatenated content",
    "Signature verification"
    "Public key for signature verification"
);
console.info(signSM2VerifyRes)

Go

Sample code for signature verification:

// TestSM2Verify test: SM2-based signature verification (install go get -u github.com/tjfoc/gmsm)
func ReadSM2PublicKeyFromHex(publicKeyStr string) (*sm2.PublicKey, error) {
    // Decode the hexadecimal string.
    pubKeyBytes, err := hex.DecodeString(publicKeyStr)
    if err != nil {
        return nil, fmt.Errorf("failed to decode hex: %v", err)
    }
    // Check whether the format is uncompressed (starting with 0x04).
    if len(pubKeyBytes) != 65 || pubKeyBytes[0] != 0x04 {
        return nil, fmt.Errorf("invalid SM2 public key format (expected 65 bytes, starting with 0x04)")
    }
    // Parse the X and Y coordinates. (The length of the SM2 public key is 65 bytes: 0x04 + 32-byte X + 32-byte Y.)
    x := pubKeyBytes[1:33] // X coordinate (32 bytes)
    y := pubKeyBytes[33:65] // Y coordinate (32 bytes)
    // Construct an SM2 public key.
    publicKey := &sm2.PublicKey{
        Curve: sm2.P256Sm2(), // SM2 curve.
            X:     new(big.Int).SetBytes(x),
            Y:     new(big.Int).SetBytes(y),
    }
    return publicKey, nil
}
func (s *SignatureUtils) VerifySM2(content, publicKeyStr, signData string) (bool, error) {
    // Convert the Base64 signature to the hexadecimal format.
    signBytes, err := base64.StdEncoding.DecodeString(signData)
    if err != nil {
        return false, fmt.Errorf("decode sign data failed: %v", err)
    }
    signHex := hex.EncodeToString(signBytes)
    // Parse the public key.
    publicKey, err := ReadSM2PublicKeyFromHex(publicKeyStr)
    if err != nil {
        return false, fmt.Errorf("parse public key failed: %v", err)
    }
    // Convert the hexadecimal signature to a byte array.
    signature, err := hex.DecodeString(signHex)
    if err != nil {
        return false, fmt.Errorf("decode signature hex failed: %v", err)
    }
    // SM2-based signature verification.
    isVerified := publicKey.Verify([]byte(content), signature)
    if !isVerified {
        return false, fmt.Errorf("SM2 verification requires external library, please install: go get github.com/tjfoc/gmsm")
    }
    return true, nil
}

Python

Sample code for signature verification:

import binascii
import json
import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives.serialization import load_der_private_key, load_der_public_key
from cryptography.hazmat.backends import default_backend
from pyasn1.codec.ber import decoder
class SignatureUtils:
    RS_LEN = 32
    def verify_sm2(self, content, public_key_str, sign_data):
        """
        SM2-based signature verification.
        Args:
            content: Original text.
            public_key_str: Public key (in hexadecimal format).
            sign_data: Signature (in Base64 format).
        Returns:
            bool: Signature verification result.
        Raises:
            Exception: The SM2 library needs to be imported.
        """
        try:
            from gmssl import sm2, func
            
            # Convert the Base64 signature to the hexadecimal format.
            sign_hex = self.base64_to_hex(sign_data)
            print(f'plainData: {content}')
            print(f'sign: {sign_data}')
            print(f'sign hex: {sign_hex}')
            # Create an SM2 instance.
            sm2_crypt = sm2.CryptSM2(
                public_key=self._preprocessing_public_key(public_key_str),
                private_key=None,
                asn1=True
            )
            # SM2-based signature verification.
            is_verified = sm2_crypt.verify_with_sm3(sign_hex, content.encode())
            
            return is_verified
        except ImportError:
            raise Exception("SM2 verification requires gmssl library, please install: pip install gmssl")
        except Exception as e:
            raise Exception(f"SM2 verification failed: {str(e)}")
    def is_asn1_format_key(self, key_hex_str: str, is_public_key: bool = False) -> bool:
        """
        Checks whether the key is in the ASN1 format.
        Args:
            key_hex_str: Hexadecimal key string.
            is_public_key: Whether the key is a public key.
        Returns:
            boolean
        """
        ken_len = len(binascii.unhexlify(key_hex_str))
        if is_public_key:
            return ken_len != self.RS_LEN * 2 and ken_len != self.RS_LEN * 2 + 1
        return ken_len != self.RS_LEN and ken_len != self.RS_LEN + 1
    @classmethod
    def covert_asn1_der_public_key(self, public_key: str) -> str:
        """
        Convert the DER-encoded public key in ASN.1 format of SM2 to the original unencoded key.
        Args:
            public_key: DER-encoded public key string in ASN.1 format.
        Returns:
            Original unencoded public key, which is directly represented as a 65-byte hexadecimal string.
        """
        asn1_obj, _ = decoder.decode(binascii.unhexlify(public_key))
        public_key_bytes = asn1_obj[1].asOctets()
        return public_key_bytes.hex()
    @classmethod
    def covert_asn1_der_private_key(self, private_key: str) -> str:
        """
        Convert the private key in ASN.1 format to the original unencoded key.
        Args:
            private_key: DER-encoded private key string in ASN.1 format.
        Returns:
            Private key in BCEC format.
        """
        asn1_obj, _ = decoder.decode(binascii.unhexlify(private_key))
        # Extract the large integer part of the private key. This part is specially adapted to the logic of Huawei Pay Java SDK. If the private key is in another format, this part may need to be adjusted.
        private_key_sequence = decoder.decode(asn1_obj[2].asOctets())[0]
        private_value_bytes = private_key_sequence[1].asOctets()
        return private_value_bytes[:32].hex()  # 32 bytes, which is the standard length in BCEC format.
    @classmethod
    def _preprocessing_public_key(self, public_key):
        """Public key preprocessing"""
        if self.is_asn1_format_key(self, public_key, is_public_key=True):
            public_key = self.covert_asn1_der_public_key(public_key)
        # Manually remove the prefix 04 to prevent the lstrip method from being used when CryptSM2 initialization detects that the public key starts with 04. If the original public key starts with 0, the initial 0 will be removed by mistake.
        if public_key.startswith('04'):
            public_key = public_key[2:]
        return public_key
# Test case
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
from signature import SignatureUtils
class TestSignatureUtils(unittest.TestCase):
    """Test on signature utilities"""
    def setUp(self):
        """Test preparations"""
        self.utils = SignatureUtils()
    def test_sm2_verify(self):
        """Test: SM2-based signature verification (install GMSSL)"""
        # SM2 test data
        plain_data = ''
        public_key_str = ''
        sign_data = '' # SM2-based signature in Base64 format. Replace it with the actual data.
        try:
            # SM2-based signature verification.
            verified = self.utils.verify_sm2(plain_data, public_key_str, sign_data)
            print(f'SM2 verification result: {verified}')
        except Exception as e:
            if 'gmssl' in str(e):
                self.skipTest('SM2 test skipped - gmssl not installed')
            else:
                raise
if __name__ == '__main__':
    unittest.main()

PHP

Sample code for signature verification:

# Use a third-party library. Run the installation command: composer require lpilp/guomi
/**
* Test: SM2-based signature verification (using the given sample data)
*/
public function testSM2Verify()
{
    $plainData = '';
    $publicKeyStr = '';
    $signData = "";
    try {
        // SM2-based signature verification.
        $verified = $this->sm2Verify($plainData, $signData, $publicKeyStr);
        $this->assertTrue($verified, "SM2 verification should succeed with given sample data");
    } catch (Exception $e) {
        $this->fail('SM2 verify failed: ' . $e->getMessage());
    }
}
public function sm2Verify(string $data, string $signature, string $publicKey): bool
{
    try {
        if (empty($data)) {
            throw new Exception('The data on which signature verification is performed cannot be empty.');
        }
        if (empty($signature)) {
            throw new Exception('The signature cannot be empty.');
        }
        if (empty($publicKey)) {
            throw new Exception('The public key cannot be empty.');
        }
        if (!class_exists('Rtgm\sm\RtSm2')) {
            throw new Exception('The Rtgm\sm\RtSm2 class does not exist. Check the composer dependency.');
        }
        $signatureHex = bin2hex(base64_decode($signature));
        $sm2 = new RtSm2();
        $result = $sm2->verifySign($data, $signatureHex, $publicKey);
        echo'[sm2Verify] SM2 verification result: '.$result;
        return $result;
    } catch (Exception $e) {
        echo '[sm2Verify Error] SM2-based verification error: ' . $e->getMessage();
        return false;
    }
}

C#

Sample code for signature verification:

using Xunit;
using System.Text;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.Utilities.Encoders;
using Org.BouncyCastle.Asn1.X9;
namespace PaySignatureTest
{
    public class SignatureUtilsTest3
    {
        /// <summary>
        ///Test: SM2-based signature verification (BouncyCastle must be imported). Command: dotnet add package BouncyCastle
        /// </summary>
        [Fact]
        public void TestSM2Verify()
        {
            // SM2 test data.
            var plainData = "";
            var publicKeyStr = "";
            var signData = ""; // SM2-based signature in Base64 format. Replace it with the actual data.
            // SM2-based signature verification.
            var verified = verifySM2(plainData, publicKeyStr, signData);
            Console.WriteLine($"SM2 verification result: {verified}");
        }
        public bool verifySM2(string content, string publicKeyStr, string signData)
        {
            // Use the BouncyCastle library to implement SM2-based signature verification.
            // Convert the Base64 signature to the hexadecimal format.
            var signBytes = Convert.FromBase64String(signData);
            var signHex = Hex.ToHexString(signBytes);
            Console.WriteLine($"plainData: {content}");
            Console.WriteLine($"sign: {signData}");
            Console.WriteLine($"sign hex: {signHex}");
            // Parse the public key.
            var curve = ECNamedCurveTable.GetByName("sm2p256v1");
            var domain = new ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H);
            var q = curve.Curve.DecodePoint(Hex.Decode(publicKeyStr));
            var publicKey = new ECPublicKeyParameters("EC", q, domain);
            // Parse the signature.
            var signer = new SM2Signer();
            signer.Init(false, publicKey);
            signer.BlockUpdate(Encoding.UTF8.GetBytes(content), 0, content.Length);
            var isVerified = signer.VerifySignature(Hex.Decode(signHex));
            return isVerified;
        }
    }
}

Notification Callback API Description

API Constraints