Skip to content

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.

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

  1. Use strong hash functions: SHA-256 or better for signatures
  2. Prefer ECDSA over RSA: Smaller keys, same security
  3. Validate certificates: Always verify certificate chains
  4. Use timestamping: For legal/compliance requirements
  5. 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

  1. RFC 8017: PKCS #1: RSA Cryptography Specifications Version 2.2
  2. RFC 6979: Deterministic Usage of the Digital Signature Algorithm (DSA) and Elliptic Curve Digital Signature Algorithm (ECDSA)
  3. RFC 3161: Internet X.509 Public Key Infrastructure Certificate Timestamping
  4. SEC 1: Elliptic Curve Cryptography Standards