All API requests must use HTTPS.
Supported TLS versions: 1.2 and 1.3.
Supported cipher suites:
TLS_CHACHA20_POLY1305_SHA256 TLS_AES_128_GCM_SHA256 TLS_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
Currently, the external APIs of Payment Kit involve two HTTP methods: GET and POST.
GET: applicable to query. This method is used to query resource data from the server, such as order and signing information.
POST: applicable to resource creation or update, such as pre-order and refund requests.
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: confirms the identity of the caller to prevent repudiation and data tampering. For all APIs open to merchants, the merchants must pass the PayMercAuth field in the request header. The subsequent process can be performed only after the authentication of the open gateway is passed.
Authorization: ensures that only specific callers have permissions to call given APIs. Some APIs can be used only after specific merchants apply for and enable them.
Signing scenario: parameter signature in the header of a request from the merchant cloud.
Signature algorithm: SHA256WithRSA/PSS or SM2.
Development procedure
Filtering: Exclude the sign field from the JSON string in the request and retain only other fields.
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¤cy=CNY&mercNo=xxxxxx&mercOrderNo=xxxxxx&totalAmount=2&tradeSummary=Magazine and newspaper"
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¤cy=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.
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 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.
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.
Obtain the fields involved in signing, convert them into a map, sort the key-value pairs in the map, and concatenate them using &.
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.
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;
}
}
} For callback notifications, if Payment Kit does not receive a response of the application/json type or the received response is not in {"resultCode":"000000","resultDesc":"Success."} format, Payment Kit will periodically resend the notifications. However, the final success of the notifications is not guaranteed.
The same notification may be sent to the merchant server multiple times. The merchant server needs to correctly handle the repeated requests. The handling suggestions are as follows:
If the callback request from Payment Kit is not received within the expected time, check whether the network connection of the provided callback URL is normal. If there is no network connectivity issue, call the synchronous query API to confirm the order status. The check suggestions are as follows:
For the received asynchronous callback request, signature verification must be performed. Subsequent service processes can be performed only after the signature verification is successful. Otherwise, potential attacks on the merchant may occur due to information leakage, causing financial losses.
The merchant shall bear any business exceptions and fund losses caused by the merchant's own system implementation.
If the merchant has IP address firewall policy restrictions on the payment callback URL, the following network segments must be added to the trustlist. Any changes will be updated here.
When receiving a callback notification, the merchant system must return a response within 3 seconds. Otherwise, Huawei Pay considers the notification failed and triggers the retry mechanism.
The callbackId parameter in the server asynchronous notification becomes invalid only when the merchant system receives the asynchronous notification and returns a {"resultCode":"000000","resultDesc":"Success."} response. The value of callbackId remains unchanged for multiple retries of the same asynchronous notification request.
API Constraints
Do not configure an IP address trustlist on your server to restrict Huawei egress IP addresses. Configuring an IP address trustlist is meaningless because it restricts business development without improving security, especially when a more secure RSA signature mechanism is available. You are accountable for any consequence in the case of your breach of this rule.
The address must support HTTPS and have a valid commercial certificate. Otherwise, notifications cannot be received.
Supported TLS versions: 1.2 and 1.3.
Supported cipher suites:
TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 TLS_DHE_DSS_WITH_AES_128_GCM_SHA256 TLS_DHE_DSS_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_AES_128_GCM_SHA256