--- url: 'https://acquirer-api-docs-v4-en.pingpongx.com/en/notes/guide/sign/index.md' description: >- The signature convention defines the request signature generation and verification rules for PingPongCheckout API v4 to ensure data security. Applicable to all scenarios using this API for payment processing. Supports both MD5 and SHA256 signature algorithms, with the more secure SHA256 recommended. Details the assembly of the string to be signed, signature value calculation steps, and provides example code for signature utility classes. --- # Signature Convention PingPongCheckout API v4 ensures the authenticity of requests and integrity of data by verifying signatures. ::: warning In PingPongCheckout API v4, `accId`, `clientId`, `signType`, `version`, and `bizContent` all participate in the signature. If you were previously integrating with v2 or v3 versions and need to upgrade to v4, you must integrate according to the new signature rules. ::: ## Signature Types | Signature Type | Description | |:---------------|:------------| | MD5 | Indicates selection of MD5 algorithm, merchants use Salt to perform message digest signing and verification | | SHA256 | Indicates selection of SHA256 algorithm, merchants use Salt to perform message digest signing and verification | ## Assembly of String to be Signed 1. Obtain all POST request content, excluding the sign field; 2. Sort by the first character's key ASCII code in ascending order (alphabetical ascending order); 3. Combine the sorted parameters with their corresponding values in the format Parameter=ParameterValue, then connect these parameters with & characters, and place the signature secret (salt) at the beginning of the string to be signed, i.e., signContent = {salt}key1=val2&key2=val2&key3=val3, at which point the complete string to be signed is obtained; ::: warning Important Notice **Please avoid passing special characters in request parameters** The current system will trigger XSS protection interception for requests containing `<` and `>`, causing the request to be rejected. Additionally, although the system does not actively intercept the following special characters, considering that upstream channels, card organizations, and banks may have different risk control rules, **it is strongly recommended to avoid using these characters in business**: `\` `"` `'` `{` `}` `[` `]` `:` `,` `<` `>` `&` `/` `(` `)` `;` `=` `%` `+` `\n` `\r` To ensure transaction success rate and stability, please ensure that the merchant system validates and cleans parameter values at the **business layer** before initiating requests, avoiding including the above special characters. ::: ## Calculating Signature Value > It is recommended to use SHA256 signature method, which has higher security than MD5 The signature process generates a signature value based on the data in the request body and adds it to the request body. The signature value is a string calculated using salt, request parameters, and signature method, depending on the specific signature method (MD5 or SHA256). The signature value will be used to verify the source and integrity of the request. ## Signature Utility Class # Code Examples ::: code-tabs @tab ☕ JAVA ```java:line-numbers import com.alibaba.fastjson.JSONObject; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * This class is used to sign request content to ensure request security. */ public class PingPongCheckoutClient { private final String salt; // Salt value, used to increase signature complexity private final SignAlgorithm signAlgorithm; // Signature algorithm enumeration /** * Constructor, used to initialize salt value and signature algorithm. * * @param salt Salt value * @param signAlgorithm Signature algorithm */ // [!code highlight:4] public PingPongCheckoutClient(String salt, SignAlgorithm signAlgorithm) { this.salt = salt; this.signAlgorithm = signAlgorithm; } /** * Signs the request content and adds the signature result to the request parameters. * * @param requestBody Request content * @return Request parameters with added signature result */ // [!code highlight:5] public JSONObject signRequest(JSONObject requestBody) { String sign = getSign(salt, signAlgorithm, requestBody); // [!code focus] requestBody.put("sign", sign); // [!code focus] return requestBody; } /** * Gets the signature result of the request content. * * @param salt Salt value * @param signAlgorithm Signature algorithm * @param requestBody Request content * @return Signature result of the request content */ public static String getSign(String salt, SignAlgorithm signAlgorithm, JSONObject requestBody) { StringBuilder stringBuilder = new StringBuilder(); List keys = new ArrayList<>(requestBody.keySet()); Collections.sort(keys); // Sort request parameter keys in ascending order for (String key : keys) { Object valueObject = requestBody.get(key); // Exclude null values if (valueObject == null) { continue; } // Exclude non-string type values if (!(valueObject instanceof String)) { throw new IllegalArgumentException("request body illegal"); } String value = (String) valueObject; if (StringUtils.isNotBlank(value)) { stringBuilder.append(key).append("=").append(value).append("&"); // Concatenate request parameter key and value into string } } String needSignStr = stringBuilder.toString(); if (needSignStr.endsWith("&")) { needSignStr = needSignStr.substring(0, needSignStr.length() - 1); // Remove last & symbol } String sign = null; // [!code highlight:7] if (signAlgorithm == SignAlgorithm.MD5) { sign = md5Sign(salt, needSignStr); // [!code focus] } else if (signAlgorithm == SignAlgorithm.SHA256) { sign = sha256(salt, needSignStr); // [!code focus] } else { throw new IllegalArgumentException("Signature algorithm not supported"); // [!code error] } return sign; // [!code focus] } /** * Performs MD5 signature on request content. * * @param salt Salt value * @param content Request content * @return MD5 signature result of request content */ // [!code highlight:11] public static String md5Sign(String salt, String content) { try { MessageDigest md = MessageDigest.getInstance("MD5"); // [!code focus] md.update(salt.getBytes()); // [!code focus] md.update(content.getBytes()); // [!code focus] byte[] digest = md.digest(); return byteToHexString(digest); // [!code focus] } catch (Exception e) { throw new RuntimeException("md5 signature failed", e); // [!code error] } } /** * Converts byte array to hexadecimal string. * * @param bytes Byte array * @return Hexadecimal string */ public static String byteToHexString(byte[] bytes) { StringBuilder hexString = new StringBuilder(); for (int i = 0; i < bytes.length; i++) { String hex = Integer.toHexString(bytes[i] & 0xFF); if (hex.length() == 1) { hex = '0' + hex; } hexString.append(hex.toUpperCase()); // Concatenate converted hexadecimal strings } return hexString.toString(); // Return converted hexadecimal string } /** * Performs SHA256 signature on request content. * * @param salt Salt value * @param content Request content * @return SHA256 signature result of request content */ // [!code highlight:7] public static String sha256(String salt, String content) { try { String contentStr = salt.concat(content); // [!code focus] return DigestUtils.sha256Hex(contentStr.getBytes("UTF-8")).toUpperCase(); // [!code focus] } catch (Exception e) { throw new RuntimeException("sha256 signature failed", e); // [!code error] } } /** * Signature algorithm enumeration class. */ public enum SignAlgorithm { MD5, SHA256 } } ``` @tab 🐘 PHP ```php:line-numbers validate($requestBody); // Concatenate request parameters $signContent = $this->signContent($salt, $requestBody); // [!code focus] $sign = $this->getSign($signContent, $requestBody); // [!code focus] $requestBody['sign'] = strtoupper($sign); // [!code focus] if (is_array($requestBody['bizContent'])) { $requestBody['bizContent'] = json_encode($requestBody['bizContent'], JSON_UNESCAPED_SLASHES); } return $requestBody; } /** * Concatenate request parameters * * @param string $salt * @param array $requestBody * @return array|string */ public function signContent(string $salt, array $requestBody) { unset($requestBody['sign']); if (is_array($requestBody['bizContent'])) { $requestBody['bizContent'] = json_encode($requestBody['bizContent'],JSON_UNESCAPED_SLASHES); } // Sort by key ksort($requestBody); $signContent = urldecode(http_build_query($requestBody)); $signContent = $salt . $signContent; return $signContent; } /** * Gets the signature result of the request content. * * @param string $signContent * @param array $requestBody * @return array|string */ public function getSign(string $signContent, array $requestBody) { switch ($requestBody['signType']) { case "MD5": $sign = $this->md5Sign($signContent); break; case "SHA256": $sign = $this->sha256($signContent); break; } return $sign; } /** * md5 encryption * * @param string $signContent * @return string */ // [!code highlight:4] public function md5Sign(string $signContent) { return md5($signContent); // [!code focus] } /** * sha256 encryption * * @param string $signContent * @return string */ // [!code highlight:4] public function sha256(string $signContent) { return hash("sha256", $signContent); // [!code focus] } /** * Validates the signature parameters involved * * @param array $requestBody * @return true * @throws \Exception */ public function validate(array $requestBody) { $keys = ['accId', 'clientId', 'signType', 'version', 'bizContent']; foreach ($keys as $key) { if (!isset($requestBody[$key]) || empty($requestBody[$key])) { throw new \Exception('invalid -' . $key); } } if (!in_array($requestBody['signType'], self::SignAlgorithm)) { throw new \Exception('invalid - signType' ); } if (!is_array($requestBody['bizContent']) && !is_string($requestBody['bizContent'])) { throw new \Exception('invalid - bizContent' ); } return true; } } ``` ::: ## Signature Verification Utility Class ```php:line-numbers title="Service/PingPongCheckoutDecrypt.php" signContent($salt, $requestBody); // [!code focus] $sign = $this->getSign($signData['signContent'], $signData['data']); // [!code focus] if ($signData['sign'] == strtoupper($sign)) { // [!code focus] return true; } else { return false; } } /** * Concatenate request parameters * * @param string $salt * @param array $requestBody * @return array|string */ // [!code highlight:26] public function signContent(string $salt, string $requestBody) { // Replace special characters $requestBody = str_replace("\\n", '\\\\n', $requestBody); $requestBody = str_replace("\\r", '\\\\r', $requestBody); $requestBody = str_replace("\\f", '\\\\f', $requestBody); $requestBody = str_replace("\\t", '\\\\t', $requestBody); $requestBody = str_replace("\\v", '\\\\v', $requestBody); $requestBody = str_replace('\\\\"', '\\\\\\"', $requestBody); $data = json_decode($requestBody,true); // [!code focus] if (!$data) { if (function_exists('json_last_error') && json_last_error() != 0) { $error = json_last_error_msg(); } else { $error = 'data does not meet the requirements'; } throw new \Exception($error); // [!code error] } $sign = $data['sign']; unset($data['sign']); // [!code focus] // Sort by key ksort($data); // [!code focus] $signContent = urldecode(http_build_query($data)); $signContent = $salt . $signContent; // [!code focus] return ['sign' => $sign, 'data' => $data, 'signContent' => $signContent]; } /** * Gets the signature result of the request content. * * @param string $signContent * @param array $requestBody * @return array|string */ public function getSign(string $signContent, array $requestBody) { switch ($requestBody['signType']) { case "MD5": $sign = $this->md5Sign($signContent); break; case "SHA256": $sign = $this->sha256($signContent); break; } return $sign; } /** * md5 encryption * * @param string $signContent * @return string */ public function md5Sign(string $signContent) { return md5($signContent); } /** * sha256 encryption * * @param string $signContent * @return string */ public function sha256(string $signContent) { return hash("sha256", $signContent); } } ```