Digitale Signaturen in Java

Java Top

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> Überprüfen Sie den Kurs

1. Übersicht

In diesem Tutorial lernen wir den Mechanismus der digitalen Signatur kennen und wie wir ihn mithilfe der Java Cryptography Architecture (JCA) implementieren können . Wir werden die KeyPair-, MessageDigest-, Cipher-, KeyStore-, Certificate- und Signature- JCA-APIs untersuchen.

Wir beginnen damit, zu verstehen, was digitale Signatur ist, wie ein Schlüsselpaar generiert wird und wie der öffentliche Schlüssel von einer Zertifizierungsstelle (CA) zertifiziert wird. Danach erfahren Sie, wie Sie die digitale Signatur mithilfe der JCA-APIs auf niedriger und hoher Ebene implementieren.

2. Was ist digitale Signatur?

2.1. Definition der digitalen Signatur

Die digitale Signatur ist eine Technik, um Folgendes sicherzustellen:

  • Integrität: Die Nachricht wurde während der Übertragung nicht geändert
  • Authentizität: Der Autor der Nachricht ist wirklich der, für den er sich ausgibt
  • Nicht-Zurückweisung: Der Autor der Nachricht kann später nicht leugnen, dass sie die Quelle waren

2.2. Senden einer Nachricht mit digitaler Signatur

Technisch gesehen ist eine digitale Signatur der verschlüsselte Hash (Digest, Prüfsumme) einer Nachricht . Das heißt, wir generieren einen Hash aus einer Nachricht und verschlüsseln ihn mit einem privaten Schlüssel gemäß einem ausgewählten Algorithmus.

Die Nachricht, der verschlüsselte Hash, der entsprechende öffentliche Schlüssel und der Algorithmus werden dann alle gesendet. Dies wird als Nachricht mit ihrer digitalen Signatur klassifiziert.

2.3. Empfangen und Überprüfen einer digitalen Signatur

Um die digitale Signatur zu überprüfen, generiert der Nachrichtenempfänger aus der empfangenen Nachricht einen neuen Hash, entschlüsselt den empfangenen verschlüsselten Hash mit dem öffentlichen Schlüssel und vergleicht sie. Wenn sie übereinstimmen, wird die digitale Signatur als verifiziert bezeichnet.

Wir sollten beachten, dass wir nur den Nachrichten-Hash verschlüsseln und nicht die Nachricht selbst. Mit anderen Worten, Digital Signature versucht nicht, die Nachricht geheim zu halten. Unsere digitale Signatur beweist nur, dass die Nachricht während der Übertragung nicht geändert wurde.

Wenn die Signatur überprüft wird, können wir sicher sein, dass nur der Eigentümer des privaten Schlüssels der Autor der Nachricht sein kann .

3. Digitales Zertifikat und Identität des öffentlichen Schlüssels

Ein Zertifikat ist ein Dokument, das einem bestimmten öffentlichen Schlüssel eine Identität zuordnet. Zertifikate werden von einem Drittanbieter namens Certificate Authority (CA) signiert.

Wir wissen, dass die Nachricht signiert ist, wenn der Hash, den wir mit dem veröffentlichten öffentlichen Schlüssel entschlüsseln, mit dem tatsächlichen Hash übereinstimmt. Woher wissen wir jedoch, dass der öffentliche Schlüssel wirklich von der richtigen Entität stammt? Dies wird durch die Verwendung digitaler Zertifikate gelöst.

Ein digitales Zertifikat enthält einen öffentlichen Schlüssel und ist selbst von einer anderen Entität signiert. Die Signatur dieser Entität kann selbst von einer anderen Entität überprüft werden und so weiter. Am Ende haben wir eine sogenannte Zertifikatskette. Jede Top-Entität zertifiziert den öffentlichen Schlüssel der nächsten Entität. Die Entität der obersten Ebene ist selbstsigniert, was bedeutet, dass sein öffentlicher Schlüssel von seinem eigenen privaten Schlüssel signiert wird.

Das X.509 ist das am häufigsten verwendete Zertifikatformat und wird entweder als Binärformat (DER) oder als Textformat (PEM) geliefert. JCA bietet hierfür bereits eine Implementierung über die X509Certificate- Klasse an.

4. KeyPair-Verwaltung

Da Digital Signature einen privaten und einen öffentlichen Schlüssel verwendet, verwenden wir die JCA-Klassen PrivateKey und PublicKey zum Signieren bzw. Überprüfen einer Nachricht.

4.1. Ein KeyPair bekommen

Um ein Schlüsselpaar aus einem privaten und einem öffentlichen Schlüssel zu erstellen , verwenden wir das Java- Keytool .

Lassen Sie uns mit dem Befehl genkeypair ein Schlüsselpaar generieren :

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

Dadurch wird für uns ein privater Schlüssel und der dazugehörige öffentliche Schlüssel erstellt. Der öffentliche Schlüssel wird in ein selbstsigniertes X.509-Zertifikat eingeschlossen, das wiederum in eine Einzelelement-Zertifikatkette eingeschlossen wird. Wir speichern die Zertifikatkette und den privaten Schlüssel in der Keystore-Datei sender_keystore.p12 , die wir mithilfe der KeyStore-API verarbeiten können.

Hier haben wir das PKCS12-Schlüsselspeicherformat verwendet, da es der Standard ist und gegenüber dem Java-proprietären JKS-Format empfohlen wird. Außerdem sollten wir uns das Passwort und den Alias ​​merken, da wir sie im nächsten Unterabschnitt beim Laden der Keystore-Datei verwenden werden.

4.2. Laden des privaten Schlüssels zum Signieren

Um eine Nachricht zu signieren, benötigen wir eine Instanz des PrivateKey.

Mit der KeyStore- API und der vorherigen Keystore-Datei sender_keystore.p12 können wir ein PrivateKey- Objekt abrufen :

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

4.3. Veröffentlichen des öffentlichen Schlüssels

Bevor wir den öffentlichen Schlüssel veröffentlichen können, müssen wir zunächst entscheiden, ob wir ein selbstsigniertes Zertifikat oder ein CA-signiertes Zertifikat verwenden möchten.

Wenn Sie ein selbstsigniertes Zertifikat verwenden, müssen Sie es nur aus der Keystore-Datei exportieren. Wir können dies mit dem Befehl exportcert tun :

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

Andernfalls müssen wir eine Zertifikatsignierungsanforderung (Certificate Signing Request, CSR) erstellen , wenn wir mit einem CA-signierten Zertifikat arbeiten möchten . Wir machen das mit dem Befehl 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.

Wie immer ist der Code aus diesem Artikel auf GitHub verfügbar.

Java unten

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> Überprüfen Sie den Kurs