Signature generation

TIP

Merchants can follow the steps below to generate the signature of the request, and the platform will verify the signature after receiving the request. If the signature verification fails, the request is refused and the appropriate status code is returned.

Preparation

Merchants need to register with a merchant number and create a payment application through the merchant backstage to obtain APP_ID and APP_SECRECT.

Condition

All interfaces of the POST method in this document need to validate the signature, and other interfaces do not need to validate for the moment.

Generation

The signature string has four lines, one parameter per action. The line ends with \n (line break, The ASCII encoding value is 0x0A), and don't add \n on the last line. If the parameter itself ends in \n, you also need to attach an \n.

URL\n
Request timestamp\n
Request random string\n
Request message body

Let's take order inquiry as an example.

The first step is to get the absolute URL of the request and remove the domain name part to get the participating signed URL. If there are query parameters in the request, the URL should be appended with '? 'and the corresponding query string.

/v1/transaction/query

The second step is to obtain the current timestamp (milliseconds) of the system when the request is initiated. That is to say, the total number of seconds from 00:00 00 GMT on January 1, 1970, to the present, as the request timestamp. The platform will refuse to process requests made long ago, please keep the time of the merchant's own system accurate.

1554208460

The third step is to generate a 32-bit random string.

593BEC0C930BF1AFEB40B4A08C8FB242

The fourth step is to obtain the request message body in the request(request body).

{
    "app_id": "8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd",
    "mch_id": "1234567890",
    "transaction_id": "e98b30294xxxxxxxxxxxx97a9d9e09ce",
    "out_trade_no": "fb72xxxx-xxxx-xxxx-xxxx-xxxx8a7b52cb"
}

The fifth step is to construct the request signature string according to the previous rules as follows:

/v1/transaction/query\n
1554208460\n
593BEC0C930BF1AFEB40B4A08C8FB242\n
{"app_id":"8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd","mch_id":"1234567890","transaction_id":"e98b30294xxxxxxxxxxxx97a9d9e09ce","out_trade_no":"fb72xxxx-xxxx-xxxx-xxxx-xxxx8a7b52cb"}

Encryption

Take the javascript encryption process as an example:

let cryptoJs = require("crypto-js");
let key = CryptoJS.enc.Utf8.parse("9db664697xxxxxxxxxxxx2d27a3c925c") //the secret key of APP
//AES encryption
let ciphertext = cryptoJs.AES.encrypt(Signature string, key, {
  mode: CryptoJS.mode.ECB,
  padding: CryptoJS.pad.Pkcs7,
}).toString();

Example after encryption:

QCwHvoBM9TJ2wokF8hhaoS34P0nkJpYMisBUizpOj5q/77I6+KFPVvFUCaaUiu+KFctisJFU1DfJdCHrLpJIx9CirX5ku3L9TMGihFcEG8MGoh2dwDvunH8JgJOVV9ClSkpXqjad4flSuYMoxPOZqPHr+ktOLZ3pPzs12BMqmbZVNIe+oOezTZsQ8xxxxRgOJzwU/AbouZSl2xto7DcYCjvNSnw7BkuzBFgTfxVXB3+R7e+1SpdeJajuCKGKvYMVTe7slS5j/4LQ4vcr1QqOPhpoemsOV92tPhgQ0iGw3GKpLIEOoDAwy2+ojzP5XERh

Concatenate

The splice format of signature information is as follows:
app_id=APP_ID, mch_id=Merchant ID, nonce_str=The random string generated in step 3,timestamp=The timestamp generated in step 2, signature=Cryptographic signature string.

HTTP header

The document API passes the signature through an HTTP Authorization header. Authorization consists of two parts: authentication type and signature information. authentication type only supports TTPAY-AES-256-ECB for the moment.

Authorization: authentication type  signature information

Authorization header as follows: (Notes: Example because typesetting may contain line breaks, the actual data should be on one line)

Authorization: TTPAY-AES-256-ECB app_id=8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd,mch_id=1234567890,nonce_str=593BEC0C930BF1AFEB40B4A08C8FB242,timestamp=1554208460,signature=QCwHvoBM9TJ2wokF8hhaoS34P0nkJpYMisBUizpOj5q/77I6+KFPVvFUCaaUiu+KFctisJFU1DfJdCHrLpJIx9CirX5ku3L9TMGihFcEG8MGoh2dwDvunH8JgJOVV9ClSkpXqjad4flSuYMoxPOZqPHr+ktOLZ3pPzs12BMqmbZVNIe+oOezTZsQ8xxxxRgOJzwU/AbouZSl2xto7DcYCjvNSnw7BkuzBFgTfxVXB3+R7e+1SpdeJajuCKGKvYMVTe7slS5j/4LQ4vcr1QqOPhpoemsOV92tPhgQ0iGw3GKpLIEOoDAwy2+ojzP5XERh"

Finally, we can create an HTTP request that contains a signature.

$ curl https://api.ttpay.io/v1/transaction/query -H "Content-Type: application/json" -H 'Authorization: TTPAY-AES-256-ECB app_id=8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd,mch_id=1234567890,nonce_str=593BEC0C930BF1AFEB40B4A08C8FB242,timestamp=1554208460,signature=QCwHvoBM9TJ2wokF8hhaoS34P0nkJpYMisBUizpOj5q/77I6+KFPVvFUCaaUiu+KFctisJFU1DfJdCHrLpJIx9CirX5ku3L9TMGihFcEG8MGoh2dwDvunH8JgJOVV9ClSkpXqjad4flSuYMoxPOZqPHr+ktOLZ3pPzs12BMqmbZVNIe+oOezTZsQ8xxxxRgOJzwU/AbouZSl2xto7DcYCjvNSnw7BkuzBFgTfxVXB3+R7e+1SpdeJajuCKGKvYMVTe7slS5j/4LQ4vcr1QqOPhpoemsOV92tPhgQ0iGw3GKpLIEOoDAwy2+ojzP5XERh' -X POST -d '{"out_trade_no": "fb72xxxx-xxxx-xxxx-xxxx-xxxx8a7b52cb", "transaction_id":"e98b30294xxxxxxxxxxxx97a9d9e09ce", "app_id":"8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd", "mch_id":"1234567890" }'

Demo code

package com.example.http;

import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.sun.istack.internal.NotNull;
import okhttp3.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.Base64;

public class HttpApplication {
    private static String SECRET = "AES";
    private static String CIPHER_ALGORITHM = "AES/ECB/PKCS7Padding";
    private static String schema = "TTPAY-AES-256-ECB ";

    public static void main(String[] args) throws Exception {
        String key = "xxxxxxxx";    // AppSecret
        String reqURL = "/v1/transaction/query";    // Request interface

        OkHttpClient client = new OkHttpClient();
        JSONObject params = new JSONObject();

        try {
            params.put("app_id", "xxxxxxxx");           // Application ID
            params.put("mch_id", "xxxxxxxx");           // Merchat ID
            params.put("transaction_id", "xxxxxxxx");   // Platform order number
            params.put("out_trade_no", "xxxxxxxx");     // Merchat order number
        } catch (JSONException e) {
            e.printStackTrace();
        }

        Security.addProvider(new BouncyCastleProvider());

        // Signature
        String auth = auth(reqURL, params, key);

        RequestBody body = RequestBody.create(MediaType.parse("application/json;charset=utf-8"), params.toString());
        Request request = new Request.Builder()
        .header("Authorization", auth)
        .url(reqURL)
        .post(body)
        .build();

        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                System.out.println("http request error");
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                System.out.println(response.body().string());
            }
        });
    }

    /**
    * Signature
    * @param reqURL Request URL
    * @param raw Message body
    * @param key AppSecret
    * @return Signature information
    */
    public static String auth(String reqURL, JSONObject raw, String key) throws Exception {
        HttpUrl parseReqURL = HttpUrl.parse(reqURL);
        String url = parseReqURL.encodedPath();
        String appID = raw.getString("app_id");
        String mchID = raw.getString("mch_id");
        long currentTimeMillis = System.currentTimeMillis();
        String timestamp = String.valueOf(currentTimeMillis);
        String nonceStr = randomString(32);
        String message = url + "\n" + timestamp + "\n" + nonceStr + "\n" + raw.toString();
        String aes256ECBPkcs7PaddingEncrypt = aes256ECBPkcs7PaddingEncrypt(message, key);

        return schema + "app_id=" + appID + ",mch_id=" + mchID + ",nonce_str=" + nonceStr + ",timestamp=" + timestamp + ",signature=" + aes256ECBPkcs7PaddingEncrypt;
    }

    /**
    * Generate random string
    * @param len String length
    * @return Random string
    */
    public static String randomString(Integer ...len) {
        int e = len.length <= 0 ? 32 : len[0];
        String str = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678";
        int strLen = str.length();
        StringBuilder stringBuilder = new StringBuilder();

        for (int i = 0; i < e; i++) {
            double random = Math.random();
            int v = (int) Math.floor(random * strLen);
            char charAt = str.charAt(v);

            stringBuilder.append(charAt);
        }

        return stringBuilder.toString();
    }

    /**
    * AES encryption
    * @param str String
    * @param key Secret key
    * @return Cryptographic string
    * @throws Exception Abnormal information
    */
    public static String aes256ECBPkcs7PaddingEncrypt(String str, String key) throws Exception {
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);

        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(keyBytes, SECRET));

        byte[] doFinal = cipher.doFinal(str.getBytes(StandardCharsets.UTF_8));

        return new String(Base64.getEncoder().encode(doFinal));
    }

    /**
    * AES encryption
    * @param str String
    * @param key Secret key
    * @return Decryption string
    * @throws Exception Abnormal information
    */
    public static String aes256ECBPkcs7PaddingDecrypt(String str, String key) throws Exception {
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);

        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBytes, SECRET));

        byte[] doFinal = cipher.doFinal(Base64.getDecoder().decode(str));

        return new String(doFinal);
    }
}
package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"math/rand"
	"net/http"
	"time"

	"github.com/forgoer/openssl"
)

const (
	SignatureMessageFormat = "%s\n%d\n%s\n%s" // The original format of the digital signature
	HeaderAuthorizationFormat = "%s app_id=%s,mch_id=%s,nonce_str=%s,timestamp=%d,signature=%s"
)

type QueryReq struct {
	AppID         string `json:"app_id"`
	MchID         string `json:"mch_id"`
	TransactionID string `json:"transaction_id,omitempty"`
	OutTradeNo    string `json:"out_trade_no,omitempty"`
}

func main() {

	appSecret := "**********"           // AppSecret
	reqPath := "/v1/transaction/query"  // Request interface path
	queryReq := QueryReq{
		AppID:         "**********",    // Application ID
		MchID:         "**********",    // Merchat ID
		TransactionID: "**********",    // Platform order number
		OutTradeNo:    "**********",    // Merchat order number
	}

	b, _ := json.Marshal(queryReq)

    // Signature
	authorization, err := GenerateAuthorizationHeader(queryReq.AppID, queryReq.MchID, reqPath, string(b), appSecret)
	if err != nil {
		panic(err)
	}

    // Request interface
	url := "" + reqPath

	payload := bytes.NewBuffer(b)

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", authorization)

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(string(body))
}

// Signature
func GenerateAuthorizationHeader(appID, mchID, reqURL, signBody, appSecret string) (string, error) {
	nonceStr := RandomString(32)
	timestamp := time.Now().Unix()
	message := fmt.Sprintf(SignatureMessageFormat, reqURL, timestamp, nonceStr, signBody)

	signatureResult, err := AesECBEncrypt(message, appSecret)
	if err != nil {
		return "", err
	}
	authorization := fmt.Sprintf(
		HeaderAuthorizationFormat, getAuthorizationType(), appID,
		mchID, nonceStr, timestamp, signatureResult,
	)
	return authorization, nil
}

// AES encryption
func AesECBEncrypt(orig, key string) (string, error) {
	dst, _ := openssl.AesECBEncrypt([]byte(orig), []byte(key), openssl.PKCS7_PADDING)
	return base64.StdEncoding.EncodeToString(dst), nil
}

// AES encryption
func AesECBDecrypt(crypted, key string) (string, error) {
	x := len(crypted) * 3 % 4
	switch {
	case x == 2:
		crypted += "=="
	case x == 1:
		crypted += "="
	}
	crytedByte, err := base64.StdEncoding.DecodeString(crypted)
	if err != nil {
		return "", err
	}
	origData, err := openssl.AesECBDecrypt(crytedByte, []byte(key), openssl.PKCS7_PADDING)
	if err != nil {
		return "", err
	}
	return string(origData), err
}

// Generate random character
func RandomString(length int) string {
	str := []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
	var result []byte
	rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
	for i := 0; i < length; i++ {
		result = append(result, str[rnd.Intn(len(str))])
	}
	return string(result)
}

func getAuthorizationType() string {
	return "TTPAY-AES-256-ECB"
}
<?php
$key = "xxxxxxxx";                //  AppSecret
$APP_ID = "xxxxxxxx";              // Application ID
$mchID = "xxxxxxxx";              // Merchat ID
$transactionID = "xxxxxxxx";      // Platform order number
$outTradeNo = "xxxxxxxx";         // Merchat order number
$url = "/v1/transaction/query";   // Interface path
$timestamp = time();              // Current timestamp
$nonce = generateStr(32);         // 32-bit random string

// Generate random string
function generateStr($length) {
  $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  $res ='';

  for ( $i = 0; $i < $length; $i++ ) {
    $res .= $chars[ mt_rand(0, strlen($chars) - 1) ];
  }

  return $res;
}

// Message body
$body = '{"app_id":"'.$APP_ID.'","mch_id":"'.$mchID.'","transaction_id":"'.$transactionID.'","out_trade_no":"'.$outTradeNo.'"}';

// Construct signature string
$message = $url."\n".$timestamp."\n".$nonce."\n".$body;

// Encrypt signature string
$cipher = "aes-256-ecb";
$sign = openssl_encrypt($message, $cipher, $key);

// Split signature information
$schema = 'TTPAY-AES-256-ECB';
$authorization = sprintf('%s app_id=%s,mch_id=%s,nonce_str=%s,timestamp=%d,signature=%s', $schema, $APP_ID, $mchID, $nonce, $timestamp, $sign);

// Set HTTP header
$header[] = "Accept: application/json";
$header[] = "Content-Type: application/json";
$header[] = "Authorization: $authorization";

// Request interface
$curl = curl_init();
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
curl_setopt($curl, CURLOPT_URL, "".$url);
curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$result = curl_exec($curl);
curl_close($curl);

var_export($result);

let cryptoJs = require("crypto-js");

let jsonObj = JSON.parse(pm.request.body.raw); //Convert the submitted body string into an object
let jsonStr = JSON.stringify(jsonObj)//Turn the object back into a string to compress, avoiding line breaks in the body that affect signature compression.
let appID = jsonObj.app_id;
let mchID = jsonObj.mch_id;

// Generate the current timestamp
let timestamp = Math.round(new Date() / 1000).toString()

//Define random string
var nonceStr = randomString(); // Random string
function randomString(e) {
    e = e || 32;
    var t = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678",
    a = t.length,
    n = "";
    for (i = 0; i < e; i++) n += t.charAt(Math.floor(Math.random() * a));
    return n
}

//Get the full path
var reqURL = "/" + pm.request.url.path.join("/");

//Splice signature plaintext
message = reqURL + "\n" + timestamp + "\n" + nonceStr + "\n" + jsonStr

let key = CryptoJS.enc.Utf8.parse("********************************") // AppSecret
//AES encryption
let ciphertext = cryptoJs.AES.encrypt(message, key, {
    mode: CryptoJS.mode.ECB,
    padding: CryptoJS.pad.Pkcs7,
  }).toString();

//Splice on the authorization of Header, TTPAY-{EncryMode)},currently supports only AES-256-ECB
let authorization = 'TTPAY-AES-256-ECB app_id=' + appID + ',mch_id=' + mchID + ',nonce_str=' + nonceStr + ',timestamp=' + timestamp + ',signature=' + ciphertext

pm.request.headers.upsert({ key: "authorization", value: authorization })
Last updated: