Signature Algorithm
Overview
All Agreement Payment APIs use RSA2 (SHA256withRSA) signature for authentication. This system only supports RSA2 signature algorithm.
Signature Algorithm
| Algorithm | Description |
|---|---|
| RSA2 | RSA-SHA256 with 2048-bit key (signType=RSA2) |
Notes:
- Only RSA2 signature algorithm (SHA256withRSA) is supported; HMAC-SHA256 is NOT supported
signTypeis fixed asRSA2- Private key supports both PKCS1 and PKCS8 PEM formats:
- PKCS1 format:
-----BEGIN RSA PRIVATE KEY----- - PKCS8 format:
-----BEGIN PRIVATE KEY-----
- PKCS1 format:
Request Signature
Signature Flow
1. Construct String to Sign
String to sign = HTTP Method + "\n" + Request Path + "\n" + Timestamp + "\n" + Request Body
Example:
POST
/v5/pay/agreement/deduction
1703318400000
{"merchantId":"M123456789","userId":"U_123456789",...}
2. Calculate Signature
Signature = Base64(RSA_SHA256_Sign(String to sign, Merchant Private Key))
3. Set Request Headers
X-Request-Id: 550e8400-e29b-41d4-a716-446655440000
X-Timestamp: 1703318400000
X-Signature: Base64 encoded signature value
Authorization: Bearer {access_token}
Code Examples
Java (RSA2)
Supports both PKCS1 and PKCS8 private key formats:
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
public class SignatureUtil {
/**
* RSA2 Signature (SHA256withRSA)
*/
public static String sign(String content, String privateKeyStr) throws Exception {
PrivateKey privateKey = loadPrivateKey(privateKeyStr);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(content.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(signature.sign());
}
/**
* Load private key (auto-detect PKCS1 and PKCS8 formats)
*/
private static PrivateKey loadPrivateKey(String privateKeyStr) throws Exception {
if (privateKeyStr.contains("BEGIN PRIVATE KEY")) {
String keyContent = privateKeyStr
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
byte[] keyBytes = Base64.getDecoder().decode(keyContent);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
} else if (privateKeyStr.contains("BEGIN RSA PRIVATE KEY")) {
PEMParser pemParser = new PEMParser(new java.io.StringReader(privateKeyStr));
Object object = pemParser.readObject();
pemParser.close();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
if (object instanceof PEMKeyPair) {
return converter.getPrivateKey(((PEMKeyPair) object).getPrivateKeyInfo());
}
throw new IllegalArgumentException("Invalid PKCS1 private key");
}
throw new IllegalArgumentException("Unsupported private key format");
}
/**
* Construct request signature
*/
public static String signRequest(String method, String path,
long timestamp, String body,
String privateKeyStr) throws Exception {
String content = method + "\n" + path + "\n" + timestamp + "\n" + body;
return sign(content, privateKeyStr);
}
}
Maven Dependency (BouncyCastle, for PKCS1 format support):
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>
Python (RSA2)
import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
def sign(method, path, timestamp, body, private_key):
content = f"{method}\n{path}\n{timestamp}\n{body}"
signature = private_key.sign(
content.encode('utf-8'),
padding.PKCS1v15(),
hashes.SHA256()
)
return base64.b64encode(signature).decode('utf-8')
cURL Example
#!/bin/bash
API_HOST="https://api.bybit.com"
API_PATH="/v5/pay/agreement/deduction"
MERCHANT_PRIVATE_KEY="./merchant_private_key.pem"
ACCESS_TOKEN="your_access_token"
REQUEST_BODY='{
"merchantId": "M123456789",
"userId": "U_123456789",
"agreementType": "CYCLE",
"agreementNo": "AGR202312230001",
"outTradeNo": "TAXI20231224001",
"sceneCode": "TAXI",
"amount": {
"total": "2350",
"currency": "USDT",
"currencyType": "CRYPTO",
"chain": "TRC20"
},
"orderInfo": {
"orderTitle": "Ride fare"
},
"notifyUrl": "https://merchant.com/notify/pay"
}'
TIMESTAMP=$(date +%s000)
REQUEST_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
SIGN_CONTENT="POST
${API_PATH}
${TIMESTAMP}
${REQUEST_BODY}"
SIGNATURE=$(echo -n "$SIGN_CONTENT" | openssl dgst -sha256 -sign "$MERCHANT_PRIVATE_KEY" | base64 | tr -d '\n')
curl -X POST "${API_HOST}${API_PATH}" \
-H "Content-Type: application/json" \
-H "X-Request-Id: ${REQUEST_ID}" \
-H "X-Timestamp: ${TIMESTAMP}" \
-H "X-Signature: ${SIGNATURE}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-d "${REQUEST_BODY}"
Webhook Signature Verification
Merchant needs to verify signature after receiving Webhook notification to prevent forged requests.
Verification Steps
- Get signature-related parameters from HTTP request headers:
X-Timestamp,X-Signature,X-Nonce,X-Sign-Type - Verify timestamp: Check if
X-Timestampis within 5 minutes of current server time (prevent replay attack) - Construct content to verify:
timestamp + nonce + requestBody(string concatenation, not JSON format) - Request body is pure JSON: Contains only business fields (notifyId, notifyType, notifyTime, merchantId, data), no signature fields need to be removed
- Use platform public key to verify signature (RSA2/SHA256withRSA)
Java Example
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class WebhookVerifier {
/**
* Verify Webhook signature
* @param body Original request body JSON string (pure JSON, only business fields)
* @param timestamp X-Timestamp from request header
* @param nonce X-Nonce from request header
* @param signature X-Signature from request header
* @param platformPublicKey Platform public key (PEM format)
*/
public boolean verifyWebhook(String body, String timestamp, String nonce,
String signature, String platformPublicKey) throws Exception {
// 1. Check timestamp (prevent replay attack)
long now = System.currentTimeMillis();
long ts = Long.parseLong(timestamp);
if (Math.abs(now - ts) > 5 * 60 * 1000) {
return false;
}
// 2. Construct content to verify: timestamp + nonce + requestBody
String contentToVerify = timestamp + nonce + body;
// 3. Use platform public key to verify RSA2 signature (SHA256withRSA)
return verifyRSA2(contentToVerify, signature, platformPublicKey);
}
private boolean verifyRSA2(String content, String signBase64,
String publicKeyStr) throws Exception {
String keyContent = publicKeyStr
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s+", "");
byte[] keyBytes = Base64.getDecoder().decode(keyContent);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(content.getBytes("UTF-8"));
return signature.verify(Base64.getDecoder().decode(signBase64));
}
}
Key Management
| Key Type | Purpose | Custodian | Format |
|---|---|---|---|
| Merchant Private Key | Sign API requests (RSA2) | Merchant (strictly confidential) | PEM (PKCS1 or PKCS8) |
| Merchant Public Key | Platform verifies merchant requests | Platform | PEM (X.509) |
| Platform Private Key | Sign webhook notifications (RSA2) | Platform | PEM |
| Platform Public Key | Merchant verifies webhooks | Merchant | PEM (X.509) |
Private Key Formats
# PKCS1 format (traditional RSA format)
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...(Base64 encoded content)
-----END RSA PRIVATE KEY-----
# PKCS8 format (PKCS#8 standard format, recommended)
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkq...(Base64 encoded content)
-----END PRIVATE KEY-----
Security Requirements
- Store private keys encrypted; never store in plaintext
- Rotate keys periodically (recommended annually)
- Contact platform immediately if key is compromised
- Use different keys for test and production environments
- PKCS8 format is recommended (more universal); if using PKCS1 format, BouncyCastle library is required