A assinatura digital tem como função oferecer a garantia da procedência de um documento e se este documento sofreu alguma alteração não autorizada. Com a ocorrência cada vez maior dos crimes digitais e dada a importância dos documentos e arquivos para pessoas e organizações torna-se cada mais importante a sua segurança.

Devido o grande número de pessoas e empresas que utilizam a assinatura digital foram realizadas formas de organizar e padronizar as operações. Com isso a ICP-Brasil realizou toda essa organização e padronização por meio da certificação digital e das suas autoridades certificadoras. Por isso as assinaturas digitais hoje são consideradas bastante seguras.

Portanto, a assinatura digital garante que um documento não foi modificado e realmente procede a identidade de quem diz ter enviado certo documento.

No restante do artigo veremos como funciona a assinatura digital em Java, o que é o algoritmo DSA utilizado para assinatura digital e por fim faremos um exemplo completo em Java demonstrando como implementa-lo na prática.

Nota: Quer começar a estudar Java e não sabe por onde começar? Preparamos um Checklist especial para quem quer se tornar um programador Java.

Como Funciona a Assinatura Digital em Java

Veremos nesta seção como se dá o suporte provido pela API Java para a criptografia e a verificação de dados através de assinaturas digitais.

Todo o suporte necessário para implementarmos a assinatura digital em Java é provido pela API padrão do Java. Dessa forma, não necessitaremos recorrer a bibliotecas externas.

O Java traz dois métodos para os desenvolvedores que querem trabalhar com assinaturas digitais, são eles: os Message Digest e as Assinaturas digitais. O Message Digest são funções hash que geram código de tamanho fixo e unidirecional, ou seja, a partir de dados de um tamanho qualquer teremos como resultado um hash que não poderá mais ser revertido de volta para a mensagem original. Por isso quando estamos trabalhando com hashs deveremos gerar um hash de uma mensagem e comparar com o hash anterior. A API Java implementa dois algoritmos de Message Digest, são eles o MD5 e a família SHA; O outro método é o de Assinaturas Digitais que serve para autenticar o remetente da informação e oferecer a garantia de que o dado é confiável. As assinaturas digitais trabalham com a assinatura em si, uma chave pública e uma chave privada. O remetente é quem deve gerar a assinatura, a chave pública e a chave privada e, além disso, também deve fornecer a chave pública aos destinatários de suas mensagens.

Quando o remetente deseja enviar um dado ele gera uma assinatura usando a chave privada. O destinatário receberá a mensagem e a assinatura e com isso fará a validação da informação usando a chave pública. Se a validação for realizada com sucesso o destinatário tem a garantia que a mensagem foi enviada por um remetente confiável, possuidor da chave privada.

Para utilizar o Message Digest basta obter uma instância do algoritmo a ser utilizado, passar a informação que desejamos criptografar e por fim realizar a criptografia.

Para obter a instância do algoritmo podemos utilizar o código da Listagem 1.


MessageDigest md5 = MessageDigest.getInstance("MD5");
Listagem 1. Obtendo uma instância do algoritmo

Após isso, podemos utilizar o método update() passando os dados a serem criptografados. Para o algoritmo voltar ao estado original utilizamos o método reset(). Por fim, utilizamos digest() para gerar a chave criptografada.

Uma assinatura possui duas propriedades: a primeira propriedade diz que através da chave pública (correspondente à chave privada usada para geração da assinatura digital) podemos verificar a autenticidade e a integridade dos dados que estão sendo transmitidos; e a outra propriedade diz que a assinatura digital e a chave pública não revelam a chave privada.

A classe Java responsável por gerar as assinaturas digitais é a Signature. Para criar um objeto Signature utilizamos a chamada presente na Listagem 2.


Signature signature = Signature.getInstance("DSA");
Listagem 2. Criando um objeto Signature para gerar as assinaturas digitais

No exemplo acima estamos utilizando o algoritmo DSA.

Para gerar as chaves públicas e privadas utilizamos a classe KeyPairGenerator. KeyPairGenerator também necessita de um algoritmo para gerar as chaves. Segue na Listagem 3 um exemplo de como podemos gerar as chaves:


KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
Listagem 3. Instanciando o objeto KeyPairGenerator para gerar as chaves pública e privada

Tanto as chaves quanto a assinatura devem ser geradas utilizando-se o mesmo algoritmo (no exemplo o DSA). Após obtermos as chaves do KeyPairGenerator devemos inicializa-lo através do método initialize() que recebe um tamanho de chave e um número aleatório através da classe SecureRandom. Segue na Listagem 4 como podemos gerar as chaves.


KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");  
SecureRandom secureRan = new SecureRandom();  
kpg.initialize(512, secureRan);  
KeyPair kp = keyGen.generateKeyPair();  
PublicKey pubKey = kp.getPublic();  
PrivateKey priKey = kp.getPrivate();
Listagem 4. Gerando as chaves pública e privada

Agora que temos a nossa chave privada podemos utilizá-la no nosso objeto Signature, segue o código da Listagem 5.


signature.initSign(priKey);
Listagem 5. Utilizando a chave privada no objeto Signature

Nesse momento já podemos utilizar o método update() para que a informação possa ser assinada ou verificada, como mostra a Listagem 6.


String mensagem = "Exemplo de mensagem";
signature.update(mensagem.getBytes());
Listagem 6. Criptografando os dados com update()

E por fim chamamos o método sign() para a geração da assinatura, conforme mostra o exemplo da Listagem 7.


byte[] assinatura = signature.sign();
Listagem 7. Gerando a assinatura digital com sign()

Com isso, o remetente agora precisa fornecer a chave pública juntamente com o dado a ser enviado e a assinatura correspondente.

O destinatário receberá a assinatura digital e deve ter acesso à chave pública para então validar a assinatura junto com o dado recebido utilizando a chave pública. Segue na Listagem 8 um exemplo de como isso pode ser feito.


Signature signature = Signature.getInstance("DSA");  
signature.initVerify(pubKey);  
signature.update(mensagem.getBytes());  
      
if (signature.verify(assinatura)) {  
         //Mensagem assinada corretamente  
} else {  
         //Mensagem não pode ser validada
}
Listagem 8. Destinatário validando a assinatura digital através dos dados recebidos pelo remetente

Algoritmo DSA

A assinatura digital traz ao público de segurança de dados algoritmos como o DSA, que foi proposto pelo National Institute of Standards and Technology (NIST) e mais tarde padronizado. O DSA pode ser combinado com um algoritmo de criptografia, como, por exemplo, o RSA, e com alguma das funções hash como o MD5, o MD2, a família SHA, entre outros.

O DSA (Digital Signature Algorithm) é um algoritmo utilizado para assinatura digital, não sendo utilizado para criptografar dados. O algoritmo DSA foi criado no NSA (National Security Agency), a agência de segurança dos Estados Unidos com funções relacionadas a inteligência de sinais incluindo a interceptação e criptoanálise, e é patenteado pelo governo dos Estados Unidos. O NIST, uma agência governamental não regulatória da administração de tecnologia do Departamento de Comércio dos Estados Unidos que tem como missão promover a inovação e competitividade industrial dos Estados Unidos, aprovou o DSA como um padrão de assinatura digital federal.

Antes de verificarmos melhor o DSA devemos saber a relação entre o DSS e o DSA. O Digital Signature Standard (DSS) ou Padrão de Assinatura Digital é baseado em um método de criptografia com chave pública que utiliza o Digital Signature Algorithm (DSA) ou Algoritmo de Assinatura Digital. DSS é o formato das assinaturas digitais apoiado pelo governo dos Estados Unidos. O algoritmo DSA consiste de uma chave privada que somente o criador do documento (assinante) conhece e de uma chave pública.

Inicialmente quando o DSA foi proposto pela primeira vez, a Indústria RSA de Segurança de Dados e as companhias que já haviam autorizado o algoritmo RSA lançaram uma campanha contra o algoritmo DSA. Essa campanha contra o DSA foi por causa de uma possível "porta de armadilha" que poderia permitir ao governo dos Estados Unidos quebrar o DSA. A insatisfação foi com relação a velocidade do algoritmo e o tamanho da chave.

Quando o padrão foi proposto o tamanho da chave era de 512 bits, que é considerado pequeno. O padrão final permite chaves de até 1025 bits, que é mais do que o suficiente para necessidades de segurança.

As matemáticas são muito diferentes do RSA, mas a segurança é semelhante para chaves de tamanhos semelhantes. Por isso, qualquer grande avanço na quebra do RSA também implicará um avanço semelhante na quebra do RSA e vice-versa.

Em termos de velocidade e eficiência entre o DSA e o RSA as publicações realizadas pela Indústria RSA de Segurança de Dados mostram que o RSA é melhor que o DAS. Já as publicações realizadas pelo NIST mostram que o DSA é melhor que o RSA. Em termos gerais utilizando o RSA temos um tempo maior para assinar uma mensagem do que verificar uma assinatura. Com o DSA tanto a operação de assinatura quanto a operação de verificação levam a mesma quantidade de tempo.

O funcionamento do algoritmo não é muito complicado conforme veremos nos próximos passos a serem seguidos.

A primeira parte do algoritmo DSA é a geração da chave pública e da chave privada, conforme descrito abaixo:

  • Escolher um número primo q, que é chamado de divisor primo.
  • Escolher outro número primo p, tal que p-1 mod q=0. p é chamado de módulo principal.
  • Escolher um inteiro g, tal que 1 < g < p, g**q mod p = 1 e g = h**((p–1)/q) mod p.
  • Escolher um inteiro, tal que 0 < x < q.
  • Computar y como g**x mod p.
  • Empacotar a chave pública como {p, q, g, y}.
  • Empacotar a chave privada como {p, q, g, x}.

A segunda parte do algoritmo DSA é a geração da assinatura e a verificação da assinatura, que pode serão descritas em dois passos principais.

Para gerar uma assinatura de uma mensagem, o remetente pode seguir estes passos:

  • Gerar o Message Digest h, usando um algoritmo de hash como SHA1.
  • Gerar um número aleatório k, tal que 0 < k < q.
  • Computar r como (g**k mod p) mod q. Se r = 0, selecionar um k diferente.
  • Computar i, tal que k*i mod q = 1.
  • Computar s = i*(h+r*x) mod q. Se s=0, selecionar um k diferente.
  • Empacotar a assinatura digital como {r,s}.

Para verificar uma assinatura de mensagem, o receptor da mensagem e da assinatura digital pode seguir os seguintes passos:

  • Gerar o Message Digest h, usando o mesmo algoritmo de hash.
  • Computar w, tal que s*w mod q = 1.
  • Computar u1 = h*w mod q.
  • Computar u2 = r*w mod q.
  • Computar v = (((g**u1)*(y**u2)) mod p) mod q.
  • Se v==r, a assinatura digital é válida.

Para demonstrar um exemplo simples do funcionamento do DSA, vamos tentar com o menor divisor primo q = 3 e um módulo primo p = 7.

O processo de geração da chave pública e da chave privada é ilustrada abaixo:

  • q = 3, onde o divisor primo é selecionado;
  • p = 7, módulo primo computado: (p-1) mod q = 0;
  • g = 4, computado através de 1 < g < p, g**q mod p = 1 e g = h**((p–1)/q) mod p, assim 4**3 mod 7 = 1: 64 mod 7 = 1;
  • x = 5, selecionado: 0 < x < q;
  • y = 2, computado: y = g**x mod p = 4**5 mod 7;
  • {7,3,4,2}, a chave pública: {p,q,g,y}.
  • {7,3,4,5}, a chave privada: {p,q,g,x}.

Com a chave privada {p,q,g,x}={7,3,4,5}, o processo de geração da assinatura digital de uma mensagem de valor de hash h = 3 pode ser ilustrado como:

  • h = 3, valor do hash;
  • k = 2, selecionado: 0 < k < q;
  • r = 2, onde temos que r = (g**k mod p) mod q = (4**2 mod 7) mod 3;
  • i = 5, onde temos que k*i mod q = 1: 2*i mod 3 = 1;
  • s = 2, onde temos que s = i*(h+r*x) mod q = 5*(3+2*5) mod 3;
  • {2,2}, que é a assinatura digital.

O processo de verificação da assinatura digital {r, s} = {2,2} com a chave pública {p, q, g, y} = {7,3,4,2} pode ser ilustrado como:

  • h = 3, o valor do hash;
  • w = 5, onde temos que s*w mod q = 1: 2*w mod 3 = 1;
  • u1 = 0, onde temos que u1 = h*w mod q = 3*5 mod 3 = 0;
  • u2 = 1, onde temos que u2 = r*w mod q = 2*5 mod 3 = 1;
  • v = 2, onde temos que v = (((g**u1)*(y**u2)) mod p) mod q que é igual a (((4**0)*(2**1)) mod 7) mod 3 = 2;
  • v == r, portanto a verificação está Ok.

Dessa forma, o valor de verificação corresponde ao valor esperado.

O autor William Stallings provou através de um teorema que o DSA é legítimo e tem fundamento, portanto ele realmente funciona. Para quem estiver mais interessado sobre o assunto pode visitar o site e verificar como Stallings provou o correto funcionamento do DSA.

Exemplificando a Assinatura Digital em Java

Primeiramente mostraremos abaixo a parte do remetente que é responsável por gerar uma mensagem e enviar a mensagem com uma assinatura digital e a chave pública para o destinatário. Segue na Listagem 9 o código do remetente.


import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.SignatureException;


public class RemetenteAssiDig {

     private PublicKey pubKey;
     
     public PublicKey getPubKey() {
           return pubKey;
     }

     public void setPubKey(PublicKey pubKey) {
           this.pubKey = pubKey;
     }
     

     public byte[] geraAssinatura(String mensagem) throws NoSuchAlgorithmException, 
     InvalidKeyException, SignatureException {
           Signature sig = Signature.getInstance("DSA");
         
         //Geração das chaves públicas e privadas
         KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
         SecureRandom secRan = new SecureRandom();  
         kpg.initialize(512, secRan);  
         KeyPair keyP = kpg.generateKeyPair();  
         this.pubKey = keyP.getPublic();  
         PrivateKey priKey = keyP.getPrivate();
         
         //Inicializando Obj Signature com a Chave Privada
         sig.initSign(priKey);
         
         //Gerar assinatura
         sig.update(mensagem.getBytes());  
         byte[] assinatura = sig.sign(); 
         
         return assinatura;
     }
     
}
Listagem 9. Exemplo de código do remetente

Também devemos criar o destinatário que é quem vai receber a mensagem, a assinatura digital e a chave pública para fazer a validação da mensagem recebida. Segue na Listagem 10 o código do destinatário.


import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;


public class DestinatarioAssiDig {

   public void recebeMensagem(PublicKey pubKey, String mensagem, byte[] assinatura) throws 
   NoSuchAlgorithmException, InvalidKeyException, SignatureException {
       Signature clientSig = Signature.getInstance("DSA");  
       clientSig.initVerify(pubKey);  
       clientSig.update(mensagem.getBytes());  
         
       if (clientSig.verify(assinatura)) {  
           //Mensagem corretamente assinada
          System.out.println("A Mensagem recebida foi assinada corretamente.");
       } else {
           //Mensagem não pode ser validada
          System.out.println("A Mensagem recebida NÃO pode ser validada.");
       }  
   }
   
}
Listagem 10. Exemplo de código do destinatário

Agora podemos testar os códigos através de uma classe principal que gera diferentes tipos de mensagens, assinaturas e chaves para validar a mensagem por um destinatário. Segue na Listagem 11 o código.


import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SignatureException;


public class AssinaturaDigital {

 public static void main(String args[]) throws NoSuchAlgorithmException, 
 InvalidKeyException, SignatureException {
     //Remetente Gera Assinatura Digital para uma Mensagem
       RemetenteAssiDig remetenteAssiDig = new RemetenteAssiDig();
       String mensagem = "Exemplo de mensagem.";
     byte[] assinatura = remetenteAssiDig.geraAssinatura(mensagem);
     //Guarda Chave Pública para ser Enviada ao Destinatário
     PublicKey pubKey = remetenteAssiDig.getPubKey();

     //Destinatário recebe dados correto
     DestinatarioAssiDig destinatarioAssiDig = new DestinatarioAssiDig();
     destinatarioAssiDig.recebeMensagem(pubKey, mensagem, assinatura);
     
     //Destinatário recebe mensagem alterada
     String msgAlterada = "Exemplo de mensagem alterada.";
     destinatarioAssiDig.recebeMensagem(pubKey, msgAlterada, assinatura);

     //Criando outra Assinatura
       String mensagem2 = "Exemplo de outra mensagem.";  
     byte[] assinatura2 = remetenteAssiDig.geraAssinatura(mensagem2);
     //Guarda Chave Pública para ser Enviada ao Destinatário
     PublicKey pubKey2 = remetenteAssiDig.getPubKey();

     //Destinatário recebe outra assinatura
     destinatarioAssiDig.recebeMensagem(pubKey, mensagem, assinatura2);
     
     //Destinatário recebe outra chave pública
     destinatarioAssiDig.recebeMensagem(pubKey2, mensagem, assinatura);


 }
 
}
Listagem 11. Exemplo de código que testa o remetente e o destinatário

Para o código acima temos o resultado abaixo como saída:


A Mensagem recebida foi assinada corretamente.
A Mensagem recebida NÃO pode ser validada.
A Mensagem recebida NÃO pode ser validada.
A Mensagem recebida NÃO pode ser validada.

Neste artigo procuramos focar mais no algoritmo DSA que é bastante utilizado na indústria e conforme havia sido solicitado pelos leitores. Nos próximos artigos sobre o assunto falaremos mais sobre o Elliptic Curve Digital Signature Algorithm (ECDSA) e analisamos algumas das suas vantagens e diferenças em relação ao DSA. Aproveito o espaço para informar que estamos abertos a sugestões dos leitores aqui neste espaço ou por e-mail.

Bibliografia:
  • MessageDigest.
  • STALLINGS, William. Criptografia e segurança de redes: Princípios e práticas, 4 ed. São Paulo: Prentice Hall, 2008.