1. Обзор
В этом уроке мы узнаем о механизме цифровой подписи и о том, как мы можем реализовать его с помощью Архитектуры криптографии Java (JCA) . Мы рассмотрим пару ключей , Дайджест сообщений, Шифр, хранилище ключей, сертификат, и Подпись API JCA.
Мы начнем с понимания того, что такое цифровая подпись, как создать пару ключей и как сертифицировать открытый ключ в центре сертификации (ЦС). После этого мы рассмотрим, как реализовать цифровую подпись с использованием низкоуровневых и высокоуровневых API JCA.
2. Что Такое Цифровая Подпись?
2.1. Определение Цифровой Подписи
Цифровая подпись-это метод обеспечения:
- Целостность: сообщение не было изменено при передаче
- Подлинность: автор сообщения действительно тот, за кого они себя выдают
- Неотрицание: автор сообщения не может позже отрицать, что они были источником
2.2. Отправка Сообщения с цифровой подписью
Технически говоря, a |/цифровая подпись-это зашифрованный хэш (дайджест, контрольная сумма) сообщения . Это означает, что мы генерируем хэш из сообщения и шифруем его закрытым ключом в соответствии с выбранным алгоритмом.
Затем отправляется сообщение, зашифрованный хэш, соответствующий открытый ключ и алгоритм. Это классифицируется как сообщение с его цифровой подписью.
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=Baeldung" -validity 365 -storetype PKCS12 \ -keystore sender_keystore.p12 -storepass changeit
Это создает для нас закрытый ключ и соответствующий ему открытый ключ. Открытый ключ завернут в самозаверяющий сертификат X. 509, который, в свою очередь, завернут в цепочку сертификатов из одного элемента. Мы храним цепочку сертификатов и закрытый ключ в файле хранилища ключей sender_keystore.p12 , который мы можем обработать с помощью API хранилища ключей|/.
Здесь мы использовали формат хранилища ключей PKCS12, поскольку он является стандартным и рекомендуемым по сравнению с форматом JKS, принадлежащим Java. Кроме того, мы должны помнить пароль и псевдоним, так как мы будем использовать их в следующем подразделе при загрузке файла хранилища ключей.
4.2. Загрузка закрытого ключа для подписи
Чтобы подписать сообщение, нам нужен экземпляр закрытого ключа .
Использование хранилища ключей 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. Публикация открытого ключа
Прежде чем мы сможем опубликовать открытый ключ, мы должны сначала решить, будем ли мы использовать самозаверяющий сертификат или сертификат, подписанный центром сертификации.
При использовании самозаверяющего сертификата нам нужно только экспортировать его из файла хранилища ключей. Мы можем сделать это с помощью команды exportcert :
keytool -exportcert -alias senderKeyPair -storetype PKCS12 \ -keystore sender_keystore.p12 -file \ sender_certificate.cer -rfc -storepass changeit
В противном случае, если мы собираемся работать с сертификатом, подписанным центром сертификации, нам необходимо создать запрос на подпись сертификата (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
И используя Хранилище ключей API, как и раньше, мы можем получить Открытый ключ экземпляр:
KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(new FileInputStream("receiver_keytore.p12"), "changeit"); Certificate certificate = keyStore.getCertificate("receiverKeyPair"); PublicKey publicKey = certificate.getPublicKey();
Теперь, когда у нас есть экземпляр PrivateKey на стороне отправителя и экземпляр Открытого ключа на стороне получателя, мы можем начать процесс подписания и проверки.
5. Цифровая подпись С Дайджестом сообщений и Классами Шифров
Как мы уже видели, цифровая подпись основана на хэшировании и шифровании.
Обычно мы используем класс MessageDigest с SHA или MD5 для хэширования и класс Cipher для шифрования.
Теперь давайте начнем внедрять механизмы цифровой подписи.
5.1. Генерация хэша сообщения
Сообщение может быть строкой, файлом или любыми другими данными. Итак, давайте возьмем содержимое простого файла:
byte[] messageBytes = Files.readAllBytes(Paths.get("message.txt"));
Теперь, используя Дайджест сообщений, давайте использовать метод digest для генерации хэша:
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);
Алгоритм подписи, который мы выбрали, SHA256withRSA в этом примере , представляет собой комбинацию алгоритма хеширования и алгоритма шифрования. Другие альтернативы включают, в частности, 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);
Затем нам нужно добавить полученные байты сообщений в объект подписи, вызвав метод update :
byte[] messageBytes = Files.readAllBytes(Paths.get("message.txt")); signature.update(messageBytes);
И, наконец, мы можем проверить подпись, вызвав метод verify :
boolean isCorrect = signature.verify(receivedSignature);
7. Заключение
В этой статье мы впервые рассмотрели, как работает цифровая подпись и как установить доверие к цифровому сертификату. Затем мы реализовали цифровую подпись, используя классы Message Digest, | Cipher, и Signature из архитектуры криптографии Java.
Мы подробно рассмотрели, как подписывать данные с помощью закрытого ключа и как проверить подпись с помощью открытого ключа.
Как всегда, код из этой статьи доступен на GitHub .