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¶
- Use funções hash fortes: SHA-256 ou melhor para assinaturas
- Prefira ECDSA sobre RSA: Chaves menores, mesma segurança
- Valide certificados: Sempre verifique cadeias de certificados
- Use timestamping: Para requisitos legais/conformidade
- 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¶
- 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