Pular para conteúdo

Assinaturas Digitais (Digital Signatures)

Assinaturas digitais fornecem três garantias de segurança críticas: autenticidade (a mensagem foi assinada pelo remetente alegado), integridade (a mensagem não foi modificada) e não-repúdio (o signatário não pode negar tê-la assinado).

Como Funcionam as Assinaturas Digitais

O conceito fundamental é simples, mas poderoso: 1. Hash da mensagem para criar um digest de tamanho fixo $h = H(M)$ 2. Criptografe o hash com a chave privada do remetente: $s = D_{priv}(h)$ 3. O resultado $s$ é a assinatura digital

Verificação: 1. Recebedor hash da mensagem recebida: $h' = H(M')$ 2. Descriptografe assinatura com chave pública: $h'' = E_{pub}(s)$ 3. Compare: se $h' == h''$, assinatura é válida

Assinaturas Digitais RSA (RSA-PSS)

Fundamento Matemático

Usando RSA, uma assinatura digital é criada calculando: $$S = M^d \mod n$$

Onde $M$ é o hash da mensagem, não a mensagem em si. Isso previne certos ataques em assinaturas RSA cruas.

Recomendado: RSA-PSS (Probabilistic Signature Scheme)

O RSA-PSS fornece melhor segurança que o tradicional PKCS#1 v1.5 usando padding probabilístico.

Implementação Java com RSA-PSS

import java.security.*;
import java.util.Base64;

public class AssinaturaRSA {

    /**
     * Assina uma mensagem usando RSA-PSS.
     */
    public static String assinarMensagem(byte[] mensagem, PrivateKey chavePrivada) throws Exception {
        // Criar instância de assinatura com padding PSS
        Signature assinatura = Signature.getInstance("SHA256withRSA");

        // Inicializar assinatura com chave privada
        assinatura.initSign(chavePrivada);

        // Atualizar com dados da mensagem
        assinatura.update(mensagem);

        // Gerar e retornar assinatura
        byte[] bytesAssinatura = assinatura.sign();
        return Base64.getEncoder().encodeToString(bytesAssinatura);
    }

    /**
     * Verifica uma assinatura digital usando RSA-PSS.
     */
    public static boolean verificarAssinatura(byte[] mensagem, String base64Assinatura, PublicKey chavePublica) throws Exception {
        Signature assinatura = Signature.getInstance("SHA256withRSA");

        // Inicializar verificação com chave pública
        assinatura.initVerify(chavePublica);

        // Atualizar com dados da mensagem
        assinatura.update(mensagem);

        // Decodificar e verificar assinatura
        byte[] bytesAssinatura = Base64.getDecoder().decode(base64Assinatura);
        return assinatura.verify(bytesAssinatura);
    }

    public static void main(String[] args) throws Exception {
        // Gerar par de chaves RSA
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
        keyGen.initialize(2048);
        KeyPair parDeChaves = keyGen.generateKeyPair();

        PrivateKey chavePrivada = parDeChaves.getPrivate();
        PublicKey chavePublica = parDeChaves.getPublic();

        String mensagem = "Este é um documento importante que precisa ser assinado";

        // Assinar a mensagem
        String assinatura = assinarMensagem(mensagem.getBytes(), chavePrivada);
        System.out.println("Assinatura: " + assinatura);

        // Verificar a assinatura (deve ter sucesso)
        boolean isValido1 = verificarAssinatura(mensagem.getBytes(), assinatura, chavePublica);
        System.out.println("Verificação 1 passou: " + isValido1);

        // Modificar mensagem e tentar verificar (deve falhar)
        String mensagemModificada = "Este é um documento MODIFICADO";
        boolean isValido2 = verificarAssinatura(mensagemModificada.getBytes(), assinatura, chavePublica);
        System.out.println("Verificação modificada passou: " + isValido2);
    }
}

ECDSA (Elliptic Curve Digital Signature Algorithm)

O ECDSA fornece a mesma segurança do RSA com tamanhos de chave muito menores.

Implementação Java com ECDSA

import java.security.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

public class AssinaturaECDSA {

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * Gera um par de chaves ECC para assinatura.
     */
    public static KeyPair gerarParDeChaves() throws Exception {
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC", "BC");
        keyGen.initialize(256); // Curva P-256
        return keyGen.generateKeyPair();
    }

    /**
     * Assina uma mensagem usando ECDSA.
     */
    public static byte[] assinarMensagem(byte[] mensagem, PrivateKey chavePrivada) throws Exception {
        Signature assinatura = Signature.getInstance("SHA256withECDSA", "BC");

        // Inicializar assinatura com chave privada
        assinatura.initSign(chavePrivada);

        // Atualizar com dados da mensagem
        assinatura.update(mensagem);

        // Gerar e retornar assinatura
        return assinatura.sign();
    }

    /**
     * Verifica uma assinatura ECDSA.
     */
    public static boolean verificarAssinatura(byte[] mensagem, byte[] bytesAssinatura, PublicKey chavePublica) throws Exception {
        Signature assinatura = Signature.getInstance("SHA256withECDSA", "BC");

        // Inicializar verificação com chave pública
        assinatura.initVerify(chavePublica);

        // Atualizar com dados da mensagem
        assinatura.update(mensagem);

        // Verificar assinatura
        return assinatura.verify(bytesAssinatura);
    }

    /**
     * Assina uma mensagem e retorna assinatura codificada em Base64.
     */
    public static String assinarMensagemBase64(byte[] mensagem, PrivateKey chavePrivada) throws Exception {
        byte[] assinatura = assinarMensagem(mensagem, chavePrivada);
        return Base64.getEncoder().encodeToString(assinatura);
    }

    public static void main(String[] args) throws Exception {
        // Gerar par de chaves ECC
        KeyPair parDeChaves = gerarParDeChaves();
        PrivateKey chavePrivada = parDeChaves.getPrivate();
        PublicKey chavePublica = parDeChaves.getPublic();

        String mensagem = "Demonstração de assinatura digital";

        System.out.println("Mensagem original: " + mensagem);

        // Assinar a mensagem
        byte[] assinatura = assinarMensagem(mensagem.getBytes(), chavePrivada);
        System.out.println("Comprimento da assinatura: " + assinatura.length + " bytes");

        // Verificar a assinatura (deve ter sucesso)
        boolean isValido1 = verificarAssinatura(mensagem.getBytes(), assinatura, chavePublica);
        System.out.println("Verificação 1 passou: " + isValido1);

        // Modificar mensagem e tentar verificar (deve falhar)
        String mensagemModificada = "MENSAGEM MODIFICADA";
        boolean isValido2 = verificarAssinatura(mensagemModificada.getBytes(), assinatura, chavePublica);
        System.out.println("Verificação modificada passou: " + isValido2);
    }
}

HMAC (Hash-based Message Authentication Code)

O HMAC é usado quando você não tem chaves assimétricas mas precisa de autenticação de mensagem. Ele usa uma chave secreta compartilhada com uma função hash.

Fundamento Matemático

$$\text{HMAC}(K, m) = H((K' \oplus opad) || H((K' \oplus ipad) || m))$$

Onde: - $K$ é a chave original - $K'$ é $K$ preenchida até o tamanho do bloco - $ipad$ é 0x36 repetido - $opad$ é 0x5C repetido

Implementação Java com HMAC-SHA256

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class AutenticacaoHMAC {

    private static final String ALGORITMO = "HmacSHA256";

    /**
     * Cria uma assinatura HMAC.
     */
    public static byte[] criarHMAC(byte[] chave, byte[] mensagem) throws Exception {
        Mac mac = Mac.getInstance(ALGORITMO);

        // Inicializar com chave secreta
        SecretKeySpec chaveSecreta = new SecretKeySpec(chave, ALGORITMO);
        mac.init(chaveSecreta);

        // Gerar HMAC
        return mac.doFinal(mensagem);
    }

    /**
     * Verifica uma assinatura HMAC.
     */
    public static boolean verificarHMAC(byte[] chave, byte[] mensagem, byte[]assinaturaEsperada) throws Exception {
        Mac mac = Mac.getInstance(ALGORITMO);

        // Inicializar com chave secreta
        SecretKeySpec chaveSecreta = new SecretKeySpec(chave, ALGORITMO);
        mac.init(chaveSecreta);

        // Gerar HMAC da mensagem recebida
        byte[] assinaturaCalculada = mac.doFinal(mensagem);

        // Comparar assinaturas (use comparação de tempo constante)
        return java.util.Arrays.equals(assinaturaCalculada, assinaturaEsperada);
    }

    /**
     * Cria assinatura HMAC codificada em Base64.
     */
    public static String criarHMACBase64(byte[] chave, byte[] mensagem) throws Exception {
        byte[] hmac = criarHMAC(chave, mensagem);
        return Base64.getEncoder().encodeToString(hmac);
    }

    /**
     * Verifica HMAC e retorna assinatura decodificada.
     */
    public static boolean verificarHMACBase64(byte[] chave, byte[] mensagem, String base64Assinatura) throws Exception {
        byte[] esperada = Base64.getDecoder().decode(base64Assinatura);
        return verificarHMAC(chave, mensagem, esperada);
    }

    /**
     * Exemplo de uso com mensagens de string.
     */
    public static void main(String[] args) throws Exception {
        // Gerar uma chave segura aleatória (na produção, use gerenciamento adequado de chaves)
        byte[] chave = new java.security.SecureRandom().generateNewByteArray(32);

        String mensagem = "Dados importantes de transação";

        System.out.println("Mensagem: " + mensagem);

        // Criar assinatura HMAC
        byte[] hmac = criarHMAC(chave, mensagem.getBytes());
        String hmacBase64 = Base64.getEncoder().encodeToString(hmac);
        System.out.println("HMAC (Base64): " + hmacBase64);

        // Verificar a assinatura (deve ter sucesso)
        boolean isValido1 = verificarHMAC(chave, mensagem.getBytes(), hmac);
        System.out.println("Verificação 1 passou: " + isValido1);

        // Modificar mensagem e tentar verificar (deve falhar)
        String mensagemModificada = "DADOS MODIFICADOS de transação";
        boolean isValido2 = verificarHMAC(chave, mensagemModificada.getBytes(), hmac);
        System.out.println("Verificação modificada passou: " + isValido2);
    }
}

Assinaturas Baseadas em Certificados (Certificados X.509)

Certificados digitais vinculam chaves públicas a identidades e são assinados por Autoridades Certificadoras (CAs).

Verificação de Cadeia de Certificados

import java.security.*;
import javax.security.auth.x500.X500Principal;

public class VerificacaoCertificado {

    /**
     * Verifica uma cadeia de certificados.
     */
    public static boolean verificarCadeiaDeCertificados(X509Certificate[] cadeia) throws Exception {
        if (cadeia == null || cadeia.length < 1) {
            return false;
        }

        X509Certificate folha = cadeia[0];

        // Verificar cada certificado na cadeia
        for (int i = 1; i < cadeia.length; i++) {
            X509Certificate emissor = cadeia[i];
            X509Certificate sujeito = cadeia[i - 1];

            // Verificar que o emissor assinou este certificado
            if (!emissor.verify(sujeito.getPublicKey())) {
                return false;
            }
        }

        // Verificar assinatura do certificado folha usando sua própria chave pública
        return folha.verify(folha.getPublicKey());
    }

    /**
     * Verifica um certificado contra CAs confiáveis.
     */
    public static boolean verificarCertificado(X509Certificate cert, KeyStore trustStore) throws Exception {
        // Verificar que o certificado não expirou
        if (cert.isExpired()) {
            return false;
        }

        // Verificar assinatura usando chave pública do emissor
        PublicKey chavePublicaEmissor = cert.getIssuerPublicKey();
        cert.verify(chavePublicaEmissor);

        // Verificar se o emissor está no truststore
        String issuerDN = cert.getIssuerX500Principal().getName();
        Certificate[] certificados = trustStore.getCertificates(issuerDN);

        if (certificados == null || certificados.length == 0) {
            return false;
        }

        // Verificar recursivamente até o topo da cadeia
        X509Certificate certEmissor = (X509Certificate) certificados[0];
        return verificarCertificado(certEmissor, trustStore);
    }
}

Assinatura de Código

Assinatura de código garante autenticidade e integridade de software. Isso é usado para: - Arquivos executáveis (.exe, .dll) - Aplicações móveis (.apk, .ipa) - Gerenciadores de pacotes (npm, pip, cargo)

Exemplo de Assinatura JAR Java

import java.security.*;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public class AssinaturaJar {

    /**
     * Assina um arquivo JAR.
     */
    public static void assinarJar(String caminhoJar, PrivateKey chavePrivada) throws Exception {
        // Obter manifesto do JAR
        Manifest manifesto = new Manifest(new java.util.jar.JarInputStream(
            new java.io.FileInputStream(caminhoJar)));

        // Criar assinatura para cada entrada no manifesto
        String[] entradas = manifesto.getMainAttributes().getString("Entries").split("\\s+");

        for (String entrada : entradas) {
            byte[] dados = obterDadosDeEntrada(caminhoJar, entrada);

            Signature assinatura = Signature.getInstance("SHA256withRSA");
            assinatura.initSign(chavePrivada);
            assinatura.update(dados);

            // Armazenar assinatura em atributos do manifesto
        }

        // Escrever JAR assinado
    }

    /**
     * Verifica um arquivo JAR assinado.
     */
    public static boolean verificarJar(String caminhoJar, PublicKey chavePublica) throws Exception {
        Manifest manifesto = new Manifest(new java.util.jar.JarInputStream(
            new java.io.FileInputStream(caminhoJar)));

        String[] entradas = manifesto.getMainAttributes().getString("Entries").split("\\s+");

        for (String entrada : entradas) {
            byte[] dados = obterDadosDeEntrada(caminhoJar, entrada);

            Signature assinatura = Signature.getInstance("SHA256withRSA");
            assinatura.initVerify(chavePublica);
            assinatura.update(dados);

            if (!assinatura.verify(Base64.getDecoder().decode(
                manifesto.getAttributes().get("SHA256WithRSA")))) {
                return false;
            }
        }

        return true;
    }
}

Timestamping (RFC 3161)

Timestamps fornecem prova de que uma assinatura existia em um momento específico, útil para fins legais e de conformidade.

Estrutura de Solicitação de Timestamp

Uma solicitação de timestamp inclui: - A mensagem a ser assinada - Um nonce (para prevenir ataques de replay) - A identidade do signatário

A autoridade de timestamp assina isso com seu próprio certificado, criando uma prova imutável da existência naquele momento.

Considerações de Segurança

Melhores Práticas

  1. Use funções hash fortes: SHA-256 ou melhor para assinaturas
  2. Prefira ECDSA sobre RSA: Chaves menores, mesma segurança
  3. Valide certificados: Sempre verifique cadeias de certificados
  4. Use timestamping: Para requisitos legais/conformidade
  5. Proteja chaves privadas: Use HSMs ou armazenamento seguro de chaves

Vulnerabilidades Comuns

Vulnerabilidade Descrição Mitigação
Geração fraca de números aleatórios Assinaturas previsíveis Use RNG criptograficamente seguro
Falsificação de certificados Atacante cria certificados falsos Valide contra CAs confiáveis
Maleabilidade de assinatura Múltiplas assinaturas válidas para mesma mensagem Use assinatura determinística (RFC 6979)

Referências

  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