Signature
DaxPay Payment API uses RSA asymmetric encryption for signing. Requests, responses, and callbacks are all signed to prevent tampering.
Key System
| Key | Held By | Used For |
|---|---|---|
| Merchant private key | Merchant server | Signing requests |
| Merchant public key | DaxPay platform | Platform verifies requests |
| Platform private key | DaxPay platform | Signing responses/callbacks |
| Platform public key | Merchant server | Merchant verifies responses/callbacks |
Key Generation
Use 2048-bit RSA keys. Generate with OpenSSL or Java KeyPairGenerator.
Signature Field Location
Signature-related fields are in the request/response JSON body, not in HTTP headers:
| Field | Location | Description |
|---|---|---|
| sign | JSON body | Signature value (Base64) |
| reqId | JSON body | Unique request identifier |
| nonceStr | JSON body | Random string |
| reqTime | JSON body | Request time (yyyy-MM-dd HH:mm:ss, Beijing time) |
Time field format (the most common cause of signature failures)
The platform builds and verifies the sign string against the literal values in the message, so time fields must match the canonical literal exactly: Beijing time yyyy-MM-dd HH:mm:ss (no timezone suffix, no sub-second fraction).
| Literal | Usable | Note |
|---|---|---|
2024-12-01 12:00:00 | ✅ | Canonical literal (all examples in this documentation use it) |
2024-12-01T04:00:00Z | ❌ | ISO 8601 form, differs from the canonical literal → 20052 |
2024-12-01 12:00:00.123 | ❌ | Sub-second precision is not part of the canonical literal → 20052 |
Time fields in responses and async notifications (resTime, payTime, finishTime, ...) use the same Beijing time format — just verify using the literal values from the JSON.
Signing Steps
1. Build sign string
- Get all fields from the request JSON (including flattened nested objects).
- Exclude the
signfield. - Exclude empty values (null or empty string).
- Sort remaining fields by key in ASCII order.
- Join as
key1=value1&key2=value2.
Nested object & list flattening
- Nested objects use dot notation:
terminal.terminalNo=value - List elements use bracket index:
limitPay[0]=value - Flattened entries participate in sorting and joining
2. RSA sign
Sign the string with merchant private key:
- Algorithm:
SHA256withRSA - Encoding: UTF-8
- Output: Base64 string
3. Set sign field
Set the Base64 result as the sign field in the request JSON.
Response Verification
Use platform public key to verify the sign field:
- Get all fields from the response JSON.
- Remove
sign. - Sort and join the same way as request signing.
- Verify with platform public key using
SHA256withRSA.
Code Examples
Java — Sign Request
import java.security.*;
import java.util.*;
import java.util.Base64;
// 1. Build params (TreeMap auto-sorts by key)
Map<String, String> params = new TreeMap<>();
params.put("appId", "APP001");
params.put("bizOrderNo", "ORDER20241201001");
params.put("method", "wechat_qr");
params.put("mchNo", "M200000001");
params.put("nonceStr", "5K8264ILTKCH16CQ2502SI8ZNMTM67VS");
params.put("notifyUrl", "https://your-domain.com/notify");
params.put("amount", "100");
params.put("reqId", "REQ20241201001");
params.put("reqTime", "2024-12-01 12:00:00");
params.put("title", "Test Product");
// 2. Build sign string (exclude sign, ASCII sorted)
String signStr = params.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
// 3. RSA sign
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey); // merchant private key
signature.update(signStr.getBytes(StandardCharsets.UTF_8));
String sign = Base64.getEncoder().encodeToString(signature.sign());Java — Verify Response/Callback
// 1. Remove sign, sort by key, join
String verifyStr = sortedParams.entrySet().stream()
.filter(e -> !"sign".equalsIgnoreCase(e.getKey()))
.filter(e -> e.getValue() != null && !e.getValue().isEmpty())
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
// 2. Verify with platform public key
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey); // platform public key
signature.update(verifyStr.getBytes(StandardCharsets.UTF_8));
byte[] signBytes = Base64.getDecoder().decode(receivedSign);
boolean valid = signature.verify(signBytes);
if (!valid) {
throw new RuntimeException("Signature verification failed");
}Rules Summary
| Rule | Description |
|---|---|
| Sort | ASCII ascending by key |
| Format | key1=value1&key2=value2 (no spaces, no escaping) |
| Empty values | Null or empty string fields excluded |
| sign excluded | The sign field itself is not included (case-insensitive) |
| Encoding | UTF-8 |
| Algorithm | SHA256withRSA |
| Output | Base64 string |
| Location | JSON body sign field (not HTTP header) |