跳至主要内容

Signature Algorithm

Overview​

All Agreement Payment APIs use RSA2 (SHA256withRSA) signature for authentication. This system only supports RSA2 signature algorithm.


Signature Algorithm​

AlgorithmDescription
RSA2RSA-SHA256 with 2048-bit key (signType=RSA2)

Notes:

  • Only RSA2 signature algorithm (SHA256withRSA) is supported; HMAC-SHA256 is NOT supported
  • signType is fixed as RSA2
  • Private key supports both PKCS1 and PKCS8 PEM formats:
    • PKCS1 format: -----BEGIN RSA PRIVATE KEY-----
    • PKCS8 format: -----BEGIN PRIVATE KEY-----

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​

  1. Get signature-related parameters from HTTP request headers: X-Timestamp, X-Signature, X-Nonce, X-Sign-Type
  2. Verify timestamp: Check if X-Timestamp is within 5 minutes of current server time (prevent replay attack)
  3. Construct content to verify: timestamp + nonce + requestBody (string concatenation, not JSON format)
  4. Request body is pure JSON: Contains only business fields (notifyId, notifyType, notifyTime, merchantId, data), no signature fields need to be removed
  5. 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 TypePurposeCustodianFormat
Merchant Private KeySign API requests (RSA2)Merchant (strictly confidential)PEM (PKCS1 or PKCS8)
Merchant Public KeyPlatform verifies merchant requestsPlatformPEM (X.509)
Platform Private KeySign webhook notifications (RSA2)PlatformPEM
Platform Public KeyMerchant verifies webhooksMerchantPEM (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​

  1. Store private keys encrypted; never store in plaintext
  2. Rotate keys periodically (recommended annually)
  3. Contact platform immediately if key is compromised
  4. Use different keys for test and production environments
  5. PKCS8 format is recommended (more universal); if using PKCS1 format, BouncyCastle library is required