生成签名及加验签

生成签名并对请求加签

在发送请求给万里汇前,集成商需要对信息进行加签。

下图为生成签名的流程:

yuque_diagram (6).jpg

步骤一:生成密钥对

在加签前,集成商需要生成一对公钥和私钥。生成一对RSA2密钥的示例方法如下:

  1. 执行以下指令,private.pem文件作为输出文件生成私钥:
copy
openssl genrsa -out private.pem 2048
  1. 执行以下指令生成公钥,其中private.pem为上一步骤中生成的私钥:
copy
openssl rsa -in private.pem -pubout -out public.pem
  1. 若需要PKCS8格式的密钥,可以执行以下操作将上述生成的RSA私钥转为PKCS8格式私钥:
copy
openssl pkcs8 -topk8 -inform PEM -in private.pem -out private_pkcs8.pem -nocrypt

步骤二:生成待加签数据

按照以下规则生成待加签数据::

copy
<HTTP-Method> <Request-URL-Endpoint>
<Client-ID>.<Request-Time>.<Request-Body>

例如,当请求信息的请求体(<Request-Body>)为以下内容时:

copy
{
  "removeBeneficiaryRequestId":"*****",
  "beneficiaryToken":"ALIPAYqwertyuiopoiuytrewqwertyuiopoiuytr*****",
  "customerId":"*****"
}

集成商需要生成下述格式的数据:

copy
POST /v1/business/account/removeBeneficiary
5Y60382Z2Y4S*****.2022-04-28T12:31:30+08:00.{"removeBeneficiaryRequestId":"*****","beneficiaryToken":"*****","customerId":"*****"}

其中:

  • HTTP调用方法(<HTTP-Method>) 为 POST
  • 请求URLEndpoint(<Request-URL-Endpoint>) 为 /v1/business/account/removeBeneficiary
  • Client ID(<Client-ID>) 为 5Y60382Z2Y4S*****
  • 请求时间(<Request-Time>) 为 2022-04-28T12:31:30+08:00,时间遵循ISO 8601标准。
  • 请求信息为 {\"removeBeneficiaryRequestId\":\"*****\",\"beneficiaryToken\":\"*****\",\"customerId\":\"*****\"}.

步骤三:生成签名

步骤二生成的待加签数据,使用SHA256算法生成Hash值。然后使用Base64算法对hash值进行编码。

使用SHA256方法处理待加签数据,生成hash值。然后使用Base64对hash值进行编码。

copy
def RSA_sign(data,privateKey):
    private_keyBytes = base64.b64decode(privateKey)
    priKey = RSA.importKey(private_keyBytes)
    signer = PKCS1_v1_5.new(priKey)
    hash_obj = SHA256.new(data.encode('utf-8'))
    signature = base64.b64encode(signer.sign(hash_obj))
    return signature
  • privateKey步骤一中生成的私钥
  • data步骤二中生成的待加签数据

生成的签名 (signature)范例:

copy
KEhXthj4bJ8...*****

步骤四:加签

按照以下规则生成请求头中的完整签名部分:

copy
'Signature: algorithm=<algorithm>, keyVersion=<key-version>, signature=<signature>'
  • algorithm: 生成签名使用的算法,默认值为RSA256
  • keyVersion: 加验签使用的密钥版本
  • signature: 步骤三中生成的签名内容。

注:对信息进行加签后,集成商可将请求信息发送至万里汇。请求发送方法详见快速上手页面。

对响应信息验签

集成商收到响应后,需要对完成响应信息的验签,具体流程如下图所示:

yuque_diagram (7).jpg

步骤一:获取万里汇公钥

联系万里汇技术支持获取公钥,或登录开发者中心下载公钥。

copy
MIIBIjANBgkqhkiG9...*****

步骤二:生成待验证数据

本步骤与加签的“生成待加签数据”步骤规则相同,集成商通过本步骤处理返回体,生成待验证数据。并准备对数据进行验证。

步骤三:获取签名

万里汇按照下述格式生成字符串类型的签名:

copy
'Signature: algorithm=<algorithm>, keyVersion=<key-version>, signature=<signature>'
  • algorithm: 生成签名使用的算法,默认值为RSA256
  • keyVersion: 加验签使用的密钥版本
  • signature: 需要验证的签名内容

返回头中包含的字符串类型签名完整格式如下:

copy
signature: algorithm=RSA256, keyVersion=2, signature=KEhXthj4b...*****

步骤四:验证签名

对步骤二中准备的待验证数据,使用SHA256算法处理待验证数据,生成hash值。

用公钥对步骤三中获取的签名进行解密。签名需要用Base64算法进行解码。

例:

copy
def verify(httpMethod, uriWithQueryString, clientId, timeString, content,signature,publicKey):
    public_keyBytes = base64.b64decode(publicKey)
    pubKey = RSA.importKey(public_keyBytes)
    data = sign(httpMethod, uriWithQueryString, clientId, timeString, content)
    ssda =data.encode('utf-8')
    h = SHA256.new(ssda)
    verifier = PKCS1_v1_5.new(pubKey)
    return verifier.verify(h, base64.b64decode(signature))
  • publicKey: 步骤一中从万里汇获取的公钥。
  • data: 步骤二中生成的待验证数据。
  • signature: 步骤三中截取的签名内容

示例代码中,验证完成后返回布尔值。若返回值为false,则签名与数据不一致。验证失败可能因为:密钥对不匹配,或步骤二中生成的待验证数据(data)有误。

示例代码

Python 示例代码

copy
import base64
import warnings
import urllib.parse
import flask
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5

warnings.filterwarnings("ignore")


NAME_VALUE_SEPARATOR = "="
COMMA = ","
ALGORITHM = "algorithm"
SIGNATURE = "signature"
KEY_VERSION = "keyVersion"
RSA_256 = "RSA256"
server = flask.Flask(__name__)  # __name__ represents the current python file.

#Generate signature and sign a request
def RSA_sign(data,privateKey):
    private_keyBytes = base64.b64decode(privateKey)
    priKey = RSA.importKey(private_keyBytes)
    signer = PKCS1_v1_5.new(priKey)
    hash_obj = SHA256.new(data.encode('utf-8'))
    signature = base64.b64encode(signer.sign(hash_obj))
    return signature

def sign(httpMethod, uriWithQueryString, clientId, timeString, reqBody):
    reqContent = httpMethod + " " + uriWithQueryString + "\n" + clientId + "." + timeString + "." + reqBody
    return reqContent

#Validate the signature of a response
def verify(httpMethod, uriWithQueryString, clientId, timeString, content,signature,publicKey):
    public_keyBytes = base64.b64decode(publicKey)
    pubKey = RSA.importKey(public_keyBytes)
    data = sign(httpMethod, uriWithQueryString, clientId, timeString, content)
    ssda =data.encode('utf-8')
    h = SHA256.new(ssda)
    verifier = PKCS1_v1_5.new(pubKey)
    return verifier.verify(h, base64.b64decode(signature))

@server.route('/signature/add', methods=['post'])
def signatureAdd():
    content = str(flask.request.get_data())
    content = content[2:-1]
    httpMethod = flask.request.headers['httpMethods']
    uriWithQueryString =flask.request.headers['uriWithQueryString']
    clientId = flask.request.headers['clientId']
    timeString = flask.request.headers['timeString']
    privateKey = flask.request.headers['privateKey']
    data = sign(httpMethod, uriWithQueryString, clientId, timeString, content)
    res_sign1 = RSA_sign(data,privateKey)
    signature = res_sign1.decode('utf-8')
    values = {}
    values["signature"] = signature
    data = urllib.parse.urlencode(values)
    res = ALGORITHM + NAME_VALUE_SEPARATOR + RSA_256 + COMMA + KEY_VERSION + NAME_VALUE_SEPARATOR + "1" + COMMA + data
    return res

@server.route('/signature/verification', methods=['post'])
def verification():
    content = str(flask.request.get_data())
    content = content[2:-1]
    httpMethod = flask.request.headers['httpMethods']
    uriWithQueryString =  flask.request.headers['uriWithQueryString']
    clientId = flask.request.headers['clientId']
    timeString = flask.request.headers['timeString']
    publicKey = flask.request.headers['publicKey']
    signature = flask.request.headers['signature']
    databoolea = verify(httpMethod, uriWithQueryString, clientId, timeString, content,urllib.parse.unquote(signature),publicKey)
    return str(databoolea)


if __name__ == '__main__':
    server.run(port=5001, debug=True,host='0.0.0.0')  #Default port is 5000.

C# 示例代码

copy
using System.Text;
using System.Web;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Security;

namespace test_0505
{
    class Program
    {
        static string signPrivateKey = "MIIEvgIBADANBgkqhkiG9w0B...*****";
        
        static string verifyPublicKey = "MIIBIjA...*****";
        static void Main(string[] args)
        {
            string httpMethod = "POST";
            string uriWithQueryString = "/amsin/api/v1/business/account/inquiryBalance";
            string clientId = "*****";
            string timeString = "2022-04-28T12:31:30+08:00";
            string content = "{\"customerId\":\"*****\"}";
            string reqContent = httpMethod + " " + uriWithQueryString + "\n" + clientId + "." + timeString + "." + content;
            Console.WriteLine(reqContent);
            string signature = Sign(reqContent, signPrivateKey);
            Console.WriteLine(signature);
            bool result = ValidationPublicKey(reqContent, signature, verifyPublicKey);
            if(result){
                Console.WriteLine("verify success");
            }
        }
        static string Sign(string contentForSign, string privateKey)
        {
            
            AsymmetricKeyParameter priKey = GetPrivateKeyParameter(privateKey);
            byte[] byteData = System.Text.Encoding.UTF8.GetBytes(contentForSign);
            
            ISigner normalSig = SignerUtilities.GetSigner("SHA256WithRSA");
            normalSig.Init(true, priKey);
            normalSig.BlockUpdate(byteData, 0, contentForSign.Length);
            byte[] normalResult = normalSig.GenerateSignature();
            var arr = Convert.ToBase64String(normalResult);
            Console.WriteLine(arr);
            return UrlEncode(arr, Encoding.UTF8);
        }
        static AsymmetricKeyParameter GetPrivateKeyParameter(string s)
        {
            s = s.Replace("\r", "").Replace("\n", "").Replace(" ", "");
            byte[] privateInfoByte = Convert.FromBase64String(s);
            AsymmetricKeyParameter priKey = PrivateKeyFactory.CreateKey(privateInfoByte);
            return priKey;
        }
        static string UrlEncode(string temp, Encoding encoding)
        {
            StringBuilder stringBuilder = new StringBuilder();
            for (int i = 0; i < temp.Length; i++)
            {
                string t = temp[i].ToString();
                string k = HttpUtility.UrlEncode(t, encoding);
                if (t == k)
                {
                    stringBuilder.Append(t);
                }
                else
                {
                    stringBuilder.Append(k.ToUpper());
                }
            }
            return stringBuilder.ToString();
        }
        
        static bool ValidationPublicKey(string plainData, string sign, string key)
        {
            AsymmetricKeyParameter pubKey = GetPublicKeyParameter(key);
            string newSign = HttpUtility.UrlDecode(sign, Encoding.UTF8);
            Console.WriteLine(newSign);
            
            byte[] plainBytes = Encoding.UTF8.GetBytes(plainData);
            
            
            ISigner verifier = SignerUtilities.GetSigner("SHA256WithRSA");
            verifier.Init(false, pubKey);
            verifier.BlockUpdate(plainBytes, 0, plainBytes.Length);
            byte[] signBytes = Convert.FromBase64String(newSign);
            
            return verifier.VerifySignature(signBytes);
        }
        
        static AsymmetricKeyParameter GetPublicKeyParameter(string s)
        {
            s = s.Replace("\r", "").Replace("\n", "").Replace(" ", "");
            byte[] publicInfoByte = Convert.FromBase64String(s);
            AsymmetricKeyParameter pubKey = PublicKeyFactory.CreateKey(publicInfoByte);
            return pubKey;
        }
    }
}

Java 示例代码

copy
package com.alibaba.test;


import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;


public class Demo {

    /**
     * name and value separator
     */
    private static final String NAME_VALUE_SEPARATOR = "=";

    /**
     * comma
     */
    private static final String COMMA = ",";

    /**
     * algorithm
     */
    private static final String ALGORITHM = "algorithm";

    /**
     * signature
     */
    private static final String SIGNATURE = "signature";

    /**
     * keyVersion
     */
    private static final String KEY_VERSION = "keyVersion";

    /**
     * RSA256
     */
    private static final String RSA_256 = "RSA256";



    private static final String signPrivateKey =  "MIIEvwIBADANBgkqhkiG...*****";

    private static final String verifyPublicKey = "MIIBIjANBg...*****";

    public static void main(String[] args) throws Exception {
        String httpMethod = "POST";
        String uriWithQueryString = "/amsin/api/v1/business/account/inquiryBalance";
        String clientId = "*****";
        String timeString = "2022-03-02T15:03:30+08:00";
        String content = "{\"transferFactor\":{\"transferFundType\":\"GLOBAL_WORLDFIRST\"},\"currency\":\"USD\"}";

        String signature = sign(httpMethod, uriWithQueryString, clientId, timeString, content, signPrivateKey);

        String signatureHeaderPayload = ALGORITHM +
                NAME_VALUE_SEPARATOR +
                RSA_256 +
                COMMA +
                KEY_VERSION +
                NAME_VALUE_SEPARATOR +
                "1" +
                COMMA +
                SIGNATURE +
                NAME_VALUE_SEPARATOR +
                signature;
        System.out.println("signatureHeaderPayload:\n" + signatureHeaderPayload);

        boolean res = verify(httpMethod, uriWithQueryString, clientId, timeString, content, signature, verifyPublicKey);
        if (res) {
            System.out.println("verify success.");
        }
    }

    /**
     * Sign the contents of the merchant request
     *
     * @param httpMethod         http method                e.g., POST, GET
     * @param uriWithQueryString query string in url        e.g., if your request url is https://{domain_name}.com/ams/api/pay/query uriWithQueryString should be /ams/api/pay/query not https://{domain_name}.com/ams/api/pay/query
     * @param clientId           clientId issued by WorldFirst  e.g., *****
     * @param timeString         "request-time" in request  e.g., 2020-01-03T14:36:27+08:00
     * @param reqBody            json format request        e.g., "{"paymentRequestId":"*****","refundRequestId":"*****","refundAmount":{"currency":"USD","value":"123"},"extendInfo":{"":""}}"
     * @param merchantPrivateKey your private key
     */
    public static String sign(
            String httpMethod,
            String uriWithQueryString,
            String clientId,
            String timeString,
            String reqBody,
            String merchantPrivateKey) throws Exception {
        // 1. construct the request content
        String reqContent = httpMethod + " " + uriWithQueryString + "\n" + clientId + "." + timeString + "." + reqBody;
        System.out.println("reqContent is " + "\n" + reqContent);

        // 2. sign with your private key
        String originalString = signWithSHA256RSA(reqContent, merchantPrivateKey);
        //  System.out.println("originalString is " + originalString);

        // 4. return the encoded String
        return URLEncoder.encode(originalString, "UTF-8");
    }

    /**
     * Check the response of WorldFirst
     *
     * @param httpMethod         http method                  e.g., POST, GET
     * @param uriWithQueryString query string in url          e.g., if your request url is https://{domain_name}.com/ams/api/pay/query uriWithQueryString should be /ams/api/pay/query not https://{domain_name}.com/ams/api/pay/query
     * @param clientId           clientId issued by WorldFirst    e.g., *****
     * @param timeString         "response-time" in response  e.g., 2020-01-02T22:36:32-08:00
     * @param rspBody            json format response         e.g., "{"acquirerId":"xxx","refundAmount":{"currency":"CNY","value":"123"},"refundFromAmount":{"currency":"JPY","value":"234"},"refundId":"xxx","refundTime":"2020-01-03T14:36:32+08:00","result":{"resultCode":"SUCCESS","resultMessage":"success","resultStatus":"S"}}"
     * @param worldfirstPublicKey    public key from WorldFirst
     */
    public static boolean verify(
            String httpMethod,
            String uriWithQueryString,
            String clientId,
            String timeString,
            String rspBody,
            String signature,
            String worldfirstPublicKey) throws Exception {
        // 1. construct the response content
        String responseContent = httpMethod + " " + uriWithQueryString + "\n" + clientId + "." + timeString + "." + rspBody;

        // 2. decode the signature string
        String decodedString = URLDecoder.decode(signature, "UTF-8");

        // 3. verify the response with WorldFirst's public key
        return verifySignatureWithSHA256RSA(responseContent, decodedString, worldfirstPublicKey);

    }


    /**
     * Generate base64 encoded signature using the sender's private key
     *
     * @param reqContent:    the original content to be signed by the sender
     * @param strPrivateKey: the private key which should be base64 encoded
     */
    private static String signWithSHA256RSA(String reqContent, String strPrivateKey) throws Exception {
        Signature privateSignature = Signature.getInstance("SHA256withRSA");
        privateSignature.initSign(getPrivateKeyFromBase64String(strPrivateKey));
        privateSignature.update(reqContent.getBytes(StandardCharsets.UTF_8));
        byte[] bytes = privateSignature.sign();

        return Base64.getEncoder().encodeToString(bytes);
    }


    private static PrivateKey getPrivateKeyFromBase64String(String privateKeyString) throws Exception {
        byte[] b1 = Base64.getDecoder().decode(privateKeyString);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(b1);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        return kf.generatePrivate(spec);
    }

    /**
     * Verify if the received signature is correctly generated with the sender's public key
     *
     * @param rspContent: the original content signed by the sender and to be verified by the receiver.
     * @param signature:  the signature generated by the sender
     * @param strPk:      the public key string-base64 encoded
     */
    private static boolean verifySignatureWithSHA256RSA(String rspContent, String signature, String strPk) throws Exception {
        PublicKey publicKey = getPublicKeyFromBase64String(strPk);

        Signature publicSignature = Signature.getInstance("SHA256withRSA");
        publicSignature.initVerify(publicKey);
        publicSignature.update(rspContent.getBytes(StandardCharsets.UTF_8));

        byte[] signatureBytes = Base64.getDecoder().decode(signature);
        return publicSignature.verify(signatureBytes);
    }


    private static PublicKey getPublicKeyFromBase64String(String publicKeyString) throws Exception {
        byte[] b1 = Base64.getDecoder().decode(publicKeyString);
        X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(b1);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        return kf.generatePublic(X509publicKey);
    }
}

JavaScript 示例代码

copy

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title></title>
    <script src="https://cdn.bootcss.com/crypto-js/3.1.9-1/crypto-js.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/jsencrypt/3.2.1/jsencrypt.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/jsrsasign/10.5.13/jsrsasign-all-min.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/2.2.4/jquery.min.js"></script>

</head>

<body>
    <div>

        <div>
            <div>Signature:</div>
            <div>client-id</div>
            <div>
                <input id="clientId" type="text" />
            </div>

            <div>request-time</div>
            <div>
                <input id="requestTime" type="text"></input>
            </div>

            <div>httpMethod</div>
            <div>
                <input id="httpMethod" type="text" />
            </div>

            <div>api</div>
            <div>
                <input id="api" type="text" />
            </div>

            <div>body</div>
            <div>
                <input id="bodyData" type="text" />
            </div>

            <div>Private Key:</div>
            <div>
                <input id="privkey" type="text" />
            </div>


        </div>
        <button onclick="getUrlceshi();">Generate the signature</button>
    </div>
</body>
<script>

    function getUrlceshi() {
        var cid = $("#clientId").val();
        var requestTime = $("#requestTime").val();
        var httpMethod = $("#httpMethod").val();
        if (httpMethod == '' || httpMethod == undefined) {
            httpMethod = "POST"
        }
        var urlApi = $("#api").val();
        var bodyData = $("#bodyData").val();

        //Private Key
        var privkey = $("#privkey").val();
        console.log(privkey);

        //Assemble the data to be signed
        var signatureBody = httpMethod + " " + urlApi + "\n" + cid + "." + requestTime + "." + bodyData;
        console.log(signatureBody);

        var signatureData = RsaSign(privkey, signatureBody)
        console.log(signatureData);
    }


    //Signature
    function RsaSign(privKey, plainText) {
        var signStr = new JSEncrypt();

        //Set the Private Key
        // signStr.setPrivateKey('-----BEGIN RSA PRIVATE KEY-----'+privKey+'-----END RSA PRIVATE KEY-----');
        signStr.setPrivateKey(privKey)

        //Sign the request with the signature
        var signature = signStr.sign(plainText, CryptoJS.SHA256, "sha256"); 
        return encodeURIComponent(signature);
    }

    //Validate the signature
    function RsaVerify(plainText, signature) {
        var verify = new JSEncrypt();

        //Set the Public Key
        // verify.setPublicKey('-----BEGIN PUBLIC KEY-----' + pubKey + '-----END PUBLIC KEY-----');
        verify.setPublicKey(pubKey)

        //Validate the signature
        var verified = verify.verify(plainText, signature, CryptoJS.SHA1);
        return verified;
    }


</script>

</html>

@2024 WorldFirst