Java Implementation for OpenAPI Signature and Verification
This document provides a detailed example of how to implement signature generation and verification for OpenAPI requests using Java. The implementation uses ECDSA signatures, which is a secure cryptographic algorithm. You can also refer to the Node.js Demo for signing a message with ECDSA algorithm using Node.js.
Required Maven Dependencies
Add the following dependencies to your pom.xml file:
<dependency>
<groupId>org.bitcoinj</groupId>
<artifactId>bitcoinj-core</artifactId>
<version>0.16.3</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
Complete Implementation Code
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Utils;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
/**
* Utility class for OpenAPI signature generation and verification.
* This implementation follows the ECDSA signature standard.
*/
@Slf4j
public class OpenApiSignatureUtil {
// Replace these values with your actual credentials
private static final String API_KEY = "your_actual_api_key_here";
private static final String PRIVATE_KEY_HEX = "your_actual_private_key_hex_here";
private static final String PUBLIC_KEY_HEX = "your_actual_public_key_hex_here";
private static final String OPEN_API_BASE_URL = "https://api.example.com";
/**
* Builds the source string for signature generation.
* The parameters are sorted lexicographically by key and formatted as "key=value&key=value".
*
* @param params The map of parameters to include in the signature
* @return The formatted source string for signing
*/
public static String buildSignSource(Map<String, Object> params) {
// Use TreeMap to automatically sort parameters by key
TreeMap<String, Object> sortedMap = new TreeMap<>(params);
StringBuilder sb = new StringBuilder();
// Append each key-value pair in the format "key=value"
for (Map.Entry<String, Object> entry : sortedMap.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
// Skip null values, empty strings, and the 'Sign' parameter itself
if (value != null && !value.toString().isEmpty() && !"Sign".equals(key)) {
sb.append(key).append("=").append(value.toString()).append("&");
}
}
// Remove the trailing '&' if present
if (sb.length() > 0) {
sb.deleteCharAt(sb.length() - 1);
}
return sb.toString();
}
/**
* Generates an ECDSA signature using the provided private key.
* The private key should be in HEX format, and the result will be DER-encoded and Base64-encoded.
*
* @param privateKeyHex HEX-encoded private key
* @param message The data to sign
* @return Base64-encoded signature result
* @throws Exception If signature generation fails
*/
public static String sign(String privateKeyHex, String message) throws Exception {
// Input validation
if (StrUtil.isBlank(privateKeyHex) || StrUtil.isBlank(message)) {
throw new IllegalArgumentException("Private key and message to sign cannot be empty");
}
try {
// 1. Decode the HEX private key
byte[] privateKeyBytes = Utils.HEX.decode(privateKeyHex);
// 2. Load the private key
ECKey ecKey = ECKey.fromPrivate(privateKeyBytes);
// 3. Generate ECDSA signature
String signature = ecKey.signMessage(message);
return signature;
} catch (Exception e) {
throw new Exception("Signature generation failed: " + e.getMessage(), e);
}
}
/**
* Verifies an ECDSA signature using the provided public key.
* The public key should be in HEX format, and the signature should be DER-encoded and Base64-encoded.
*
* @param publicKeyHex HEX-encoded public key
* @param message The original data that was signed
* @param signedBase64 Base64-encoded signature to verify
* @return True if the signature is valid, false otherwise
*/
public static Boolean verifySignature(String publicKeyHex, String message, String signedBase64) {
// Parse public key from HEX string
ECKey publicKey = ECKey.fromPublicOnly(Utils.HEX.decode(publicKeyHex));
try {
// Verify the signature
boolean isValid = publicKey.verifyMessage(message, signedBase64);
return isValid;
} catch (Exception ex) {
log.error("Signature verification failed", ex);
return false;
}
}
/**
* Demonstrates how to sign and send an OpenAPI request.
* This example uses the /open-api/v1/public/getTokenAssetList endpoint.
*
* @return A map containing the signature source and generated signature
*/
public static Map<String, Object> signAndSendRequest() {
try {
log.info("-----------Starting API Request----------");
// 1. Prepare request body parameters
Map<String, Object> bodyParams = new HashMap<>();
bodyParams.put("currentPage", 1);
bodyParams.put("pageSize", 15);
String jsonBody = JSONUtil.toJsonStr(bodyParams);
// 2. Build signature parameters
String apiKey = API_KEY;
String timestampStr = Long.toString(System.currentTimeMillis() / 1000); // Unix timestamp in seconds
String nonce = UUID.randomUUID().toString().replace("-", ""); // UUID without hyphens
Map<String, Object> params = new HashMap<>();
// Add header parameters to the signature
params.put("API-Key", apiKey);
params.put("Timestamp", timestampStr);
params.put("Nonce", nonce);
// Add body parameters to the signature
params.putAll(bodyParams);
// Generate the source string for signing
String signSource = buildSignSource(params);
log.info("Signature source generated: signSource={}", signSource);
// 3. Generate ECDSA signature
String privateKeyHex = PRIVATE_KEY_HEX;
String signature = sign(privateKeyHex, signSource);
log.info("Signature generated: signature={}", signature);
// 4. Build and send the HTTP request
String openApiUrl = OPEN_API_BASE_URL + "/open-api/v1/public/getTokenAssetList";
HttpResponse response = HttpRequest.post(openApiUrl)
.header("Content-Type", "application/json;charset=UTF-8")
.header("Sign", signature) // Add signature to header
.header("API-Key", apiKey)
.header("Timestamp", timestampStr)
.header("Nonce", nonce)
.body(jsonBody)
.timeout(3000)
.execute();
// 5. Check response status
int statusCode = response.getStatus();
if (statusCode < 200 || statusCode >= 300) {
log.error("Request failed with status code: {}, response: {}", statusCode, response.body());
}
log.info("Request completed successfully: response={}", response.body());
// 6. Return signature source and signature for verification demonstration
Map<String, Object> returnMap = new HashMap<>();
returnMap.put("signSource", signSource);
returnMap.put("signature", signature);
return returnMap;
} catch (Exception e) {
log.error("Request failed: {}", e.getMessage(), e);
return null;
}
}
/**
* Demonstrates how to verify a signature.
*
* @param signData A map containing the signature source and signature to verify
*/
public static void verifyRequestSignature(Map<String, Object> signData) {
try {
log.info("-----------Starting Signature Verification----------");
// 1. Extract signature source and signature from the provided data
String signSource = (String) signData.get("signSource");
log.info("Signature source to verify: signSource={}", signSource);
String signature = (String) signData.get("signature");
log.info("Signature to verify: signature={}", signature);
// 2. Get public key for verification
String publicKeyHex = PUBLIC_KEY_HEX;
Boolean result = verifySignature(publicKeyHex, signSource, signature);
if (result) {
log.info("Signature verification passed");
} else {
log.info("Signature verification failed");
}
} catch (Exception e) {
log.error("Signature verification error: {}", e.getMessage(), e);
}
}
/**
* Main method to demonstrate the complete flow of signing a request and verifying the signature.
*
* @param args Command line arguments (not used)
*/
public static void main(String[] args) {
// Sign and send an API request
Map<String, Object> signData = signAndSendRequest();
// Verify the generated signature
if (signData != null) {
verifyRequestSignature(signData);
}
}
}
Usage Instructions
Replace Placeholder Values:
- Replace
API_KEYwith your actual application's API key - Replace
PRIVATE_KEY_HEXwith your actual private key in HEX format - Replace
PUBLIC_KEY_HEXwith your actual public key in HEX format - Replace
OPEN_API_BASE_URLwith the actual base URL of the OpenAPI service
- Replace
Customize Request Parameters:
- Modify the
bodyParamsin thesignAndSendRequest()method to match the parameters required by your specific API endpoint - Update the endpoint URL in the
openApiUrlvariable
- Modify the
Handle API Responses:
- The current implementation logs the response body
- You should parse and process the response according to your application's needs
Security Considerations
Private Key Management:
- Store private keys securely and never expose them in client-side code or logs
- Consider using a secure key management service for production environments
Nonce Generation:
- The example uses UUID.randomUUID() for nonce generation
- Ensure that each request has a unique nonce to prevent replay attacks
Timestamp Validation:
- While this example doesn't demonstrate timestamp validation on the client side, the server may reject requests with timestamps that are significantly different from the current time
Error Handling:
- Implement robust error handling for signature generation and verification failures
- Properly handle API errors and timeouts
Troubleshooting
Signature Verification Failures:
- Check if the public key matches the corresponding private key
- Ensure the same parameters and order are used for both signing and verification
- Verify that the nonce and timestamp are identical to what was used during signing
API Request Failures:
- Verify that the API key is valid and has the necessary permissions
- Check network connectivity to the API server
- Ensure the request parameters comply with the API documentation
Dependency Issues:
- If you encounter version conflicts with bitcoinj-core, try adjusting the version
- Ensure all required dependencies are properly included in your build file