Assinaturas digitais em Java

Assinaturas digitais em Java

1. Visão geral

Neste tutorial, aprenderemos sobreDigital Signature mechanism and how we can implement it using the Java Cryptography Architecture (JCA). Exploraremos as APIs JCAKeyPair, MessageDigest, Cipher, KeyStore, Certificate, eSignature.

Começaremos entendendo o que é Assinatura Digital, como gerar um par de chaves e como certificar a chave pública de uma autoridade de certificação (CA). Depois disso, veremos como implementar a assinatura digital usando as APIs JCA de baixo e alto nível.

2. O que é assinatura digital?

2.1. Definição de assinatura digital

Assinatura digital é uma técnica para garantir:

  • Integridade: a mensagem não foi alterada em trânsito

  • Autenticidade: o autor da mensagem é realmente quem eles afirmam ser

  • Irrecusabilidade: o autor da mensagem não pode negar posteriormente que foi a fonte

2.2. Enviando uma mensagem com uma assinatura digital

Tecnicamente falando,adigital signature is the encrypted hash (digest, checksum) of a message. Isso significa que geramos um hash a partir de uma mensagem e o criptografamos com uma chave privada de acordo com o algoritmo escolhido.

A mensagem, o hash criptografado, a chave pública correspondente e o algoritmo são todos enviados. Isso é classificado como uma mensagem com sua assinatura digital.

2.3. Recebendo e verificando uma assinatura digital

Para verificar a assinatura digital, o receptor da mensagem gera um novo hash a partir da mensagem recebida, descriptografa o hash criptografado recebido usando a chave pública e os compara. Se eles corresponderem, a Assinatura Digital será confirmada.

We should note that we only encrypt the message hash, and not the message itself. Em outras palavras, a assinatura digital não tenta manter a mensagem secreta. Nossa assinatura digital prova apenas que a mensagem não foi alterada em trânsito.

When the signature is verified, we’re sure that only the owner of the private key could be the author of the message.

3. Certificado Digital e Identidade de chave pública

A certificate is a document that associates an identity to a given public key. Os certificados são assinados por uma entidade de terceiros chamada Autoridade de Certificação (CA).

Sabemos que, se o hash descriptografado com a chave pública publicada corresponder ao hash real, a mensagem será assinada. No entanto, como sabemos que a chave pública realmente veio da entidade certa? Isso é resolvido pelo uso de certificados digitais.

A Digital Certificate contains a public key and is itself signed by another entity. A assinatura dessa entidade pode ser verificada por outra entidade e assim por diante. Acabamos tendo o que chamamos de cadeia de certificados. Cada entidade principal certifica a chave pública da próxima entidade. A entidade de nível superior é autoassinada, o que significa que sua chave pública é assinada por sua própria chave privada.

O X.509 é o formato de certificado mais usado e é enviado como formato binário (DER) ou formato de texto (PEM). JCA já fornece uma implementação para isso por meio da classeX509Certificate.

4. KeyPair Management

Como a Assinatura Digital usa uma chave privada e pública, usaremos as classes JCAPrivateKey ePublicKey para assinar e verificar uma mensagem, respectivamente.

4.1. Obtendo um KeyPair

Para criar um par de chaves de uma chave privada e pública,, usaremos o Javakeytool.

Vamos gerar um par de chaves usando o comandogenkeypair:

keytool -genkeypair -alias senderKeyPair -keyalg RSA -keysize 2048 \
  -dname "CN=example" -validity 365 -storetype PKCS12 \
  -keystore sender_keystore.p12 -storepass changeit

Isso cria uma chave privada e sua chave pública correspondente para nós. A chave pública é agrupada em um certificado autoassinado X.509 que, por sua vez, é agrupado em uma cadeia de certificados de elemento único. Armazenamos a cadeia de certificados e a chave privada no arquivo Keystoresender_keystore.p12, que podemos processar usandoKeyStore API.

Aqui, usamos o formato de armazenamento de chaves PKCS12, pois é o padrão e recomendado em vez do formato JKS proprietário do Java. Além disso, devemos lembrar a senha e o alias, pois os usaremos na próxima subseção ao carregar o arquivo Keystore.

4.2. Carregando a chave privada para assinatura

Para assinar uma mensagem, precisamos de uma instância doPrivateKey.

Usando a APIKeyStore e o arquivo Keystore anterior,sender_keystore.p12,, podemos obter um objetoPrivateKey:

KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(new FileInputStream("sender_keystore.p12"), "changeit");
PrivateKey privateKey =
  (PrivateKey) keyStore.getKey("senderKeyPair", "changeit");

4.3. Publicando a chave pública

Antes de publicar a chave pública, devemos primeiro decidir se vamos usar um certificado autoassinado ou um certificado assinado por CA.

When using a self-signed certificate, we need only to export it from the Keystore file. Podemos fazer isso com o comandoexportcert:

keytool -exportcert -alias senderKeyPair -storetype PKCS12 \
  -keystore sender_keystore.p12 -file \
  sender_certificate.cer -rfc -storepass changeit

Caso contrário,if we’re going to work with a CA-signed certificate, then we need to create a certificate signing request (CSR). Fazemos isso com o comandocertreq:

keytool -certreq -alias senderKeyPair -storetype PKCS12 \
  -keystore sender_keystore.p12 -file -rfc \
  -storepass changeit > sender_certificate.csr

O arquivo CSR,sender_certificate.csr,, é então enviado a uma Autoridade de Certificação para fins de assinatura. Quando isso for feito, receberemos uma chave pública assinada embrulhada em um certificado X.509, em formato binário (DER) ou texto (PEM). Aqui, usamos a opçãorfc para um formato PEM.

A chave pública que recebemos do CA,sender_certificate.cer,, agora foi assinada por um CA e pode ser disponibilizada para os clientes.

4.4. Carregando uma chave pública para verificação

Tendo acesso à chave pública, um receptor pode carregá-la em seu Keystore usando o comandoimportcert:

keytool -importcert -alias receiverKeyPair -storetype PKCS12 \
  -keystore receiver_keystore.p12 -file \
  sender_certificate.cer -rfc -storepass changeit

E usando a APIKeyStore como antes, podemos obter uma instânciaPublicKey:

KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(new FileInputStream("receiver_keytore.p12"), "changeit");
Certificate certificate = keyStore.getCertificate("receiverKeyPair");
PublicKey publicKey = certificate.getPublicKey();

Agora que temos uma instânciaPrivateKey no lado do remetente e uma instância doPublicKey no lado do receptor, podemos iniciar o processo de assinatura e verificação.

5. Assinatura digital com classesMessageDigesteCipher

Como vimos, a assinatura digital é baseada em hash e criptografia.

Normalmente, usamos a classeMessageDigest comSHA ouMD5 para hashing e a classeCipher para criptografia.

Agora, vamos começar a implementar os mecanismos de assinatura digital.

5.1. Gerando um hash de mensagem

Uma mensagem pode ser uma sequência, um arquivo ou qualquer outro dado. Então, vamos pegar o conteúdo de um arquivo simples:

byte[] messageBytes = Files.readAllBytes(Paths.get("message.txt"));

Agora, usandoMessageDigest,, vamos usar o métododigest para gerar um hash:

MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] messageHash = md.digest(messageBytes);

Aqui, usamos o algoritmo SHA-256, que é o mais comumente usado. Outras alternativas são MD5, SHA-384 e SHA-512.

5.2. Criptografando o Hash Gerado

Para criptografar uma mensagem, precisamos de um algoritmo e de uma chave privada. Aqui, usaremos o algoritmo RSA. O algoritmo DSA é outra opção.

Vamos criar uma instânciaCipher e inicializá-la para criptografia. Em seguida, chamaremos o métododoFinal() para criptografar a mensagem com hash anterior:

Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] digitalSignature = cipher.doFinal(messageHash);

A assinatura pode ser salva em um arquivo para enviá-lo mais tarde:

Files.write(Paths.get("digital_signature_1"), digitalSignature);

Nesse momento, a mensagem, a assinatura digital, a chave pública e o algoritmo são todos enviados, e o receptor pode usar essas informações para verificar a integridade da mensagem.

5.3. Verificando assinatura

Quando recebemos uma mensagem, devemos verificar sua assinatura. Para fazer isso, descriptografamos o hash criptografado recebido e o comparamos com um hash da mensagem recebida.

Vamos ler a assinatura digital recebida:

byte[] encryptedMessageHash =
  Files.readAllBytes(Paths.get("digital_signature_1"));

Para a descriptografia, criamos uma instânciaCipher. Então chamamos o métododoFinal:

Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
byte[] decryptedMessageHash = cipher.doFinal(encryptedMessageHash);

Em seguida, geramos um novo hash de mensagem a partir da mensagem recebida:

byte[] messageBytes = Files.readAllBytes(Paths.get("message.txt"));

MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] newMessageHash = md.digest(messageBytes);

E finalmente, verificamos se o hash da mensagem recém-gerado corresponde ao descriptografado:

boolean isCorrect = Arrays.equals(decryptedMessageHash, newMessageHash);

Neste exemplo, usamos o arquivo de textomessage.txt para simular uma mensagem que queremos enviar, ou a localização do corpo de uma mensagem que recebemos. Normalmente, esperamos receber nossa mensagem junto com a assinatura.

6. Assinatura digital usando a classeSignature

Até agora, usamos as APIs de baixo nível para construir nosso próprio processo de verificação de assinatura digital. Isso nos ajuda a entender como funciona e nos permite personalizá-lo.

No entanto, o JCA já oferece uma API dedicada na forma da classeSignature.

6.1. Assinando uma Mensagem

Para iniciar o processo de assinatura, primeiro criamos uma instância da classeSignature. Para fazer isso, precisamos de um algoritmo de assinatura. Em seguida, inicializamos oSignature com nossa chave privada:

Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);

O algoritmo de assinatura que escolhemos,SHA256withRSA neste exemplo, é uma combinação de um algoritmo de hash e um algoritmo de criptografia. Outras alternativas incluemSHA1withRSA,SHA1withDSA eMD5withRSA, entre outras.

Em seguida, prosseguimos para assinar a matriz de bytes da mensagem:

byte[] messageBytes = Files.readAllBytes(Paths.get("message.txt"));

signature.update(messageBytes);
byte[] digitalSignature = signature.sign();

Podemos salvar a assinatura em um arquivo para transmissão posterior:

Files.write(Paths.get("digital_signature_2"), digitalSignature);

6.2. Verificando a assinatura

Para verificar a assinatura recebida, criamos novamente uma instânciaSignature:

Signature signature = Signature.getInstance("SHA256withRSA");

Em seguida, inicializamos o objetoSignature para verificação chamando o métodoinitVerify, que usa uma chave pública:

signature.initVerify(publicKey);

Em seguida, precisamos adicionar os bytes da mensagem recebida ao objeto de assinatura invocando o métodoupdate:

byte[] messageBytes = Files.readAllBytes(Paths.get("message.txt"));

signature.update(messageBytes);

E, finalmente, podemos chamar a verificação da assinatura chamando o métodoverify:

boolean isCorrect = signature.verify(receivedSignature);

7. Conclusão

Neste artigo, examinamos primeiro como funciona a assinatura digital e como estabelecer a confiança para um certificado digital. Em seguida, implementamos uma assinatura digital usando as classesMessageDigest,Cipher,eSignature da arquitetura de criptografia Java.

Vimos em detalhes como assinar dados usando a chave privada e como verificar a assinatura usando uma chave pública.

Como sempre, o código deste artigo está disponívelover on GitHub.