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

  1. Replace Placeholder Values:

    • Replace API_KEY with your actual application's API key
    • Replace PRIVATE_KEY_HEX with your actual private key in HEX format
    • Replace PUBLIC_KEY_HEX with your actual public key in HEX format
    • Replace OPEN_API_BASE_URL with the actual base URL of the OpenAPI service
  2. Customize Request Parameters:

    • Modify the bodyParams in the signAndSendRequest() method to match the parameters required by your specific API endpoint
    • Update the endpoint URL in the openApiUrl variable
  3. 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

  1. 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
  2. Nonce Generation:

    • The example uses UUID.randomUUID() for nonce generation
    • Ensure that each request has a unique nonce to prevent replay attacks
  3. 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
  4. Error Handling:

    • Implement robust error handling for signature generation and verification failures
    • Properly handle API errors and timeouts

Troubleshooting

  1. 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
  2. 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
  3. 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

results matching ""

    No results matching ""