Firme digitali in Java

Java Top

Ho appena annunciato il nuovo corso Learn Spring , incentrato sui fondamenti di Spring 5 e Spring Boot 2:

>> SCOPRI IL CORSO

1. Panoramica

In questo tutorial, impareremo il meccanismo della firma digitale e come possiamo implementarlo utilizzando Java Cryptography Architecture (JCA) . Esploreremo le API KeyPair, MessageDigest, Cipher, KeyStore, Certificate e Signature JCA.

Inizieremo comprendendo cos'è la firma digitale, come generare una coppia di chiavi e come certificare la chiave pubblica da un'autorità di certificazione (CA). Successivamente, vedremo come implementare la firma digitale utilizzando le API JCA di basso livello e di alto livello.

2. Che cos'è la firma digitale?

2.1. Definizione della firma digitale

La firma digitale è una tecnica per garantire:

  • Integrità: il messaggio non è stato alterato durante il transito
  • Autenticità: l'autore del messaggio è davvero chi afferma di essere
  • Non ripudio: l'autore del messaggio non può in seguito negare che ne siano state la fonte

2.2. Invio di un messaggio con una firma digitale

Tecnicamente parlando, una firma digitale è l'hash crittografato (digest, checksum) di un messaggio . Ciò significa che generiamo un hash da un messaggio e lo crittografiamo con una chiave privata secondo un algoritmo scelto.

Il messaggio, l'hash crittografato, la chiave pubblica corrispondente e l'algoritmo vengono quindi inviati. Questo è classificato come messaggio con la sua firma digitale.

2.3. Ricezione e controllo di una firma digitale

Per controllare la firma digitale, il destinatario del messaggio genera un nuovo hash dal messaggio ricevuto, decrittografa l'hash crittografato ricevuto utilizzando la chiave pubblica e li confronta. Se corrispondono, si dice che la firma digitale è verificata.

Dobbiamo notare che crittografiamo solo l'hash del messaggio e non il messaggio stesso. In altre parole, la firma digitale non cerca di mantenere segreto il messaggio. La nostra firma digitale dimostra solo che il messaggio non è stato alterato durante il trasporto.

Quando la firma è verificata, siamo sicuri che solo il proprietario della chiave privata potrebbe essere l'autore del messaggio .

3. Certificato digitale e identità a chiave pubblica

Un certificato è un documento che associa un'identità a una determinata chiave pubblica. I certificati sono firmati da un'entità di terze parti chiamata Autorità di certificazione (CA).

Sappiamo che se l'hash decifrato con la chiave pubblica pubblicata corrisponde all'hash effettivo, il messaggio viene firmato. Tuttavia, come sappiamo che la chiave pubblica proviene davvero dall'entità giusta? Questo viene risolto mediante l'uso di certificati digitali.

Un certificato digitale contiene una chiave pubblica ed è a sua volta firmato da un'altra entità. La firma di quell'entità può essere essa stessa verificata da un'altra entità e così via. Finiamo per avere quella che chiamiamo una catena di certificati. Ogni entità superiore certifica la chiave pubblica dell'entità successiva. L'entità di primo livello è autofirmata, il che significa che la sua chiave pubblica è firmata dalla sua chiave privata.

L'X.509 è il formato di certificato più utilizzato e viene fornito in formato binario (DER) o in formato testo (PEM). JCA fornisce già un'implementazione per questo tramite la classe X509Certificate .

4. Gestione KeyPair

Poiché la firma digitale utilizza una chiave privata e una chiave pubblica, utilizzeremo le classi JCA PrivateKey e PublicKey rispettivamente per firmare e controllare un messaggio.

4.1. Ottenere un KeyPair

Per creare una coppia di chiavi di una chiave privata e pubblica , useremo lo strumento di chiave Java .

Generiamo una coppia di chiavi utilizzando il comando genkeypair :

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

Questo crea una chiave privata e la sua chiave pubblica corrispondente per noi. La chiave pubblica è racchiusa in un certificato autofirmato X.509 che a sua volta viene avvolto in una catena di certificati a elemento singolo. Memorizziamo la catena di certificati e la chiave privata nel file Keystore sender_keystore.p12 , che possiamo elaborare utilizzando l'API KeyStore.

Qui abbiamo utilizzato il formato di archivio chiavi PKCS12, poiché è lo standard e consigliato rispetto al formato JKS proprietario di Java. Inoltre, dovremmo ricordare la password e l'alias, poiché li useremo nella prossima sottosezione durante il caricamento del file Keystore.

4.2. Caricamento della chiave privata per la firma

Per firmare un messaggio, abbiamo bisogno di un'istanza di PrivateKey.

Utilizzando l' API KeyStore e il precedente file Keystore, sender_keystore.p12, possiamo ottenere un oggetto PrivateKey :

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

4.3. Pubblicazione della chiave pubblica

Prima di poter pubblicare la chiave pubblica, dobbiamo prima decidere se utilizzeremo un certificato autofirmato o un certificato firmato da una CA.

Quando si utilizza un certificato autofirmato, è sufficiente esportarlo dal file Keystore. Possiamo farlo con il comando exportcert :

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

Altrimenti, se lavoreremo con un certificato firmato da una CA, dobbiamo creare una richiesta di firma del certificato (CSR) . Lo facciamo con il 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.

Come sempre, il codice di questo articolo è disponibile su GitHub.

Fondo Java

Ho appena annunciato il nuovo corso Learn Spring , incentrato sui fondamenti di Spring 5 e Spring Boot 2:

>> SCOPRI IL CORSO