Digital Signatures (Assinaturas Digitais)¶
Digital signatures provide three critical security guarantees: authenticity (the message was signed by the claimed sender), integrity (the message has not been modified), and non-repudiation (the signer cannot deny having signed it).
How Digital Signatures Work¶
The fundamental concept is simple but powerful: 1. Hash the message to create a fixed-size digest $h = H(M)$ 2. Encrypt the hash with the sender's private key: $s = D_{priv}(h)$ 3. The result $s$ is the digital signature
Verification: 1. Receiver hashes received message: $h' = H(M')$ 2. Decrypt signature with public key: $h'' = E_{pub}(s)$ 3. Compare: if $h' == h''$, signature is valid
RSA Digital Signatures (RSA-PSS)¶
Mathematical Foundation¶
Using RSA, a digital signature is created by computing: $$S = M^d \mod n$$
Where $M$ is the hash of the message, not the message itself. This prevents certain attacks on raw RSA signatures.
Recommended: RSA-PSS (Probabilistic Signature Scheme)¶
RSA-PSS provides better security than traditional PKCS#1 v1.5 by using probabilistic padding.
Java Implementation with RSA-PSS¶
import java.security.*;
import java.util.Base64;
public class RSADigitalSignature {
/**
* Signs a message using RSA-PSS.
*/
public static String signMessage(byte[] message, PrivateKey privateKey) throws Exception {
// Create signature instance with PSS padding
Signature signature = Signature.getInstance("SHA256withRSA");
// Initialize signing with private key
signature.initSign(privateKey);
// Update with message data
signature.update(message);
// Generate and return signature
byte[] signatureBytes = signature.sign();
return Base64.getEncoder().encodeToString(signatureBytes);
}
/**
* Verifies a digital signature using RSA-PSS.
*/
public static boolean verifySignature(byte[] message, String base64Signature, PublicKey publicKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
// Initialize verification with public key
signature.initVerify(publicKey);
// Update with message data
signature.update(message);
// Decode and verify signature
byte[] signatureBytes = Base64.getDecoder().decode(base64Signature);
return signature.verify(signatureBytes);
}
public static void main(String[] args) throws Exception {
// Generate RSA key pair
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
KeyPair keyPair = keyGen.generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
PublicKey publicKey = keyPair.getPublic();
String message = "This is an important document that needs to be signed";
// Sign the message
String signature = signMessage(message.getBytes(), privateKey);
System.out.println("Signature: " + signature);
// Verify the signature (should succeed)
boolean isValid1 = verifySignature(message.getBytes(), signature, publicKey);
System.out.println("Verification 1 passed: " + isValid1);
// Modify message and try to verify (should fail)
String modifiedMessage = "This is a MODIFIED document";
boolean isValid2 = verifySignature(modifiedMessage.getBytes(), signature, publicKey);
System.out.println("Modified verification passed: " + isValid2);
}
}
ECDSA (Elliptic Curve Digital Signature Algorithm)¶
ECDSA provides the same security as RSA with much smaller key sizes.
Java Implementation with ECDSA¶
import java.security.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
public class ECDSADigitalSignature {
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* Generates an ECC key pair for signing.
*/
public static KeyPair generateKeyPair() throws Exception {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC", "BC");
keyGen.initialize(256); // P-256 curve
return keyGen.generateKeyPair();
}
/**
* Signs a message using ECDSA.
*/
public static byte[] signMessage(byte[] message, PrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withECDSA", "BC");
// Initialize signing with private key
signature.initSign(privateKey);
// Update with message data
signature.update(message);
// Generate and return signature
return signature.sign();
}
/**
* Verifies an ECDSA signature.
*/
public static boolean verifySignature(byte[] message, byte[] signatureBytes, PublicKey publicKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withECDSA", "BC");
// Initialize verification with public key
signature.initVerify(publicKey);
// Update with message data
signature.update(message);
// Verify signature
return signature.verify(signatureBytes);
}
/**
* Signs a message and returns Base64-encoded signature.
*/
public static String signMessageBase64(byte[] message, PrivateKey privateKey) throws Exception {
byte[] signature = signMessage(message, privateKey);
return Base64.getEncoder().encodeToString(signature);
}
public static void main(String[] args) throws Exception {
// Generate ECC key pair
KeyPair keyPair = generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
PublicKey publicKey = keyPair.getPublic();
String message = "Digital signature demonstration";
System.out.println("Original message: " + message);
// Sign the message
byte[] signature = signMessage(message.getBytes(), privateKey);
System.out.println("Signature length: " + signature.length + " bytes");
// Verify the signature (should succeed)
boolean isValid1 = verifySignature(message.getBytes(), signature, publicKey);
System.out.println("Verification 1 passed: " + isValid1);
// Modify message and try to verify (should fail)
String modifiedMessage = "MODIFIED message";
boolean isValid2 = verifySignature(modifiedMessage.getBytes(), signature, publicKey);
System.out.println("Modified verification passed: " + isValid2);
}
}
HMAC (Hash-based Message Authentication Code)¶
HMAC is used when you don't have asymmetric keys but need message authentication. It uses a shared secret key with a hash function.
Mathematical Foundation¶
$$\text{HMAC}(K, m) = H((K' \oplus opad) || H((K' \oplus ipad) || m))$$
Where: - $K$ is the original key - $K'$ is $K$ padded to block size - $ipad$ is 0x36 repeated - $opad$ is 0x5C repeated
Java Implementation with HMAC-SHA256¶
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class HMCAuthentication {
private static final String ALGORITHM = "HmacSHA256";
/**
* Creates an HMAC signature.
*/
public static byte[] createHMAC(byte[] key, byte[] message) throws Exception {
Mac mac = Mac.getInstance(ALGORITHM);
// Initialize with secret key
SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM);
mac.init(secretKey);
// Generate HMAC
return mac.doFinal(message);
}
/**
* Verifies an HMAC signature.
*/
public static boolean verifyHMAC(byte[] key, byte[] message, byte[] expectedSignature) throws Exception {
Mac mac = Mac.getInstance(ALGORITHM);
// Initialize with secret key
SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM);
mac.init(secretKey);
// Generate HMAC of received message
byte[] computedSignature = mac.doFinal(message);
// Compare signatures (use constant-time comparison)
return java.util.Arrays.equals(computedSignature, expectedSignature);
}
/**
* Creates Base64-encoded HMAC signature.
*/
public static String createHMACBase64(byte[] key, byte[] message) throws Exception {
byte[] hmac = createHMAC(key, message);
return Base64.getEncoder().encodeToString(hmac);
}
/**
* Verifies HMAC and returns decoded signature.
*/
public static boolean verifyHMACBase64(byte[] key, byte[] message, String base64Signature) throws Exception {
byte[] expected = Base64.getDecoder().decode(base64Signature);
return verifyHMAC(key, message, expected);
}
/**
* Example usage with string messages.
*/
public static void main(String[] args) throws Exception {
// Generate a secure random key (in production, use proper key management)
byte[] key = new java.security.SecureRandom().generateNewByteArray(32);
String message = "Important transaction data";
System.out.println("Message: " + message);
// Create HMAC signature
byte[] hmac = createHMAC(key, message.getBytes());
String hmacBase64 = Base64.getEncoder().encodeToString(hmac);
System.out.println("HMAC (Base64): " + hmacBase64);
// Verify the signature (should succeed)
boolean isValid1 = verifyHMAC(key, message.getBytes(), hmac);
System.out.println("Verification 1 passed: " + isValid1);
// Modify message and try to verify (should fail)
String modifiedMessage = "MODIFIED transaction data";
boolean isValid2 = verifyHMAC(key, modifiedMessage.getBytes(), hmac);
System.out.println("Modified verification passed: " + isValid2);
}
}
Certificate-Based Signatures (X.509 Certificates)¶
Digital certificates bind public keys to identities and are signed by Certificate Authorities (CAs).
Certificate Chain Verification¶
import java.security.*;
import java.util.Date;
import javax.security.auth.x500.X500Principal;
public class CertificateVerification {
/**
* Verifies a certificate chain.
*/
public static boolean verifyCertificateChain(X509Certificate[] chain) throws Exception {
if (chain == null || chain.length < 1) {
return false;
}
X509Certificate leaf = chain[0];
// Verify each certificate in the chain
for (int i = 1; i < chain.length; i++) {
X509Certificate issuer = chain[i];
X509Certificate subject = chain[i - 1];
// Verify that issuer signed this certificate
if (!issuer.verify(subject.getPublicKey())) {
return false;
}
}
// Verify leaf certificate signature using its own public key
return leaf.verify(leaf.getPublicKey());
}
/**
* Verifies a certificate against trusted CAs.
*/
public static boolean verifyCertificate(X509Certificate cert, KeyStore trustStore) throws Exception {
// Verify the certificate hasn't expired
if (cert.isExpired()) {
return false;
}
// Verify signature using issuer's public key
PublicKey issuerKey = cert.getIssuerPublicKey();
cert.verify(issuerKey);
// Check if issuer is in trust store
String issuerDN = cert.getIssuerX500Principal().getName();
Certificate[] certificates = trustStore.getCertificates(issuerDN);
if (certificates == null || certificates.length == 0) {
return false;
}
// Recursively verify up the chain
X509Certificate issuerCert = (X509Certificate) certificates[0];
return verifyCertificate(issuerCert, trustStore);
}
}
Code Signing¶
Code signing ensures software authenticity and integrity. This is used for: - Executable files (.exe, .dll) - Mobile applications (.apk, .ipa) - Package managers (npm, pip, cargo)
Java JAR Signing Example¶
import java.security.*;
import java.util.Date;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class JarSigning {
/**
* Signs a JAR file.
*/
public static void signJar(String jarPath, PrivateKey privateKey) throws Exception {
// Get the manifest from the JAR
Manifest manifest = new Manifest(new java.util.jar.JarInputStream(
new java.io.FileInputStream(jarPath)));
// Create a signature for each entry in the manifest
String[] entries = manifest.getMainAttributes().getString("Entries").split("\\s+");
for (String entry : entries) {
byte[] data = getEntryData(jarPath, entry);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(data);
// Store signature in manifest attributes
manifest.getAttributes().put(
new java.util.Enumeration<String>() {
public boolean hasMoreElements() { return true; }
public String nextElement() {
return Base64.getEncoder().encodeToString(signature.sign());
}
}, "SHA256WithRSA");
}
// Write signed JAR
manifest.write(new java.util.jar.JarOutputStream(
new java.io.FileOutputStream(jarPath + ".signed")));
}
/**
* Verifies a signed JAR file.
*/
public static boolean verifyJar(String jarPath, PublicKey publicKey) throws Exception {
Manifest manifest = new Manifest(new java.util.jar.JarInputStream(
new java.io.FileInputStream(jarPath)));
String[] entries = manifest.getMainAttributes().getString("Entries").split("\\s+");
for (String entry : entries) {
byte[] data = getEntryData(jarPath, entry);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(data);
if (!signature.verify(Base64.getDecoder().decode(
manifest.getAttributes().get("SHA256WithRSA")))) {
return false;
}
}
return true;
}
}
Timestamping (RFC 3161)¶
Timestamps provide proof that a signature existed at a specific time, useful for legal and compliance purposes.
Timestamp Request Structure¶
A timestamp request includes: - The message to be signed - A nonce (to prevent replay attacks) - The signer's identity
The timestamp authority signs this with its own certificate, creating an immutable proof of existence at that time.
Security Considerations¶
Best Practices¶
- Use strong hash functions: SHA-256 or better for signatures
- Prefer ECDSA over RSA: Smaller keys, same security
- Validate certificates: Always verify certificate chains
- Use timestamping: For legal/compliance requirements
- Protect private keys: Use HSMs or secure key storage
Common Vulnerabilities¶
| Vulnerability | Description | Mitigation |
|---|---|---|
| Weak random number generation | Predictable signatures | Use cryptographically secure RNG |
| Certificate forgery | Attacker creates fake certificates | Validate against trusted CAs |
| Signature malleability | Multiple valid signatures for same message | Use deterministic signing (RFC 6979) |
References¶
- RFC 8017: PKCS #1: RSA Cryptography Specifications Version 2.2
- RFC 6979: Deterministic Usage of the Digital Signature Algorithm (DSA) and Elliptic Curve Digital Signature Algorithm (ECDSA)
- RFC 3161: Internet X.509 Public Key Infrastructure Certificate Timestamping
- SEC 1: Elliptic Curve Cryptography Standards