Цифрови подписи в Java

Java Top

Току що обявих новия курс Learn Spring , фокусиран върху основите на Spring 5 и Spring Boot 2:

>> ПРЕГЛЕД НА КУРСА

1. Общ преглед

В този урок ще научим за механизма на цифровия подпис и как можем да го приложим с помощта на Java Cryptography Architecture (JCA) . Ще изследваме API-тата KeyPair, MessageDigest, Cipher, KeyStore, Certificate и Signature JCA.

Ще започнем, като разберем какво е Digital Signature, как да генерираме двойка ключове и как да удостоверим публичния ключ от сертифициращ орган (CA). След това ще видим как да приложим Digital Signature, използвайки API на JCA на ниско и високо ниво.

2. Какво е цифров подпис?

2.1. Определение за цифров подпис

Цифровият подпис е техника за осигуряване на:

  • Почтеност: съобщението не е променено при транспортиране
  • Автентичност: авторът на съобщението наистина е този, за когото се твърди, че е
  • Неотрицание: авторът на съобщението не може по-късно да отрече, че те са източникът

2.2. Изпращане на съобщение с цифров подпис

Технически погледнато, един цифров подпис е криптирана хеш (храносмилане, контролна сума) на съобщението . Това означава, че генерираме хеш от съобщение и го криптираме с частен ключ според избрания алгоритъм.

След това се изпращат съобщението, криптираният хеш, съответният публичен ключ и алгоритъмът. Това се класифицира като съобщение с цифровия си подпис.

2.3. Получаване и проверка на цифров подпис

За да провери цифровия подпис, получателят на съобщение генерира нов хеш от полученото съобщение, дешифрира получения криптиран хеш с помощта на публичния ключ и ги сравнява. Ако те съвпадат, цифровият подпис се счита за проверен.

Трябва да отбележим, че криптираме само хеша на съобщението, а не самото съобщение. С други думи, Digital Signature не се опитва да запази съобщението в тайна. Нашият цифров подпис само доказва, че съобщението не е било променено по време на транспортиране.

Когато подписът е проверен, сме сигурни, че само собственикът на частния ключ може да бъде авторът на съобщението .

3. Цифров сертификат и идентификация на публичния ключ

Сертификатът е документ, който свързва идентичност с даден публичен ключ. Сертификатите се подписват от трета страна, наречена Certificate Authority (CA).

Знаем, че ако хешът, който декриптираме с публикувания публичен ключ, съвпада с действителния хеш, съобщението се подписва. Как обаче да разберем, че публичният ключ наистина идва от правилния обект? Това се решава чрез използването на цифрови сертификати.

Цифровият сертификат съдържа публичен ключ и самият той е подписан от друг обект. Подписът на този обект може сам да бъде проверен от друг обект и така нататък. В крайна сметка имаме това, което наричаме верига от сертификати. Всеки топ обект удостоверява публичния ключ на следващия обект. Обектът с най-високо ниво е самоподписан, което означава, че неговият публичен ключ е подписан от неговия собствен частен ключ.

X.509 е най-използваният формат на сертификата и се доставя като двоичен формат (DER) или текстов формат (PEM). JCA вече осигурява изпълнение за това чрез класа X509Certificate .

4. Управление на KeyPair

Тъй като Digital Signature използва частен и публичен ключ, ще използваме JCA класовете PrivateKey и PublicKey съответно за подписване и проверка на съобщение.

4.1. Получаване на KeyPair

За създаване на двойка ключове на частен и публичен ключ , ще използваме Java keytool .

Нека генерираме двойка ключове с помощта на командата genkeypair :

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

Това създава частен ключ и съответния публичен ключ за нас. Публичният ключ се увива в самоподписан сертификат X.509, който от своя страна се увива в едноелементна верига от сертификати. Съхраняваме веригата от сертификати и частния ключ във файла Keystore sender_keystore.p12 , който можем да обработим с помощта на API на KeyStore.

Тук използвахме формата за съхранение на ключове PKCS12, тъй като той е стандартен и се препоръчва над собствения на Java формат JKS. Също така трябва да помним паролата и псевдонима, тъй като ще ги използваме в следващия подраздел при зареждане на файла Keystore.

4.2. Зареждане на частния ключ за подписване

За да подпишем съобщение, се нуждаем от екземпляр на PrivateKey.

Използвайки API на KeyStore и предишния файл Keystore, sender_keystore.p12, можем да получим обект PrivateKey :

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

4.3. Публикуване на публичния ключ

Преди да можем да публикуваме публичния ключ, първо трябва да решим дали ще използваме самоподписан сертификат или CA-подписан сертификат.

Когато използваме самоподписан сертификат, трябва само да го експортираме от файла Keystore. Можем да направим това с командата exportcert :

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

В противен случай, ако ще работим със сертификат, подписан от CA, тогава трябва да създадем заявка за подписване на сертификат (CSR) . Правим това с командата 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.

Както винаги, кодът от тази статия е достъпен в GitHub.

Дъно на Java

Току що обявих новия курс Learn Spring , фокусиран върху основите на Spring 5 и Spring Boot 2:

>> ПРЕГЛЕД НА КУРСА