Signatures numériques à Java

Haut Java

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS

1. Vue d'ensemble

Dans ce didacticiel, nous allons découvrir le mécanisme de signature numérique et comment nous pouvons l'implémenter à l'aide de Java Cryptography Architecture (JCA) . Nous explorerons les API JCA KeyPair, MessageDigest, Cipher, KeyStore, Certificate et Signature .

Nous commencerons par comprendre ce qu'est la signature numérique, comment générer une paire de clés et comment certifier la clé publique auprès d'une autorité de certification (CA). Ensuite, nous verrons comment implémenter la signature numérique à l'aide des API JCA de bas niveau et de haut niveau.

2. Qu'est-ce que la signature numérique?

2.1. Définition de la signature numérique

La signature numérique est une technique pour assurer:

  • Intégrité: le message n'a pas été altéré en transit
  • Authenticité: l'auteur du message est vraiment celui qu'il prétend être
  • Non-répudiation: l'auteur du message ne peut plus tard nier qu'il en est la source

2.2. Envoi d'un message avec une signature numérique

Techniquement parlant, une signature numérique est le hachage chiffré (résumé, somme de contrôle) d'un message . Cela signifie que nous générons un hachage à partir d'un message et le chiffrons avec une clé privée selon un algorithme choisi.

Le message, le hachage chiffré, la clé publique correspondante et l'algorithme sont ensuite tous envoyés. Ceci est classé comme un message avec sa signature numérique.

2.3. Réception et vérification d'une signature numérique

Pour vérifier la signature numérique, le destinataire du message génère un nouveau hachage à partir du message reçu, déchiffre le hachage chiffré reçu à l'aide de la clé publique et les compare. S'ils correspondent, la signature numérique est dite vérifiée.

Nous devons noter que nous ne chiffrons que le hachage du message, et non le message lui-même. En d'autres termes, la signature numérique n'essaie pas de garder le message secret. Notre signature numérique prouve seulement que le message n'a pas été altéré en transit.

Lorsque la signature est vérifiée, nous sommes sûrs que seul le propriétaire de la clé privée peut être l'auteur du message .

3. Certificat numérique et identité de clé publique

Un certificat est un document qui associe une identité à une clé publique donnée. Les certificats sont signés par une entité tierce appelée autorité de certification (CA).

Nous savons que si le hachage que nous déchiffrons avec la clé publique publiée correspond au hachage réel, le message est signé. Cependant, comment savons-nous que la clé publique provient vraiment de la bonne entité? Ceci est résolu par l'utilisation de certificats numériques.

Un certificat numérique contient une clé publique et est lui-même signé par une autre entité. La signature de cette entité peut elle-même être vérifiée par une autre entité et ainsi de suite. Nous finissons par avoir ce que nous appelons une chaîne de certificats. Chaque entité supérieure certifie la clé publique de l'entité suivante. L'entité de plus haut niveau est auto-signée, ce qui signifie que sa clé publique est signée par sa propre clé privée.

Le X.509 est le format de certificat le plus utilisé, et il est livré soit au format binaire (DER), soit au format texte (PEM). JCA fournit déjà une implémentation pour cela via la classe X509Certificate .

4. Gestion de KeyPair

Puisque la signature numérique utilise une clé privée et une clé publique, nous utiliserons les classes JCA PrivateKey et PublicKey pour signer et vérifier un message, respectivement.

4.1. Obtenir un KeyPair

Pour créer une paire de clés d'une clé privée et publique , nous utiliserons l' outil de clé Java .

Générons une paire de clés à l'aide de la commande genkeypair :

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

This creates a private key and its corresponding public key for us. The public key is wrapped into an X.509 self-signed certificate which is wrapped in turn into a single-element certificate chain. We store the certificate chain and the private key in the Keystore file sender_keystore.p12, which we can process using the KeyStore API.

Here, we've used the PKCS12 key store format, as it is the standard and recommended over the Java-proprietary JKS format. Also, we should remember the password and alias, as we'll use them in the next subsection when loading the Keystore file.

4.2. Loading the Private Key for Signing

In order to sign a message, we need an instance of the PrivateKey.

Using the KeyStore API, and the previous Keystore file, sender_keystore.p12, we can get a PrivateKey object:

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

4.3. Publishing the Public Key

Before we can publish the public key, we must first decide whether we're going to use a self-signed certificate or a CA-signed certificate.

When using a self-signed certificate, we need only to export it from the Keystore file. We can do this with the exportcert command:

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

Otherwise, if we're going to work with a CA-signed certificate, then we need to create a certificate signing request (CSR). We do this with the certreq command:

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.

Comme toujours, le code de cet article est disponible à l'adresse over sur GitHub.

Fond Java

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS