Signature logic
To ensure secure and tamper-proof communication between your payment app and SHOPLINE, SHOPLINE has implemented signature verification logic for the entire transaction flow and the merchant activation and binding flow.
For scenarios where SHOPLINE initiates requests to your payment app, you need to:
- Verify the legitimacy of SHOPLINE's request data using the signature algorithm. If the signature verification fails, the request should not be processed.
- Ensure that the response data returned to SHOPLINE is signed according to the signature algorithm. SHOPLINE will verify the signature of the response. If the verification fails, it will not trust the results returned by the payment app.
For scenarios where your payment app proactively notifies SHOPLINE, you need to ensure that the request data sent to SHOPLINE is signed according to the signature algorithm. SHOPLINE will verify the signature of the request content. If the verification fails, the request will not be processed.
Signature algorithm
Communication between the payment app and SHOPLINE is encrypted using the SHA1withRSA algorithm. The general process is as follows:
- When you generate the RSA key pair, note that the keys must be in the PKCS#8 format and have a length of 2048 bits.
- Both parties must hold their own private key and the other party's public key. It is crucial to securely store the private key, as its leakage could lead to serious security risks.
- When you sign data, use your own private key.
- When you verify a signature, use the other party's public key.
Obtain the public key of SHOPLINE
You can find the public key of SHOPLINE on the payment app details page in the Partner Portal. This public key is used to verify signatures on requests initiated by SHOPLINE, preventing third-party request forgery.
Submit the public key of your payment app
You need to submit your public key on the SHOPLINE Partner Portal > Apps > App details > Payment app extensions page. For more information, refer to the steps to set up payment app extensions. SHOPLINE will use the public key to verify signatures on responses or requests from the payment app, preventing third-party forgery.
Signing and verification
Generate the text to be signed
The first step in signing or verifying a signature is to assemble the raw message into the text to be signed.
When you generate the text to be signed, make sure that the parameter names are sorted in lexicographical order. Then, use = to concatenate a parameter name and its value, and use & to concatenate multiple parameters, resulting in a string like: key1=val1&key2=val2&key3=val3.
- All non-null fields must be included in the signature.
- When a field value is a complex type, follow these processing rules:
- Key-value object: Recursively assemble each key-value pair within the object. The object name is ignored.
- List:
- If the elements in the list are key-value objects, process them recursively based on the key-value object rule.
- If the elements in the list are simple scalars, process each element and then concatenate them using commas (
,).
- When key-value objects and lists are added to the text to be signed, do not use the ampersand (
&).
For example, there is the following raw message:
{
"normalKey": "normalValue",
"emptyKey": "",
"nullKey": null,
"list": ["elem1", "elem2"],
"listObject": [{
"elemKey": "elem1"
}, {
"elemKey": "elem2"
}],
"obj": {
"emptyObjectKey": "",
"objectKey": "objectValue"
}
}
The generated text to be signed is:
emptyKey=list=elem1,elem2&elemKey=elem1&elemKey=elem2&normalKey=normalValue&emptyObjectKey=&objectKey=objectValue
Sample code
Java:
import org.apache.commons.lang3.StringUtils;
import java.math.BigDecimal;
import java.util.*;
/**
* Generate a demo of the signature string.
*/
public class SignatureSourceStringDemo {
public static void main(String[] args) {
Map<String, Object> params = new HashMap<>();
params.put("normalKey", "normalValue");
params.put("emptyKey", "");
params.put("list", Lists.newArrayList("elem1", "elem2"));
params.put("obj", new HashMap<String, Object>() {{
put("objectKey", "objectValue");
put("emptyObjectKey", "");
}});
params.put("listObject", Lists.newArrayList(
new HashMap<String, Object>() {{
put("elemKey", "elem1");
}}, new HashMap<String, Object>() {{
put("elemKey", "elem2");
}}
));
String signatureSourceString = buildSignatureSourceString(params);
System.out.println(signatureSourceString);
}
/**
* Generate the text to be signed.
*/
public static String buildSignatureSourceString(Map<String, Object> params) {
StringBuilder content = new StringBuilder();
append(content, params);
return content.toString();
}
private static void append(StringBuilder 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 the value is a scalar.
*/
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;
}
}
Sign the text
Use your private key to sign the text to be signed. After you obtain the signature value, you need to pass it in the HTTP headers:
- To respond to a request from SHOPLINE, include the signature value in the response header
pay-api-signature. For specific field information, refer to Payment request. - To proactively notify SHOPLINE, include the signature value in the request header
signature. For specific field information, refer to Payment status notification.
Sample code
Java:
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import org.apache.commons.codec.binary.Base64;
/**
* Demo for signing
*/
public class SignatureDemo {
private static String priKey ="Your private key";
public static final String KEY_ALG_NAME = "RSA";
public static final String SIGN_ALG_NAME = "SHA1withRSA";
public static void main(String[] args) {
String signatureSourceString = "emptyKey=list=elem1,elem2&elemKey=elem1&elemKey=elem2&normalKey=normalValue&emptyObjectKey=&objectKey=objectValue";
String signature = signWithPrivateKey(priKey, signatureSourceString);
System.out.println(signature);
}
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(Base64.encodeBase64(signed));
} catch (Exception ex) {
throw new RuntimeException(ex.getMessage(), ex);
}
}
private static byte[] decodeBase64(String sourceStr) {
return Base64.decodeBase64(sourceStr.getBytes(StandardCharsets.UTF_8));
}
}
Verify the signature
Use the public key of SHOPLINE to verify the signature against the source payload.
SHOPLINE will include the signature value in the request header pay-api-signature when sending requests to the payment app. For specific field information, refer to Payment request.
During signature verification, you must make sure that all data passed by SHOPLINE has been correctly assembled into the text to be signed. You must verify the signature using the raw message from SHOPLINE before parsing it into a structured format for business logic. Otherwise, the verification will fail.
To ensure this, SHOPLINE adds random key-value pairs to the request, guaranteeing that the verification uses the original, unaltered message from SHOPLINE. This mechanism ensures that when SHOPLINE later adds new parameters to its APIs (new parameters will always be included in the signature), payment apps that have integrated the APIs but have not upgraded their own code will not fail signature verification.
Sample code
Java:
package com.example.demo_jdk8.sign.demo01;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
/**
* Demo for signature verification
*/
@Slf4j
public class CheckSignDemo {
public static final String SIGN_ALG_NAME = "SHA1withRSA";
public static final String KEY_ALG_NAME = "RSA";
public static void main(String[] args) {
String publicKey = "SHOPLINE's public key";
String signatureSourceString = "Text to be signed that is assembled from SHOPLINE's response";
String signature = "Signature value returned by SHOPLINE";
boolean check = checkSignWithPublicKey(publicKey, signatureSourceString, signature);
System.out.println(check);
}
public static boolean checkSignWithPublicKey(String publicKey, String signSourceStr, String signedStr) {
try {
X509EncodedKeySpec bobPubKeySpec = new X509EncodedKeySpec(decodeBase64(publicKey));
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALG_NAME);
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 Base64.decodeBase64(sourceStr.getBytes(StandardCharsets.UTF_8));
}
}
Examples
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);
}
public static String signature(String privateKey, Map<String, Object> params) {
String signatureSource = buildSignatureSourceString(params);
return signWithPrivateKey(privateKey, signatureSource);
}
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);
}
}
}
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 {
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;
}
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;
}
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;
}
}