Перейти к основному содержимому

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

· 8 мин. чтения

1. Обзор

В этом руководстве мы узнаем о механизме цифровой подписи и о том, как мы можем реализовать его с помощью архитектуры криптографии Java (JCA) . Мы рассмотрим KeyPair, MessageDigest, Cipher, KeyStore, Certificate и Signature JCA API.

Мы начнем с понимания того, что такое цифровая подпись, как сгенерировать пару ключей и как сертифицировать открытый ключ в центре сертификации (ЦС). После этого мы увидим, как реализовать цифровую подпись с помощью низкоуровневых и высокоуровневых API-интерфейсов JCA.

2. Что такое цифровая подпись?

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

Цифровая подпись — это метод обеспечения:

  • Целостность: сообщение не было изменено в пути
  • Подлинность: автор сообщения действительно тот, за кого себя выдает.
  • Неотказуемость: автор сообщения не может впоследствии отрицать, что он был источником

2.2. Отправка сообщения с цифровой подписью

С технической точки зрения, цифровая подпись — это зашифрованный хэш (дайджест, контрольная сумма) сообщения . Это означает, что мы генерируем хеш из сообщения и шифруем его закрытым ключом в соответствии с выбранным алгоритмом.

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

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

Для проверки цифровой подписи получатель сообщения генерирует новый хэш из полученного сообщения, расшифровывает полученный зашифрованный хеш с помощью открытого ключа и сравнивает их. Если они совпадают, говорят, что цифровая подпись проверена.

Следует отметить, что мы шифруем только хэш сообщения, а не само сообщение. Другими словами, цифровая подпись не пытается сохранить сообщение в секрете. Наша цифровая подпись только доказывает, что сообщение не было изменено при передаче.

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

3. Цифровой сертификат и идентификация открытого ключа

Сертификат — это документ, который связывает личность с данным открытым ключом. Сертификаты подписываются сторонней организацией, называемой Центром сертификации (ЦС).

Мы знаем, что если хэш, который мы расшифровываем с помощью опубликованного открытого ключа, совпадает с фактическим хэшем, то сообщение считается подписанным. Однако откуда мы знаем, что открытый ключ действительно получен от правильной сущности? Это решается использованием цифровых сертификатов.

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

X.509 является наиболее часто используемым форматом сертификата и поставляется либо в двоичном формате (DER), либо в текстовом формате (PEM). JCA уже предоставляет реализацию для этого через класс X509Certificate .

4. Управление парой ключей

Поскольку цифровая подпись использует закрытый и открытый ключи, мы будем использовать классы JCA PrivateKey и PublicKey для подписи и проверки сообщения соответственно.

4.1. Получение пары ключей

Чтобы создать пару ключей из закрытого и открытого ключа , мы будем использовать Java keytool .

Сгенерируем пару ключей с помощью команды genkeypair :

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

Это создает для нас закрытый ключ и соответствующий ему открытый ключ. Открытый ключ заключен в самозаверяющий сертификат X.509, который, в свою очередь, включен в цепочку сертификатов с одним элементом. Мы храним цепочку сертификатов и закрытый ключ в файле хранилища ключей sender_keystore.p12 , который мы можем обработать с помощью KeyStore API .

Здесь мы использовали формат хранилища ключей PKCS12, поскольку он является стандартом и рекомендуется по сравнению с собственным форматом JKS, принадлежащим Java. Кроме того, мы должны помнить пароль и псевдоним, так как мы будем использовать их в следующем подразделе при загрузке файла хранилища ключей.

4.2. Загрузка закрытого ключа для подписи

Чтобы подписать сообщение, нам нужен экземпляр PrivateKey.

Используя KeyStore API и предыдущий файл хранилища ключей 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. Публикация открытого ключа

Прежде чем мы сможем опубликовать открытый ключ, мы должны сначала решить, будем ли мы использовать самозаверяющий сертификат или сертификат, подписанный ЦС.

При использовании самоподписанного сертификата нам нужно только экспортировать его из файла 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

Затем файл CSR sender_certificate.csr отправляется в центр сертификации для подписания. Когда это будет сделано, мы получим подписанный открытый ключ, завернутый в сертификат X.509, либо в двоичном (DER), либо в текстовом (PEM) формате. Здесь мы использовали параметр rfc для формата PEM.

Открытый ключ, который мы получили от ЦС, sender_certificate.cer, теперь подписан ЦС и может быть доступен для клиентов.

4.4. Загрузка открытого ключа для проверки

Имея доступ к открытому ключу, получатель может загрузить его в свое хранилище ключей с помощью команды importcert :

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

А используя KeyStore API, как и раньше, мы можем получить экземпляр PublicKey :

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

Теперь, когда у нас есть экземпляр PrivateKey на стороне отправителя и экземпляр PublicKey на стороне получателя, мы можем начать процесс подписания и проверки.

5. Цифровая подпись с классами MessageDigest и Cipher

Как мы видели, цифровая подпись основана на хешировании и шифровании.

Обычно мы используем класс MessageDigest с SHA или MD5 для хеширования и класс Cipher для шифрования.

Теперь приступим к реализации механизмов цифровой подписи.

5.1. Генерация хэша сообщения

Сообщение может быть строкой, файлом или любыми другими данными. Итак, давайте возьмем содержимое простого файла:

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

Теперь, используя MessageDigest, воспользуемся методом дайджеста для генерации хеша:

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

Здесь мы использовали алгоритм SHA-256, который используется чаще всего. Другими альтернативами являются MD5, SHA-384 и SHA-512.

5.2. Шифрование сгенерированного хэша

Чтобы зашифровать сообщение, нам нужен алгоритм и закрытый ключ. Здесь мы будем использовать алгоритм RSA. Алгоритм DSA является еще одним вариантом.

Давайте создадим экземпляр Cipher и инициализируем его для шифрования. Затем мы вызовем метод doFinal() для шифрования ранее хешированного сообщения:

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

Подпись можно сохранить в файл для последующей отправки:

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

На этом этапе отправляется сообщение, цифровая подпись, открытый ключ и алгоритм, и получатель может использовать эти фрагменты информации для проверки целостности сообщения.

5.3. Проверка подписи

Когда мы получаем сообщение, мы должны проверить его подпись. Для этого мы расшифровываем полученный зашифрованный хэш и сравниваем его с хэшем, который мы делаем из полученного сообщения.

Прочитаем полученную ЭЦП:

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

Для расшифровки мы создаем экземпляр Cipher . Затем мы вызываем метод doFinal :

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

Далее мы генерируем новый хэш сообщения из полученного сообщения:

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

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

И, наконец, мы проверяем, соответствует ли вновь сгенерированный хэш сообщения расшифрованному:

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

В этом примере мы использовали текстовый файл message.txt , чтобы имитировать сообщение, которое мы хотим отправить, или расположение тела полученного нами сообщения. Обычно мы ожидаем получить наше сообщение вместе с подписью.

6. Цифровая подпись с использованием класса подписи

До сих пор мы использовали низкоуровневые API для создания собственного процесса проверки цифровой подписи. Это помогает нам понять, как это работает, и позволяет настраивать его.

Однако JCA уже предлагает специальный API в виде класса Signature .

6.1. Подписание сообщения

Чтобы начать процесс подписания, мы сначала создаем экземпляр класса Signature . Для этого нам нужен алгоритм подписи. Затем мы инициализируем подпись нашим закрытым ключом:

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

Алгоритм подписи, который мы выбрали, SHA256 с RSA в этом примере , представляет собой комбинацию алгоритма хеширования и алгоритма шифрования. Другие альтернативы включают , среди прочих, SHA1withRSA , SHA1withDSA и MD5withRSA .

Далее переходим к подписи байтового массива сообщения:

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

signature.update(messageBytes);
byte[] digitalSignature = signature.sign();

Мы можем сохранить подпись в файл для последующей передачи:

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

6.2. Проверка подписи

Для проверки полученной подписи снова создаем экземпляр Signature :

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

Далее мы инициализируем объект Signature для проверки, вызвав метод initVerify , который принимает открытый ключ:

signature.initVerify(publicKey);

Затем нам нужно добавить полученные байты сообщения в объект подписи, вызвав метод обновления :

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

signature.update(messageBytes);

И, наконец, мы можем проверить подпись, вызвав метод verify :

boolean isCorrect = signature.verify(receivedSignature);

7. Заключение

В этой статье мы впервые рассмотрели, как работает цифровая подпись и как установить доверие к цифровому сертификату. Затем мы реализовали цифровую подпись, используя классы MessageDigest, Cipher и Signature из Java Cryptography Architecture.

Мы подробно рассмотрели, как подписывать данные с помощью закрытого ключа и как проверять подпись с помощью открытого ключа.

Как всегда, код из этой статьи доступен на GitHub .