Como criar uma Assinatura Digital em Java

Veja neste artigo como podemos realizar o processo de assinatura digital, o que é o algoritmo DSA e também veja um exemplo prático de assinatura digital em Java mostrando como funciona todo o processo no remetente e no destinatário.

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:

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:

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

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:

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:

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:

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.

Artigos relacionados