Firmas digitales en Java

Parte superior de Java

Acabo de anunciar el nuevo curso Learn Spring , centrado en los fundamentos de Spring 5 y Spring Boot 2:

>> VER EL CURSO

1. Información general

En este tutorial, aprenderemos sobre el mecanismo de firma digital y cómo podemos implementarlo utilizando la Arquitectura de criptografía de Java (JCA) . Exploraremos las API JCA KeyPair, MessageDigest, Cipher, KeyStore, Certificate y Signature .

Comenzaremos por comprender qué es la firma digital, cómo generar un par de claves y cómo certificar la clave pública de una autoridad de certificación (CA). Después de eso, veremos cómo implementar la firma digital utilizando las API de JCA de bajo y alto nivel.

2. ¿Qué es la firma digital?

2.1. Definición de firma digital

La firma digital es una técnica para garantizar:

  • Integridad: el mensaje no ha sido alterado en tránsito
  • Autenticidad: el autor del mensaje es realmente quien dice ser
  • No repudio: el autor del mensaje no puede luego negar que fue la fuente

2.2. Envío de un mensaje con firma digital

Técnicamente hablando, una firma digital es el hash cifrado (resumen, suma de comprobación) de un mensaje . Eso significa que generamos un hash a partir de un mensaje y lo ciframos con una clave privada de acuerdo con un algoritmo elegido.

A continuación, se envían el mensaje, el hash cifrado, la clave pública correspondiente y el algoritmo. Esto se clasifica como un mensaje con su firma digital.

2.3. Recibir y verificar una firma digital

Para verificar la firma digital, el receptor del mensaje genera un nuevo hash del mensaje recibido, descifra el hash cifrado recibido usando la clave pública y los compara. Si coinciden, se dice que la firma digital está verificada.

Debemos tener en cuenta que solo encriptamos el hash del mensaje y no el mensaje en sí. En otras palabras, la firma digital no intenta mantener el mensaje en secreto. Nuestra firma digital solo demuestra que el mensaje no se modificó en tránsito.

Cuando se verifica la firma, estamos seguros de que solo el propietario de la clave privada podría ser el autor del mensaje .

3. Certificado digital e identidad de clave pública

Un certificado es un documento que asocia una identidad a una clave pública determinada. Los certificados están firmados por una entidad de terceros llamada Autoridad de certificación (CA).

Sabemos que si el hash que desciframos con la clave pública publicada coincide con el hash real, entonces el mensaje está firmado. Sin embargo, ¿cómo sabemos que la clave pública realmente provino de la entidad correcta? Esto se resuelve mediante el uso de certificados digitales.

Un certificado digital contiene una clave pública y está firmado por otra entidad. La firma de esa entidad puede ser verificada por otra entidad y así sucesivamente. Terminamos teniendo lo que llamamos una cadena de certificados. Cada entidad superior certifica la clave pública de la siguiente entidad. La entidad de nivel más alto está autofirmada, lo que significa que su clave pública está firmada por su propia clave privada.

El X.509 es el formato de certificado más utilizado y se envía como formato binario (DER) o formato de texto (PEM). JCA ya proporciona una implementación para esto a través de la clase X509Certificate .

4. Gestión de pares de claves

Dado que Digital Signature usa una clave pública y privada, usaremos las clases JCA PrivateKey y PublicKey para firmar y verificar un mensaje, respectivamente.

4.1. Obtener un par de llaves

Para crear un par de claves de una clave pública y privada , usaremos la herramienta de claves de Java .

Generemos un par de claves usando el comando genkeypair :

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

Esto crea una clave privada y su clave pública correspondiente para nosotros. La clave pública está envuelta en un certificado autofirmado X.509 que a su vez se envuelve en una cadena de certificados de un solo elemento. Almacenamos la cadena de certificados y la clave privada en el archivo de Keystore sender_keystore.p12 , que podemos procesar usando la API de KeyStore.

Aquí, hemos utilizado el formato de almacenamiento de claves PKCS12, ya que es el estándar y se recomienda sobre el formato JKS propietario de Java. Además, debemos recordar la contraseña y el alias, ya que los usaremos en la siguiente subsección cuando carguemos el archivo de almacén de claves.

4.2. Carga de la clave privada para firmar

Para firmar un mensaje, necesitamos una instancia de PrivateKey.

Usando la API de KeyStore y el archivo Keystore anterior, sender_keystore.p12, podemos obtener un objeto PrivateKey :

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

4.3. Publicar la clave pública

Antes de que podamos publicar la clave pública, primero debemos decidir si vamos a utilizar un certificado autofirmado o un certificado firmado por una CA.

Cuando usamos un certificado autofirmado, solo necesitamos exportarlo desde el archivo Keystore. Podemos hacer esto con el comando exportcert :

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

De lo contrario, si vamos a trabajar con un certificado firmado por una CA, entonces necesitamos crear una solicitud de firma de certificado (CSR) . Hacemos esto con el comando certreq :

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

The CSR file, sender_certificate.csr, is then sent to a Certificate Authority for the purpose of signing. When this is done, we'll receive a signed public key wrapped in an X.509 certificate, either in binary (DER) or text (PEM) format. Here, we've used the rfc option for a PEM format.

The public key we received from the CA, sender_certificate.cer, has now been signed by a CA and can be made available for clients.

4.4. Loading a Public Key for Verification

Having access to the public key, a receiver can load it into their Keystore using the importcert command:

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

And using the KeyStore API as before, we can get a PublicKey instance:

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

Now that we have a PrivateKey instance on the sender side, and an instance of the PublicKey on the receiver side, we can start the process of signing and verification.

5. Digital Signature With MessageDigest and Cipher Classes

As we have seen, the digital signature is based on hashing and encryption.

Usually, we use the MessageDigest class with SHA or MD5 for hashing and the Cipher class for encryption.

Now, let's start implementing the digital signature mechanisms.

5.1. Generating a Message Hash

A message can be a string, a file, or any other data. So let's take the content of a simple file:

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

Now, using MessageDigest, let's use the digest method to generate a hash:

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

Here, we've used the SHA-256 algorithm, which is the one most commonly used. Other alternatives are MD5, SHA-384, and SHA-512.

5.2. Encrypting the Generated Hash

To encrypt a message, we need an algorithm and a private key. Here we'll use the RSA algorithm. The DSA algorithm is another option.

Let's create a Cipher instance and initialize it for encryption. Then we'll call the doFinal() method to encrypt the previously hashed message:

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

The signature can be saved into a file for sending it later:

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

At this point, the message, the digital signature, the public key, and the algorithm are all sent, and the receiver can use these pieces of information to verify the integrity of the message.

5.3. Verifying Signature

When we receive a message, we must verify its signature. To do so, we decrypt the received encrypted hash and compare it with a hash we make of the received message.

Let's read the received digital signature:

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

For decryption, we create a Cipher instance. Then we call the doFinal method:

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

Next, we generate a new message hash from the received message:

byte[] messageBytes = Files.readAllBytes(Paths.get("message.txt")); MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] newMessageHash = md.digest(messageBytes);

And finally, we check if the newly generated message hash matches the decrypted one:

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

In this example, we've used the text file message.txt to simulate a message we want to send, or the location of the body of a message we've received. Normally, we'd expect to receive our message alongside the signature.

6. Digital Signature Using the Signature Class

So far, we've used the low-level APIs to build our own digital signature verification process. This helps us understand how it works and allows us to customize it.

However, JCA already offers a dedicated API in the form of the Signature class.

6.1. Signing a Message

To start the process of signing, we first create an instance of the Signature class. To do that, we need a signing algorithm. We then initialize the Signature with our private key:

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

The signing algorithm we chose, SHA256withRSA in this example, is a combination of a hashing algorithm and an encryption algorithm. Other alternatives include SHA1withRSA, SHA1withDSA, and MD5withRSA, among others.

Next, we proceed to sign the byte array of the message:

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

We can save the signature into a file for later transmission:

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

6.2. Verifying the Signature

To verify the received signature, we again create a Signature instance:

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

Next, we initialize the Signature object for verification by calling the initVerify method, which takes a public key:

signature.initVerify(publicKey);

Then, we need to add the received message bytes to the signature object by invoking the update method:

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

And finally, we can check the signature by calling the verify method:

boolean isCorrect = signature.verify(receivedSignature);

7. Conclusion

In this article, we first looked at how digital signature works and how to establish trust for a digital certificate. Then we implemented a digital signature using the MessageDigest,Cipher, and Signature classes from the Java Cryptography Architecture.

We saw in detail how to sign data using the private key and how to verify the signature using a public key.

Como siempre, el código de este artículo está disponible en GitHub.

Fondo de Java

Acabo de anunciar el nuevo curso Learn Spring , centrado en los fundamentos de Spring 5 y Spring Boot 2:

>> VER EL CURSO