Transaction signing is the cryptographic process of proving ownership and authorization for digital transactions. From blockchain operations to financial payments and legal documents, transaction signing ensures integrity, non-repudiation, and authenticity. Java's robust cryptographic libraries and platform independence make it ideal for implementing secure transaction signing systems.
Cryptographic Foundations
Key Concepts:
- Digital Signatures: Mathematical schemes for verifying authenticity
- Hash Functions: Create fixed-size fingerprints of data
- Asymmetric Cryptography: Public/private key pairs
- Nonce: Number used once to prevent replay attacks
- Timestamp: Prevents timing attacks
Core Transaction Signing Implementation
1. Basic Cryptographic Service
import java.security.*;
import java.security.spec.*;
import java.util.Base64;
public class CryptoService {
private static final String SIGNATURE_ALGORITHM = "SHA256withECDSA";
private static final String KEY_ALGORITHM = "EC";
private static final String CURVE_NAME = "secp256k1"; // Bitcoin/ETH curve
public KeyPair generateKeyPair() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
ECGenParameterSpec ecSpec = new ECGenParameterSpec(CURVE_NAME);
keyGen.initialize(ecSpec, new SecureRandom());
return keyGen.generateKeyPair();
}
public byte[] signTransaction(byte[] transactionData, PrivateKey privateKey)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(privateKey);
signature.update(transactionData);
return signature.sign();
}
public boolean verifySignature(byte[] transactionData, byte[] signatureBytes, PublicKey publicKey)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initVerify(publicKey);
signature.update(transactionData);
return signature.verify(signatureBytes);
}
public byte[] calculateHash(byte[] data) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(data);
}
// Key serialization/deserialization
public String publicKeyToBase64(PublicKey publicKey) {
return Base64.getEncoder().encodeToString(publicKey.getEncoded());
}
public PublicKey base64ToPublicKey(String base64Key) throws GeneralSecurityException {
byte[] keyBytes = Base64.getDecoder().decode(base64Key);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
return keyFactory.generatePublic(keySpec);
}
}
2. Transaction Data Structure
import java.time.Instant;
import java.util.*;
public class Transaction {
private final String transactionId;
private final String fromAddress;
private final String toAddress;
private final double amount;
private final String currency;
private final long nonce;
private final Instant timestamp;
private final Map<String, Object> metadata;
private byte[] signature;
public Transaction(String fromAddress, String toAddress, double amount, String currency) {
this.transactionId = UUID.randomUUID().toString();
this.fromAddress = fromAddress;
this.toAddress = toAddress;
this.amount = amount;
this.currency = currency;
this.nonce = generateNonce();
this.timestamp = Instant.now();
this.metadata = new HashMap<>();
}
private long generateNonce() {
return System.currentTimeMillis() + new SecureRandom().nextInt(1000);
}
public byte[] getDataToSign() {
try {
// Create deterministic representation for signing
String signingData = String.format("%s|%s|%s|%.8f|%s|%d|%d",
transactionId, fromAddress, toAddress, amount, currency, nonce, timestamp.getEpochSecond());
// Include metadata in deterministic way
if (!metadata.isEmpty()) {
String metadataString = metadata.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining("&"));
signingData += "|" + metadataString;
}
return signingData.getBytes(java.nio.charset.StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("Failed to prepare transaction data for signing", e);
}
}
public void addMetadata(String key, Object value) {
metadata.put(key, value);
}
public String toJson() {
Map<String, Object> jsonMap = new LinkedHashMap<>();
jsonMap.put("transactionId", transactionId);
jsonMap.put("fromAddress", fromAddress);
jsonMap.put("toAddress", toAddress);
jsonMap.put("amount", amount);
jsonMap.put("currency", currency);
jsonMap.put("nonce", nonce);
jsonMap.put("timestamp", timestamp.toString());
jsonMap.put("metadata", new HashMap<>(metadata));
if (signature != null) {
jsonMap.put("signature", Base64.getEncoder().encodeToString(signature));
}
return new com.google.gson.GsonBuilder()
.setPrettyPrinting()
.create()
.toJson(jsonMap);
}
public static Transaction fromJson(String json) {
// Implementation for deserialization
return null; // Simplified for example
}
// Getters and setters
public byte[] getSignature() { return signature; }
public void setSignature(byte[] signature) { this.signature = signature; }
public String getTransactionId() { return transactionId; }
public String getFromAddress() { return fromAddress; }
public String getToAddress() { return toAddress; }
public double getAmount() { return amount; }
public long getNonce() { return nonce; }
public Instant getTimestamp() { return timestamp; }
}
3. Transaction Signing Service
public class TransactionSigningService {
private final CryptoService cryptoService;
private final KeyStoreService keyStoreService;
public TransactionSigningService(CryptoService cryptoService, KeyStoreService keyStoreService) {
this.cryptoService = cryptoService;
this.keyStoreService = keyStoreService;
}
public SignedTransaction signTransaction(Transaction transaction, String keyAlias, char[] password) {
try {
// Validate transaction
validateTransaction(transaction);
// Get private key from secure storage
PrivateKey privateKey = keyStoreService.getPrivateKey(keyAlias, password);
// Get data to sign
byte[] dataToSign = transaction.getDataToSign();
// Create signature
byte[] signature = cryptoService.signTransaction(dataToSign, privateKey);
transaction.setSignature(signature);
// Create signed transaction object
return new SignedTransaction(transaction, signature);
} catch (Exception e) {
throw new TransactionSigningException("Failed to sign transaction", e);
}
}
public boolean verifyTransaction(SignedTransaction signedTransaction, PublicKey publicKey) {
try {
Transaction transaction = signedTransaction.getTransaction();
byte[] signature = signedTransaction.getSignature();
// Recreate the data that was signed
byte[] dataToVerify = transaction.getDataToSign();
// Verify the signature
return cryptoService.verifySignature(dataToVerify, signature, publicKey);
} catch (Exception e) {
throw new TransactionVerificationException("Failed to verify transaction", e);
}
}
public boolean verifyTransactionWithAddress(SignedTransaction signedTransaction, String expectedAddress) {
try {
// In blockchain contexts, we often verify against an address derived from public key
Transaction transaction = signedTransaction.getTransaction();
PublicKey publicKey = recoverPublicKey(signedTransaction);
// Verify signature first
if (!verifyTransaction(signedTransaction, publicKey)) {
return false;
}
// Verify the address matches expected
String derivedAddress = deriveAddressFromPublicKey(publicKey);
return derivedAddress.equals(expectedAddress);
} catch (Exception e) {
return false;
}
}
private PublicKey recoverPublicKey(SignedTransaction signedTransaction) {
// Implementation for public key recovery from signature
// This varies by cryptographic algorithm
throw new UnsupportedOperationException("Public key recovery not implemented");
}
private String deriveAddressFromPublicKey(PublicKey publicKey) {
// Implementation for address derivation (e.g., Ethereum, Bitcoin)
// This is algorithm-specific
throw new UnsupportedOperationException("Address derivation not implemented");
}
private void validateTransaction(Transaction transaction) {
if (transaction.getFromAddress() == null || transaction.getFromAddress().isEmpty()) {
throw new IllegalArgumentException("From address is required");
}
if (transaction.getToAddress() == null || transaction.getToAddress().isEmpty()) {
throw new IllegalArgumentException("To address is required");
}
if (transaction.getAmount() <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
if (transaction.getNonce() <= 0) {
throw new IllegalArgumentException("Invalid nonce");
}
// Check for replay attacks (nonce should be greater than last used nonce)
// This requires state management
}
}
public class SignedTransaction {
private final Transaction transaction;
private final byte[] signature;
private final Instant signedAt;
public SignedTransaction(Transaction transaction, byte[] signature) {
this.transaction = transaction;
this.signature = signature.clone(); // Defensive copy
this.signedAt = Instant.now();
}
public String toSignedJson() {
Map<String, Object> signedJson = new LinkedHashMap<>();
signedJson.put("transaction", new com.google.gson.Gson().fromJson(transaction.toJson(), Map.class));
signedJson.put("signature", Base64.getEncoder().encodeToString(signature));
signedJson.put("signedAt", signedAt.toString());
signedJson.put("signatureAlgorithm", "SHA256withECDSA");
return new com.google.gson.GsonBuilder()
.setPrettyPrinting()
.create()
.toJson(signedJson);
}
// Getters
public Transaction getTransaction() { return transaction; }
public byte[] getSignature() { return signature.clone(); } // Defensive copy
public Instant getSignedAt() { return signedAt; }
}
Advanced Signing Scenarios
1. Multi-Signature Transactions
public class MultiSigTransactionService {
private final CryptoService cryptoService;
private final int requiredSignatures;
public MultiSigTransactionService(CryptoService cryptoService, int requiredSignatures) {
this.cryptoService = cryptoService;
this.requiredSignatures = requiredSignatures;
}
public MultiSigTransaction createMultiSigTransaction(Transaction transaction,
List<PublicKey> participants) {
return new MultiSigTransaction(transaction, participants, requiredSignatures);
}
public void addSignature(MultiSigTransaction multiSigTx, byte[] signature, PublicKey signer) {
try {
// Verify the signature is valid
byte[] dataToSign = multiSigTx.getTransaction().getDataToSign();
if (!cryptoService.verifySignature(dataToSign, signature, signer)) {
throw new InvalidSignatureException("Invalid signature for multi-sig transaction");
}
// Check if signer is authorized
if (!multiSigTx.getParticipants().contains(signer)) {
throw new UnauthorizedSignerException("Signer not authorized for this transaction");
}
// Add signature
multiSigTx.addSignature(signature, signer);
} catch (Exception e) {
throw new MultiSigException("Failed to add signature to multi-sig transaction", e);
}
}
public boolean isFullySigned(MultiSigTransaction multiSigTx) {
return multiSigTx.getSignatures().size() >= requiredSignatures;
}
public byte[] generateAggregatedSignature(MultiSigTransaction multiSigTx) {
if (!isFullySigned(multiSigTx)) {
throw new InsufficientSignaturesException(
String.format("Required %d signatures, but only %d provided",
requiredSignatures, multiSigTx.getSignatures().size()));
}
// For simple aggregation, concatenate signatures
// In production, use proper threshold signature schemes like Schnorr or BLS
ByteArrayOutputStream aggregated = new ByteArrayOutputStream();
for (byte[] signature : multiSigTx.getSignatures().values()) {
try {
aggregated.write(signature);
} catch (IOException e) {
throw new MultiSigException("Failed to aggregate signatures", e);
}
}
return aggregated.toByteArray();
}
}
public class MultiSigTransaction {
private final Transaction transaction;
private final List<PublicKey> participants;
private final Map<PublicKey, byte[]> signatures;
private final int requiredSignatures;
public MultiSigTransaction(Transaction transaction, List<PublicKey> participants, int requiredSignatures) {
this.transaction = transaction;
this.participants = new ArrayList<>(participants);
this.signatures = new HashMap<>();
this.requiredSignatures = requiredSignatures;
if (requiredSignatures > participants.size()) {
throw new IllegalArgumentException("Required signatures cannot exceed number of participants");
}
}
public void addSignature(byte[] signature, PublicKey signer) {
if (!participants.contains(signer)) {
throw new UnauthorizedSignerException("Signer not in participant list");
}
signatures.put(signer, signature.clone()); // Defensive copy
}
public String toMultiSigJson() {
Map<String, Object> multiSigJson = new LinkedHashMap<>();
multiSigJson.put("transaction", new com.google.gson.Gson().fromJson(transaction.toJson(), Map.class));
Map<String, String> signatureMap = new HashMap<>();
for (Map.Entry<PublicKey, byte[]> entry : signatures.entrySet()) {
String publicKeyBase64 = Base64.getEncoder().encodeToString(entry.getKey().getEncoded());
String signatureBase64 = Base64.getEncoder().encodeToString(entry.getValue());
signatureMap.put(publicKeyBase64, signatureBase64);
}
multiSigJson.put("signatures", signatureMap);
multiSigJson.put("participants", participants.stream()
.map(pk -> Base64.getEncoder().encodeToString(pk.getEncoded()))
.collect(Collectors.toList()));
multiSigJson.put("requiredSignatures", requiredSignatures);
multiSigJson.put("currentSignatures", signatures.size());
return new com.google.gson.GsonBuilder()
.setPrettyPrinting()
.create()
.toJson(multiSigJson);
}
// Getters
public Transaction getTransaction() { return transaction; }
public List<PublicKey> getParticipants() { return new ArrayList<>(participants); }
public Map<PublicKey, byte[]> getSignatures() { return new HashMap<>(signatures); }
public int getRequiredSignatures() { return requiredSignatures; }
}
2. Blockchain-Specific Signing (Ethereum)
public class EthereumTransactionSigner {
private static final String ETHEREUM_CURVE = "secp256k1";
private static final String ALGORITHM = "SHA256withECDSA";
public EthereumSignedTransaction signEthereumTransaction(EthereumTransaction tx,
PrivateKey privateKey) {
try {
// Ethereum uses RLP encoding and specific transaction format
byte[] rawTransaction = encodeEthereumTransaction(tx);
// Sign the transaction
Signature signature = Signature.getInstance(ALGORITHM);
signature.initSign(privateKey);
signature.update(rawTransaction);
byte[] signatureBytes = signature.sign();
// Ethereum signature is 65 bytes: [r, s, v]
ECDSASignature ecSignature = parseDERSignature(signatureBytes);
// Add chain ID for replay protection (EIP-155)
int v = calculateV(ecSignature, tx.getChainId());
return new EthereumSignedTransaction(tx, ecSignature, v);
} catch (Exception e) {
throw new EthereumSigningException("Failed to sign Ethereum transaction", e);
}
}
public String getEthereumAddressFromPublicKey(PublicKey publicKey) {
try {
// Ethereum address is last 20 bytes of Keccak-256 hash of public key
byte[] publicKeyBytes = publicKey.getEncoded();
// Remove the prefix (first byte) for uncompressed keys
byte[] uncompressed = getUncompressedPublicKey(publicKeyBytes);
// Keccak-256 hash
byte[] hash = keccak256(uncompressed);
// Take last 20 bytes
byte[] addressBytes = Arrays.copyOfRange(hash, hash.length - 20, hash.length);
// Convert to hexadecimal with 0x prefix
return "0x" + bytesToHex(addressBytes);
} catch (Exception e) {
throw new EthereumException("Failed to derive Ethereum address", e);
}
}
private byte[] keccak256(byte[] input) {
// Implementation using BouncyCastle or similar
// This is a placeholder - real implementation would use Keccak-256
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(input);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 not available", e);
}
}
private ECDSASignature parseDERSignature(byte[] derSignature) {
// Parse DER-encoded ECDSA signature
// Implementation depends on signature format
return new ECDSASignature(new byte[32], new byte[32]); // Simplified
}
private int calculateV(ECDSASignature signature, long chainId) {
// Calculate v value based on chain ID (EIP-155)
return (int) (chainId * 2 + 35 + (signature.isRecoveryBit() ? 1 : 0));
}
private String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
}
public class EthereumTransaction {
private final String nonce;
private final String gasPrice;
private final String gasLimit;
private final String to;
private final String value;
private final String data;
private final long chainId;
public EthereumTransaction(String nonce, String gasPrice, String gasLimit,
String to, String value, String data, long chainId) {
this.nonce = nonce;
this.gasPrice = gasPrice;
this.gasLimit = gasLimit;
this.to = to;
this.value = value;
this.data = data;
this.chainId = chainId;
}
public byte[] encodeForSigning() {
// RLP encoding implementation for Ethereum
// This is a complex implementation that would use RLP encoding
return new byte[0]; // Simplified
}
// Getters
public long getChainId() { return chainId; }
}
public class EthereumSignedTransaction {
private final EthereumTransaction transaction;
private final ECDSASignature signature;
private final int v;
public EthereumSignedTransaction(EthereumTransaction transaction,
ECDSASignature signature, int v) {
this.transaction = transaction;
this.signature = signature;
this.v = v;
}
public String getRawTransaction() {
// Return hex-encoded raw transaction ready for broadcasting
return "0x" + bytesToHex(encodeSignedTransaction());
}
private byte[] encodeSignedTransaction() {
// RLP encoding of signed transaction
return new byte[0]; // Simplified
}
}
// Simplified ECDSA signature wrapper
class ECDSASignature {
private final byte[] r;
private final byte[] s;
private final boolean recoveryBit;
public ECDSASignature(byte[] r, byte[] s) {
this.r = r.clone();
this.s = s.clone();
this.recoveryBit = false;
}
public boolean isRecoveryBit() { return recoveryBit; }
}
Secure Key Management
1. Hardware Security Module (HSM) Integration
public class HSMKeyStoreService {
private final Provider hsmProvider;
private final String keyStoreType;
public HSMKeyStoreService(String providerName, String keyStoreType) {
this.hsmProvider = Security.getProvider(providerName);
this.keyStoreType = keyStoreType;
}
public KeyStore initializeKeyStore(char[] pin) throws KeyStoreException,
NoSuchAlgorithmException, CertificateException, IOException {
KeyStore keyStore = KeyStore.getInstance(keyStoreType, hsmProvider);
keyStore.load(null, pin);
return keyStore;
}
public String generateKeyInHSM(KeyStore keyStore, String alias, char[] pin)
throws KeyStoreException, NoSuchAlgorithmException, InvalidAlgorithmParameterException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC", hsmProvider);
ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256k1");
keyGen.initialize(ecSpec);
KeyPair keyPair = keyGen.generateKeyPair();
// Store in HSM-backed keystore
KeyStore.PrivateKeyEntry privateKeyEntry = new KeyStore.PrivateKeyEntry(
keyPair.getPrivate(),
new Certificate[]{generateSelfSignedCertificate(keyPair)}
);
keyStore.setEntry(alias, privateKeyEntry, new KeyStore.PasswordProtection(pin));
return Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
}
public byte[] signWithHSM(KeyStore keyStore, String alias, char[] pin, byte[] data)
throws UnrecoverableEntryException, NoSuchAlgorithmException,
KeyStoreException, InvalidKeyException, SignatureException {
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)
keyStore.getEntry(alias, new KeyStore.PasswordProtection(pin));
PrivateKey privateKey = privateKeyEntry.getPrivateKey();
Signature signature = Signature.getInstance("SHA256withECDSA");
signature.initSign(privateKey);
signature.update(data);
return signature.sign();
}
private Certificate generateSelfSignedCertificate(KeyPair keyPair) {
// Generate self-signed certificate for key storage
// Implementation depends on certificate requirements
return null; // Simplified
}
}
2. Software Key Storage with Encryption
public class SecureKeyStorage {
private static final String KEY_ALGORITHM = "AES";
private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_TAG_LENGTH = 128;
public void storeKeySecurely(PrivateKey privateKey, String filePath, char[] password)
throws Exception {
// Encrypt private key before storage
byte[] encryptedKey = encryptKey(privateKey.getEncoded(), password);
// Store with metadata
Map<String, Object> keyData = new HashMap<>();
keyData.put("algorithm", privateKey.getAlgorithm());
keyData.put("format", privateKey.getFormat());
keyData.put("encryptedKey", Base64.getEncoder().encodeToString(encryptedKey));
keyData.put("created", Instant.now().toString());
String jsonData = new com.google.gson.Gson().toJson(keyData);
Files.write(Paths.get(filePath), jsonData.getBytes(StandardCharsets.UTF_8));
}
public PrivateKey loadKeySecurely(String filePath, char[] password) throws Exception {
String jsonData = new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8);
Map<String, Object> keyData = new com.google.gson.Gson().fromJson(jsonData, Map.class);
String algorithm = (String) keyData.get("algorithm");
String format = (String) keyData.get("format");
String encryptedKeyBase64 = (String) keyData.get("encryptedKey");
byte[] encryptedKey = Base64.getDecoder().decode(encryptedKeyBase64);
byte[] decryptedKey = decryptKey(encryptedKey, password);
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decryptedKey);
return keyFactory.generatePrivate(keySpec);
}
private byte[] encryptKey(byte[] keyData, char[] password) throws Exception {
// Derive key from password
SecretKey secretKey = deriveKey(password);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
// Generate IV
byte[] iv = new byte[12];
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec);
byte[] encrypted = cipher.doFinal(keyData);
// Combine IV and encrypted data
ByteArrayOutputStream output = new ByteArrayOutputStream();
output.write(iv);
output.write(encrypted);
return output.toByteArray();
}
private byte[] decryptKey(byte[] encryptedData, char[] password) throws Exception {
SecretKey secretKey = deriveKey(password);
// Extract IV (first 12 bytes)
byte[] iv = Arrays.copyOfRange(encryptedData, 0, 12);
byte[] actualEncryptedData = Arrays.copyOfRange(encryptedData, 12, encryptedData.length);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec);
return cipher.doFinal(actualEncryptedData);
}
private SecretKey deriveKey(char[] password) throws Exception {
// Use PBKDF2 for key derivation
PBEKeySpec spec = new PBEKeySpec(password, getSalt(), 65536, 256);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] keyBytes = factory.generateSecret(spec).getEncoded();
return new SecretKeySpec(keyBytes, KEY_ALGORITHM);
}
private byte[] getSalt() {
// Use fixed salt or store with encrypted data
return "fixed-salt-for-demo".getBytes(StandardCharsets.UTF_8);
}
}
Enterprise Transaction Signing Service
1. Spring Boot Integration
@Service
public class EnterpriseSigningService {
private final TransactionSigningService signingService;
private final AuditService auditService;
private final RateLimitService rateLimitService;
public EnterpriseSigningService(TransactionSigningService signingService,
AuditService auditService,
RateLimitService rateLimitService) {
this.signingService = signingService;
this.auditService = auditService;
this.rateLimitService = rateLimitService;
}
@Transactional
public SignedTransaction signTransactionWithAudit(Transaction transaction,
String keyAlias,
char[] password,
String userId,
String clientInfo) {
// Check rate limits
if (!rateLimitService.isAllowed(userId, "sign_transaction")) {
throw new RateLimitExceededException("Transaction signing rate limit exceeded");
}
try {
// Sign the transaction
SignedTransaction signedTx = signingService.signTransaction(transaction, keyAlias, password);
// Audit the signing operation
auditService.auditSigningOperation(userId, transaction.getTransactionId(),
clientInfo, true, "Transaction signed successfully");
return signedTx;
} catch (Exception e) {
// Audit failure
auditService.auditSigningOperation(userId, transaction.getTransactionId(),
clientInfo, false, e.getMessage());
throw e;
}
}
}
@Component
public class AuditService {
public void auditSigningOperation(String userId, String transactionId,
String clientInfo, boolean success, String details) {
// Log to secure audit trail
System.out.printf("SIGNING_AUDIT: user=%s, tx=%s, client=%s, success=%s, details=%s%n",
userId, transactionId, clientInfo, success, details);
// In production, write to secure audit database
// with tamper-evident logging
}
}
@Component
public class RateLimitService {
private final Cache<String, RateLimitData> rateLimitCache;
public RateLimitService() {
this.rateLimitCache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.maximumSize(10000)
.build();
}
public boolean isAllowed(String userId, String operation) {
String key = userId + ":" + operation;
RateLimitData data = rateLimitCache.get(key, k -> new RateLimitData());
synchronized (data) {
if (System.currentTimeMillis() - data.lastReset > 3600000) { // 1 hour
data.count = 0;
data.lastReset = System.currentTimeMillis();
}
if (data.count >= getLimitForOperation(operation)) {
return false;
}
data.count++;
return true;
}
}
private int getLimitForOperation(String operation) {
// Return limits based on operation type
switch (operation) {
case "sign_transaction": return 1000; // 1000 signs per hour
case "create_key": return 10; // 10 key creations per hour
default: return 100;
}
}
private static class RateLimitData {
int count = 0;
long lastReset = System.currentTimeMillis();
}
}
2. REST API for Transaction Signing
@RestController
@RequestMapping("/api/transactions")
public class TransactionSigningController {
private final EnterpriseSigningService signingService;
private final TransactionValidationService validationService;
public TransactionSigningController(EnterpriseSigningService signingService,
TransactionValidationService validationService) {
this.signingService = signingService;
this.validationService = validationService;
}
@PostMapping("/sign")
public ResponseEntity<SigningResponse> signTransaction(
@RequestBody SigningRequest request,
@RequestHeader("X-User-Id") String userId,
@RequestHeader("X-Client-Info") String clientInfo) {
try {
// Validate request
validationService.validateSigningRequest(request);
// Create transaction
Transaction transaction = new Transaction(
request.getFromAddress(),
request.getToAddress(),
request.getAmount(),
request.getCurrency()
);
// Add metadata
request.getMetadata().forEach(transaction::addMetadata);
// Sign transaction
SignedTransaction signedTx = signingService.signTransactionWithAudit(
transaction, request.getKeyAlias(), request.getPassword().toCharArray(),
userId, clientInfo);
SigningResponse response = new SigningResponse(
signedTx.getTransaction().getTransactionId(),
signedTx.toSignedJson(),
"Transaction signed successfully"
);
return ResponseEntity.ok(response);
} catch (ValidationException e) {
return ResponseEntity.badRequest().body(
new SigningResponse(null, null, "Validation failed: " + e.getMessage()));
} catch (RateLimitExceededException e) {
return ResponseEntity.status(429).body(
new SigningResponse(null, null, "Rate limit exceeded"));
} catch (Exception e) {
return ResponseEntity.internalServerError().body(
new SigningResponse(null, null, "Signing failed: " + e.getMessage()));
}
}
@PostMapping("/verify")
public ResponseEntity<VerificationResponse> verifyTransaction(
@RequestBody VerificationRequest request) {
try {
SignedTransaction signedTx = request.getSignedTransaction();
boolean isValid = signingService.verifyTransactionWithAddress(
signedTx, request.getExpectedAddress());
return ResponseEntity.ok(new VerificationResponse(isValid,
isValid ? "Signature is valid" : "Signature is invalid"));
} catch (Exception e) {
return ResponseEntity.badRequest().body(
new VerificationResponse(false, "Verification failed: " + e.getMessage()));
}
}
}
// DTO classes
class SigningRequest {
private String fromAddress;
private String toAddress;
private double amount;
private String currency;
private String keyAlias;
private String password;
private Map<String, Object> metadata = new HashMap<>();
// Getters and setters
}
class SigningResponse {
private final String transactionId;
private final String signedTransaction;
private final String message;
// Constructor and getters
}
class VerificationRequest {
private SignedTransaction signedTransaction;
private String expectedAddress;
// Getters and setters
}
class VerificationResponse {
private final boolean valid;
private final String message;
// Constructor and getters
}
Security Best Practices
1. Comprehensive Security Validation
@Component
public class TransactionSecurityValidator {
public void validateTransactionSecurity(Transaction transaction, SecurityContext context) {
// Check for duplicate transactions
if (isDuplicateTransaction(transaction)) {
throw new SecurityException("Duplicate transaction detected");
}
// Check nonce sequence
if (!isValidNonce(transaction.getFromAddress(), transaction.getNonce())) {
throw new SecurityException("Invalid nonce sequence");
}
// Check amount limits
if (exceedsAmountLimit(transaction.getAmount(), context.getUserTier())) {
throw new SecurityException("Transaction amount exceeds limit for user tier");
}
// Check velocity (transactions per time period)
if (exceedsVelocityLimit(transaction.getFromAddress())) {
throw new SecurityException("Transaction velocity limit exceeded");
}
// Check for suspicious patterns
if (isSuspiciousPattern(transaction)) {
throw new SecurityException("Transaction matches suspicious pattern");
}
}
private boolean isDuplicateTransaction(Transaction transaction) {
// Check if same transaction ID or content was recently processed
return false; // Implementation depends on storage
}
private boolean isValidNonce(String address, long nonce) {
// Check if nonce is sequential for the address
// Implementation depends on state management
return true;
}
private boolean exceedsAmountLimit(double amount, String userTier) {
Map<String, Double> limits = Map.of(
"basic", 1000.0,
"premium", 10000.0,
"enterprise", 100000.0
);
return amount > limits.getOrDefault(userTier, 1000.0);
}
private boolean exceedsVelocityLimit(String address) {
// Check if address has exceeded transaction count in time window
return false; // Implementation depends on monitoring
}
private boolean isSuspiciousPattern(Transaction transaction) {
// Implement pattern detection for fraud
return false;
}
}
Conclusion
Transaction signing with Java provides:
Key Security Features:
- Non-repudiation: Cryptographic proof of authorization
- Integrity: Tamper-evident transaction data
- Authenticity: Verified sender identity
- Replay Protection: Nonce and timestamp mechanisms
Implementation Requirements:
- Strong cryptographic foundations (ECDSA, SHA-256)
- Secure key management (HSM, encrypted storage)
- Comprehensive audit trails
- Rate limiting and fraud detection
- Enterprise-grade error handling
Production Considerations:
- Hardware Security Modules for key protection
- Comprehensive audit logging
- Rate limiting and throttling
- Multi-signature support for high-value transactions
- Regular security reviews and penetration testing
Java's robust security APIs, strong typing, and enterprise ecosystem make it an excellent choice for building secure, scalable transaction signing systems that meet the demands of financial institutions, blockchain applications, and enterprise systems.