Payment App Signature Logic

To ensure secure and unaltered communication between your payment app and the SHOPLINE platform, SHOPLINE has implemented a signature verification process for the entire interaction flow. By reading this article, you will learn about the signature algorithm and its use cases.

Signature Algorithm

Algorithm Overview

The payment open interface signature generation algorithm is SHA1withRSA. The process is as follows:

Maintenance of Public Key

Acquiring SHOPLINE's Public Key

You can find SHOPLINE's platform public key on the homepage of the payment app. This public key is used to verify the signature of requests initiated by SHOPLINE, in order to prevent third-party forgery of requests. public key.jpg

Submission of Payment App Public Key You need to fill in the public key of the payment app on the payment app extension page. SHOPLINE platform will use this public key to verify the signature of the payment app's response/request to prevent third-party forgery.

choose version(payment).jpg public key(payment).jpg

Reminder: Please ensure that you keep your private key safe to avoid security issues due to key leakage.

Reminder

Please ensure that you keep your private key safe to avoid security issues due to key leakage.

Fields Involved in Signature

All fields in the request/response body that have values that are not null need to be included in the signature. The sign field will not be included in the signature.

Signature String Assembly

Sort the parameter names in the dictionary, then concatenate the parameter names and parameter values with '=', and use '&' to concatenate multiple parameters, such as key1=val1&key2=val2&key3=val3.

Special Types

When a value corresponding to a key is a complex type, there will be different ways of handling it:

  1. Key-value object: you need to recursively assemble each key-value pair of the object according to the assembly method, and the current key will be ignored.
  2. List type: you need to process each element in the list and connect them with commas, . If the element in the list is a key-value object, the key will also be ignored.

When key-value objects and list types are added to the signature text, the & link symbol will not be added.

Example Original Object:

{
"simple": 1,
"listSimple": ["1", "2"],
"object": {
"okey1": "value1",
"okey2": "value2 "
},
"listObject": [{
"lokey1": "1value1",
"lokey2": "1value2 "
},
{
"lokey1": "2value1",
"lokey2": "2value2 "
}
]
}

Unsigned text:

lokey1=1value1&lokey2=1value2   &lokey1=2value1&lokey2=2value2   listSimple=1,2&okey1=value1&okey2=value2   &simple=1

Pseudocode

// Original text to be signed.
func buildStr(Map<K,V> map) {
return buildStr("", map)
}
func buildStr(String str, Map<K,V> map) {
map.sortByKey();
for(k,v in map) {
if (v is simple) {
// Process simple types
if (str != "") {
str = str + "&";
}
str = str + key + "=" + value;
} else if (v is list) {
// Handling List Types
if (item in list is simple) {
str = str + key + "=" + value.join(",");
} else {
for (item in list) {
if (item is map) {
str = buildStr(str, item);
}
// Other types are not currently processed...
}
}
} else {
// Process complex types
str = buildStr(str, (Map)v);
}
}
return str;
}
Demo
java
import org.apache.commons.lang3.StringUtils;
import java.math.BigDecimal;
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.*;
public class SignatureDemo {
public static final String SIGN_ALG_NAME = "SHA1withRSA";
public static final String KEY_ALG_NAME = "RSA";
public static void main(String[] args) {
String privateKey = "Your private key";
String publicKey = "Your public key";
Map<String, Object> params = new HashMap<>();
params.put("key", "value");
String signatureSourceString = buildSignatureSourceString(params);
String signature = signature(privateKey, params);
boolean b = checkSignWithPublicKey(publicKey, signatureSourceString, signature);
System.out.println("b = " + b);
}
/**
*
* @param privateKey Private key for signing
* @param params The parameter object to be signed
* @return Signature string
*/
public static String signature(String privateKey, Map<String, Object> params) {
String signatureSource = buildSignatureSourceString(params);
return signWithPrivateKey(privateKey, signatureSource);
}
/**
* Generate the text to be signed
* @param params Parameters object to be signed
* @return Plain text signature/ Signed text
*/
public static String buildSignatureSourceString(Map<String, Object> params) {
StringBuffer content = new StringBuffer();
append(content, params);
return content.toString();
}
private static void append(StringBuffer content, Map<String, Object> sourceObj) {
if (null == sourceObj || sourceObj.keySet().size() == 0) {
return;
}
List<String> keyList = new ArrayList<>(sourceObj.keySet());
Collections.sort(keyList);
for (String key : keyList) {
Object value = sourceObj.get(key);
if (value instanceof List) {
Object itemValue = ((List<?>) value).get(0);
if (isScalarValue(itemValue)) {
content.append(key);
content.append("=");
content.append(StringUtils.join(((List<?>) value).toArray(), ","));
} else {
for (int i = 0; i < ((List<?>) value).size(); ++i) {
Object item = ((List<?>) value).get(i);
if (item instanceof Map) {
append(content, (Map) item);
}
}
}
} else if (value instanceof Map) {
append(content, (Map) value);
} else if (isScalarValue(value)) {
if (content.length() > 0) {
content.append("&");
}
content.append(key);
content.append("=");
content.append(value);
}
}
}
/**
* Check if a value is a scalar
* @param value
* @return true if the value is a scalar, false otherwise.
*/
private static boolean isScalarValue(Object value) {
return value instanceof String
|| value instanceof Float
|| value instanceof Double
|| value instanceof Integer
|| value instanceof Long
|| value instanceof BigDecimal
|| value instanceof Boolean;
}
private static String signWithPrivateKey(String privateKey, String signSourceStr) {
try {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(decodeBase64(privateKey));
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALG_NAME);
PrivateKey myPrivateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Signature signet = Signature.getInstance(SIGN_ALG_NAME);
signet.initSign(myPrivateKey);
signet.update(signSourceStr.getBytes(StandardCharsets.UTF_8));
byte[] signed = signet.sign();
return new String(org.apache.commons.codec.binary.Base64.encodeBase64(signed));
} catch (Exception ex) {
throw new RuntimeException(ex.getMessage(), ex);
}
}
public static boolean checkSignWithPublicKey(String publicKey, String signSourceStr, String signedStr) {
try {
X509EncodedKeySpec bobPubKeySpec = new X509EncodedKeySpec(decodeBase64(publicKey));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey pubKey = keyFactory.generatePublic(bobPubKeySpec);
byte[] signed = decodeBase64(signedStr);
Signature signetCheck = Signature.getInstance(SIGN_ALG_NAME);
signetCheck.initVerify(pubKey);
signetCheck.update(signSourceStr.getBytes(StandardCharsets.UTF_8));
return signetCheck.verify(signed);
} catch (Exception ex) {
log.warn("sign check fail, signSourceStr: {}, signedStr: {}", signSourceStr, signedStr, ex);
return false;
}
}
private static byte[] decodeBase64(String sourceStr) {
return org.apache.commons.codec.binary.Base64.decodeBase64(sourceStr.getBytes(StandardCharsets.UTF_8));
}
}
php
class SignatureUtilsBySHA1WithRSA {
/**
* Generate the plaintext string before signing based on the parameters
*/
public static function genSignatureSourceStr($params){
$signSourceStr = "";
$params = json_decode(json_encode($params));
return self::append($signSourceStr, $params);
}
public static function append(&$content, $params){
$emptyObj = false;
if(is_object($params) && count(get_object_vars($params))==0){
return ;
}
$params = (array)$params;
ksort($params);
foreach ($params as $key => $value) {
if(is_null($value)){
continue;
}
$prefix = empty($content) ? "" : "&";
if(is_scalar($value) || empty($value)){
$content .= $prefix.$key."=".(is_null($value) ? "" : $value);
}else{
if(is_array($value)){
if(empty($value)){
$content .= $key."=";
}elseif(is_scalar($value[0])){
$content .= $key."=".join(",", $value);
}else{
if(isset($value[0])){
foreach($value as $v){
self::append($content, $v);
}
}else{
self::append($content, $value);
}
}
}elseif(is_object($value)){
self::append($content, $value);
}else{
self::append($content, $value);
}
}
}
return $content;
}
/**
* Generate the signature string based on the plaintext and private key.
* @param privateKey private key for signing
* @param params object containing parameters to be signed
* @return signature string
*/
public static function genSignature($toSign, $privateKey){
$privateKey = "-----BEGIN RSA PRIVATE KEY-----\n" .
wordwrap($privateKey, 64, "\n", true) .
"\n-----END RSA PRIVATE KEY-----";
$key = openssl_get_privatekey($privateKey);
openssl_sign($toSign, $signature, $key);
openssl_free_key($key);
$sign = base64_encode($signature);
return $sign;
}
/**
* Generate the signature string based on the plaintext and private key.
* @param privateKey private key for signing
* @param params object containing parameters to be signed
* @return signature string
*/
public static function verifySignature($data, $sign, $pubKey){
$sign = base64_decode($sign);
$pubKey = "-----BEGIN PUBLIC KEY-----\n" .
wordwrap($pubKey, 64, "\n", true) .
"\n-----END PUBLIC KEY-----";
$key = openssl_pkey_get_public($pubKey);
$result = openssl_verify($data, $sign, $key, OPENSSL_ALGO_SHA1) === 1;
return $result;
}
}

Usage Scenarios

Verification Scenario

Synchronous Request

When SHOPLINE platform initiates a request to the payment app (payment, payment inquiry, refund, refund inquiry, etc.), the signature value will be carried in the Http request header pay-api-signature. To ensure that adding fields later will not cause the payment app that has been accessed to fail to verify the signature, the payment app needs to first verify the signature after receiving the original message, and then structure the data content. When SHOPLINE initiates a payment request, a random field will be added to the request body. If the data is structured before signature verification, the signature verification will definitely fail. If the signature verification fails, it needs to be treated as an illegal request to avoid security problems.

Signing Scenario

Synchronous Response

For the synchronous response to the request initiated by the SHOPLINE platform, the payment app needs to carry the signature value in the Http response header pay-api-signature. The SHOPLINE platform will verify the signature of the response. If the verification fails, the interface call will be considered a failure. Similarly, the SHOPLINE platform will verify the signature first, and then format the content of the data.

Asynchronous Notification

After the merchant completes the payment account binding (required), notifies the payment result (optional), and notifies the refund result (optional), the payment app can notify the SHOPLINE platform through the Payments APP API. When calling the open interface of the response, in addition to carrying the Authorization authentication field of the open platform in the Http request header, an additional request signature value needs to be carried in the request header signature. The SHOPLINE platform will verify the request signature. If the verification fails, it will be regarded as an illegal request.

Was this article helpful to you?