- Суть проблемы и что с ней будет завтра
- Краткое введение в класс messagedigest:
- Основные классы и интерфейсы
- Activex
- Cipher (шифр)
- Java applet
- Keys (ключи)
- Provider (поставщик криптографии)
- Анализ исходных данных
- Безопасность ключа
- Генерация ключа
- Генерация пары ключей
- Дайджест сообщения (messagedigest)
- Инициализация шифра
- Код аутентификации сообщения (mac)
- Костыли
- Личный опыт
- Необходимо провести проверку по пяти пунктам:
- Плагины к браузерам
- Подготовка к подписи по правилам смэв 3
- Подпись (signature)
- Подпись данных
- Подпись сообщений смэв 3
- Полный пример подписи и проверки
- Проблемы. хэш не совпадает
- Проверка версии и работоспособности программного обеспечения java.
- Проверка подписи
- Проверка подписи сообщения смэв 3
- Проверка электронной подписи в java bouncy castle
- Расширение криптографии java
- Результаты
- Туннель
- Хранилище ключей (key store)
- Шифрование или дешифрование данных
Суть проблемы и что с ней будет завтра
Проблема довольно тривиальна: хотите реализовать ЭЦП на клиенте в браузере, думаете использовать для этого свой любимый javascript? Ничего не выйдет, и все потому, что браузеры просто не предоставляют API для работы с сертификатами, токенами, подписью и тд. и тп… Как должен выглядить этот механизм в мечтах любого веб-разработчика? Наверное как-то так:
//получить список сертификатов пользователя
window.crypto.getCertificates(options);
//подписать какую-то строку
window.crypto.signText(text, options);
ну и так далее...
Да, а еще было бы здорово, чтобы все это поддерживало отечественные ГОСТы… ну, это я уж совсем размечтался. К сожалению, ничего подобного в современных браузерах вы не найдете. Есть, конечно, робкие попытки реализовать что-то похожее у Mozilla, но предупреждение в заголовке статьи весьма печалит:
Кстати, если кто-то пытался использовать этот API, отпишитесь, весьма интересно.Также есть упоминание Crypto в репозитории W3C. Скорее всего работа там началась совсем не так давно, материала там сейчас немного, но последнее обновление было недавно, так что работа идет:
Будет ли все это доведено до ума, а самое главное, когда это случится, сегодня вам, наверное, никто не скажет. Так что же остается делать разработчикам, которым надо здесь и сейчас?
Краткое введение в класс messagedigest:
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
В этом примере создается экземпляр MessageDigest, который использует внутренний алгоритм криптографического хэширования SHA-256 для вычисления дайджестов сообщений.
Чтобы вычислить дайджест сообщения некоторых данных, вы вызываете метод update() или digest(). Метод update() может вызываться несколько раз, а дайджест сообщения обновляется внутри объекта. Когда вы передали все данные, которые вы хотите включить в дайджест сообщения, вы вызываете digest() и извлекаете итоговые данные дайджеста сообщения.
Пример вызова update() несколько раз с последующим вызовом digest():
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
byte[] data1 = "0123456789".getBytes("UTF-8");
byte[] data2 = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
messageDigest.update(data1);
messageDigest.update(data2);
byte[] digest = messageDigest.digest();
Вы также можете вызвать digest() один раз, передав все данные, чтобы вычислить дайджест сообщения. Пример:
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
byte[] data1 = "0123456789".getBytes("UTF-8");
byte[] digest = messageDigest.digest(data1);
Основные классы и интерфейсы
API криптографии Java состоит из следующих пакетов Java:
- java.security
- java.security.cert
- java.security.spec
- java.security.interfaces
- javax.crypto
- javax.crypto.spec
- javax.crypto.interfaces
Основные классы и интерфейсы этих пакетов:
- Provider
- SecureRandom
- Cipher
- MessageDigest
- Signature
- Mac
- AlgorithmParameters
- AlgorithmParameterGenerator
- KeyFactory
- SecretKeyFactory
- KeyPairGenerator
- KeyGenerator
- KeyAgreement
- KeyStore
- CertificateFactory
- CertPathBuilder
- CertPathValidator
- CertStore
Activex
Используется в 99% случаев, когда необходимо реализовать ЭЦП в браузере, не стесняются его использовать банки, торговые площадки и прочие серьезные организации. На мой взгляд, сейчас это худший из вариантов, почему?
Вы привыкли использовать для работы свой любимый FF или Chrome? — Забудьте! ActiveX работает только в IE.
Не любите копаться в настройках браузера? — А придется! Перед тем как все заработает, вам изрядно придется покопаться в настройках безопасности вашего IE.
А может быть вы такой продвинутый, что у вас стоит Win7, да еще и x64, а может вы еще и IE9-10 себе поставили? — Очень зря, скорее всего у вас ничего не получится.
Шутки шутками, а ведь Microsoft объявила, что поддержка CAPICOM прекращена и далее компонент разрабатываться не будет и последняя версия CAPICOM, которая официально поддерживается, была в Windows Vista.
Cipher (шифр)
Класс Cipher (javax.crypto.Cipher) представляет криптографический алгоритм. Шифр может использоваться как для шифрования, так и для расшифровки данных. Класс Cipher объясняется более подробно в следующих разделах, ниже будет его краткое описание.
Создание экземпляра класса шифр, который использует алгоритм шифрования AES для внутреннего использования:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
Метод Cipher.getInstance(…) принимает строку, определяющую, какой алгоритм шифрования использовать, а также некоторые другие параметры алгоритма.В приведенном выше примере:
- AES — алгоритм шифрования
- CBC — это режим, в котором может работать алгоритм AES.
- PKCS5Padding — это то, как алгоритм AES должен обрабатывать последние байты данных для шифрования. Что именно это означает, ищите в руководстве по криптографии в целом, а не в этой статье.
Java applet
Все вроде бы не так уж плохо, и код написан один раз, и работает во всех браузерах, и даже настраивать ничего не надо. Но если у вас не установлена JRE, ничего не будет. Чаще всего работает в связке с
, хотя, используя JNI, можно много чего прикрутить. Да, чтобы все работало, сам апплет должен быть подписан.
Keys (ключи)
Для шифрования или дешифрования данных вам нужен ключ. Существует два типа ключей в зависимости от того, какой тип алгоритма шифрования используется:
- Симметричные ключи
- Асимметричные ключи
Симметричные ключи используются для симметричных алгоритмов шифрования. Алгоритмы симметричного шифрования используют один и тот же ключ для шифрования и расшифровки.Асимметричные ключи используются для алгоритмов асимметричного шифрования. Алгоритмы асимметричного шифрования используют один ключ для шифрования, а другой для дешифрования. Алгоритмы шифрования с открытым и закрытым ключом — примеры асимметричных алгоритмов шифрования.
Каким-то образом сторона, которая должна расшифровать данные, должна знать ключ, необходимый для дешифрования данных. Если дешифрующая сторона не является стороной шифрующей данные, эти две стороны должны договориться о ключе или обменяться ключом. Это называется обменом ключами.
Provider (поставщик криптографии)
Класс Provider (java.security.Provider) является центральным классом в Java crypto API. Для того чтобы использовать Java crypto API, вам нужно установить поставщика криптографии. Java SDK поставляется с собственным поставщиком криптографии. Если вы явно не установите поставщик криптографии, то будет использоваться поставщик по умолчанию.
Один из самых популярных поставщиков криптографии для Java crypto API называется Bouncy Castle. Вот пример, где в качестве поставщика криптографии устанавливается BouncyCastleProvider:
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.Security;
public class ProviderExample {
public static void main(String[] args) {
Security.addProvider(new BouncyCastleProvider());
}
}
Анализ исходных данных
Загружаем с портала СМЭВ 3:
Если уже умеем формировать обычный XMLDSig или подписывать, например, конверты сообщений СМЭВ 2, то больше всего начинает интересовать, чем же отличается конверт с подписью СМЭВ 3 от СМЭВ 2.
Открываем пример конверта СМЭВ 3 SendRequestRequestNoAttach.xml
Безопасность ключа
Ключи должны быть трудно угадываемые, чтобы злоумышленник не мог легко подобрать ключ шифрования. В примере из предыдущего раздела о классе Шифр(Cipher) использовался очень простой, жестко закодированный ключ. На практике так делать не стоит. Если ключ сторон легко угадать, злоумышленнику будет легко расшифровать зашифрованные данные и, возможно, создать поддельные сообщения самостоятельно.
Генерация ключа
Чтобы сгенерировать случайные ключи шифрования вы можете использовать класс Java KeyGenerator. KeyGenerator будет более подробно описан в следующих главах, вот небольшой пример его использования здесь:
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
SecureRandom secureRandom = new SecureRandom();
int keyBitSize = 256;
keyGenerator.init(keyBitSize, secureRandom);
SecretKey secretKey = keyGenerator.generateKey();
Полученный экземпляр SecretKey можно передать в метод Cipher.init(), например так:
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
Генерация пары ключей
Алгоритмы асимметричного шифрования используют пару ключей, состоящую из открытого ключа и закрытого ключа, для шифрования и дешифрования данных. Для создания асимметричной пары ключей вы можете использовать KeyPairGenerator (java.security.KeyPairGenerator). KeyPairGenerator будет более подробно описан в следующих главах, ниже простой пример использования Java KeyPairGenerator:
SecureRandom secureRandom = new SecureRandom();
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
KeyPair keyPair = keyPairGenerator.generateKeyPair();
Дайджест сообщения (messagedigest)
Когда вы получаете зашифрованные данные от другой стороны, можете ли вы быть уверенными что никто не изменил зашифрованные данные по пути к вам?
Обычно решение состоит в том, чтобы вычислить дайджест сообщения из данных до его шифрования, а затем зашифровать как данные, так и дайджест сообщения, и отправить его по сети. Дайджест сообщения — это хеш-значение, рассчитанное на основе данных сообщения. Если в зашифрованных данных изменяется хоть один байт, изменится и дайджест сообщения, рассчитанный по данным.
При получении зашифрованных данных вы расшифровываете их, вычисляете из них дайджест сообщения и сравниваете вычисленный дайджест сообщения с дайджестом сообщения, отправленного вместе с зашифрованными данными. Если два дайджеста сообщения одинаковы, существует высокая вероятность (но не 100%) того, что данные не были изменены.
Java MessageDigest (java.security.MessageDigest) можно использовать для вычисления дайджестов сообщений. Для создания экземпляра MessageDigest вызывается метод MessageDigest.getInstance(). Существует несколько различных алгоритмов дайджеста сообщений.
Инициализация шифра
Перед использованием экземпляра шифра его необходимо инициализировать. Экземпляр шифра инициализируется вызывом метода init(). Метод init() принимает два параметра:
- Режим — Шифрование / Расшифровка
- Ключ
Первый параметр указывает, режим работы экземпляра шифр: шифровать или расшифровывать данные. Второй параметр указывает, какой ключ они используют для шифрования или расшифровки данных.
Пример:
byte[] keyBytes = new byte[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
String algorithm = "RawBytes";
SecretKeySpec key = new SecretKeySpec(keyBytes, algorithm);
cipher.init(Cipher.ENCRYPT_MODE, key);
Обратите внимание, что способ создания ключа в этом примере небезопасен и не должен использоваться на практике. В этой статье в следующих разделах будет рассказано, как создавать ключи более безопасно.
Чтобы инициализировать экземпляр шифра для расшифровки данных, вы должны использовать Cipher.DECRYPT_MODE, например:
cipher.init(Cipher.DECRYPT_MODE, key);
Код аутентификации сообщения (mac)
Класс Java Mac используется для создания MAC(Message Authentication Code) из сообщения. MAC похож на дайджест сообщения, но использует дополнительный ключ для шифрования дайджеста сообщения. Только имея как исходные данные, так и ключ, вы можете проверить MAC.
Экземпляр Java Mac создается вызовом метода Mac.getInstance(), передавая в качестве параметра имя используемого алгоритма. Вот как это выглядит:
Mac mac = Mac.getInstance("HmacSHA256");
Прежде чем создать MAC из данных, вы должны инициализировать экземпляр Mac ключом. Вот пример инициализации экземпляра Mac ключом:
byte[] keyBytes = new byte[]{0,1,2,3,4,5,6,7,8 ,9,10,11,12,13,14,15};
String algorithm = "RawBytes";
SecretKeySpec key = new SecretKeySpec(keyBytes, algorithm);
mac.init(key);
После инициализации экземпляра Mac вы можете вычислить MAC из данных, вызвав методы update() и doFinal(). Если у вас есть все данные для расчета MAC, вы можете сразу вызвать метод doFinal(). Вот как это выглядит:
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
byte[] data2 = "0123456789".getBytes("UTF-8");
mac.update(data);
mac.update(data2);
byte[] macBytes = mac.doFinal();
Костыли
Хочу сразу оговориться, ЭЦП в браузере — это, пожалуй, один из немногих случаев когда разработчик просто вынужден использовать различные ухищрения и костыли, я перечислю наиболее популярные из них:
Давайте рассмотрим недостатки каждого из них, достоинства описывать не буду, имхо, у всех костылей оно одно — “ну, оно же работает”.
Личный опыт
Да, мне тоже “повезло” заниматься реализацией ЭЦП в браузере. Сам работаю в одной из минских компаний, занимающейся разработкой ПО, и однажды потребовалось срочно реализовать поддержку ЭЦП в браузере в одной из систем. Скажу честно, сначала хотел поступить как и 99% в такой ситуации и задействовать ActiveX.
Но, постепенно погружаясь в проблему, стал осознавать, что это обернется сначала головной болью для пользователей, а затем и для меня как разработчика. Писать плагины под каждый браузер не было времени. Оставалось два решения: java апплет и туннель.
Дело в том, что в Беларуси как и в России принят свой ГОСТ на процедуры выработки и проверки электронной цифровой подписи, только у нас он называется СТБ РБ 1176.2–99 и никаких вам RSA. Даешь свой ГОСТ каждой стране! И если для российского ГОСТа есть практические реализации каждого из вышеперечисленных способов, можно выбирать любой, то белорусским разработчикам повезло куда меньше.
По этому поводу даже лично беседовал c представителями компании, выпускающей криптографическое ПО для нашей страны. Разговор оставил двоякое впечатление. Народ вполне адекватный, проблему понимают, но говорят, что это не их задача, они, мол, написали криптопровайдер, удовлетворяющий всем ГОСТам и прошедший государственную экспертизу, а как вы его собираетесь использовать, это не их проблема.
— Можем лишь предложить вам использовать ActiveX.— Ну, а как же, проблемы связанные с ним? — У нас, конечно, есть определенные наработки с использованием java апплетов, но в качестве продукта мы вам их пока предложить не можем. Разработка такого рода ПО — не наша задача.
На мой взгляд, это не совсем верная позиция, ведь предлагая продукт, коммерчески выгодно предложить и окружение к нему.
Ну да ладно, работаем с тем, что есть. А что у нас есть? Ранее в нашей компании уже было написано desktop-ое приложение, использующее функции ЭЦП, и там имелся компонент, работающий непосредственно с CryptoAPI, написан он был на delphi. Очень уж хотелось как-то использовать этот опыт коллег и самому не тратить на это время, а самое главное сделать это быстро. Но как это сделать? Соединить компонент на delphi и веб-приложение? Ответ на схеме:
Кратко поясню, что происходит. Когда нам нужно что-то подписать, мы с помощью javascript вызываем функцию java апплета:
document.CryptoAPI.sign(text);
Апплет в свою очередь через сокеты связывается с desktop-приложением, передает ему необходимые параметры, приложение подписывает то, что мы запросили (для этого как раз используется ранее написанный компонент коллег), и передает подписанные данные апплету, а тот в свою очередь браузеру, вызывая функцию:
function signResult(params) {
//что-то делаем с подписанными данными
}
Необходимо провести проверку по пяти пунктам:
- Отображается ли JAVA 32-разрядная актуальной версии в «Программах и компонентах» или “Установка и удаление программ” (в зависимости от версии Windows). Открываем «Пуск» –> «Панель управления» –> «Программы и компоненты» (В Windows 10 щелкаем правой кнопкой мыши на «Пуск» и выбираем «Программы и компоненты» или «Приложения и возможности»). Если нет – производим установку и проверку.
- Включена ли фильтрация ActiveX в настройках Internet Explorer. Если включена – отключаем.
- Включено ли отображение содержимого Java в браузере (enable java content in the browser), должно быть включено. Открываем «Пуск» –> «Панель управления» -> «JAVA».
- Антивирус необходимо отключить на время проверки.
Нижний правый угол экрана, «Отображать скрытые значки» (стрелка вверх),
Находим значок антивируса, нажимаем на него правой кнопко мыши и выбираем «Временно отключить» («Выход» если Касперский, Avast «Управление экранами» – «Приостановить на 10 мин.»)
- Проверяем корректные ли значения «Дата и время» и часовой пояс (соответствует ли указанное значение в скобках «UTC …» региону).
Плагины к браузерам
Если чего-то нет в браузере, но оно вам очень нужно, выход есть — напишите свой плагин. Основная проблема здесь в том, что если вы хотите, чтобы это работало во всех браузерах, вам придется написать плагин под каждый из них. Есть еще проблема с поддержкой — никто не гарантирует, что ваш плагин будет работать в новой версией браузера (или старой), вспомните ситуацию с FF, когда Mozilla перешла на ускоренный выпуск версий. Ну и еще проблема с тем, что вам этот плагин как минимум придется поставить. Подробнее об этом решении можно найти
Подготовка к подписи по правилам смэв 3
Apache Santuario изначально ничего не знает про ГОСТ криптографические алгоритмы и СКЗИ КриптоПро.
В библиотеке xmlsec-1.5.0.jar в файле orgapachexmlsecurityresourceconfig.xml содержатся настройки только для работы с зарубежными криптографическими алгоритмами.
Чтобы он начал распознавать и применять ГОСТ, нужно выполнить его инициализацию.
По старинке это делалось так:
//APACHE-SANTUARIO INIT WITH CryptoPro JCP
System.setProperty("org.apache.xml.security.resource.config", "resource/jcp.xml");
org.apache.xml.security.Init.init();
String cfile1 = System.getProperty("org.apache.xml.security.resource.config");
LOGGER.log(Level.INFO, "Init class URL: " org.apache.xml.security.Init.class.getProtectionDomain().getCodeSource().getLocation());
LOGGER.log(Level.INFO, cfile1);
В новых версиях КриптоПро JCP (JCSP) инициализацию выполнит одна строчка:
ru.CryptoPro.JCPxml.xmldsig.JCPXMLDSigInit.init();
Теперь нужно Apache Santuario научить новым правилам трансформации, которые диктует СМЭВ 3. Для этого регистрируем класс трансформации:
try {
Transform.register(SmevTransformSpi.ALGORITHM_URN, SmevTransformSpi.class.getName());
santuarioIgnoreLineBreaks(true);
LOGGER.log(Level.INFO, "SmevTransformSpi has been initialized");
} catch (AlgorithmAlreadyRegisteredException e) {
LOGGER.log(Level.INFO, "SmevTransformSpi Algorithm already registered: " e.getMessage());
}
Заодно сразу выполняем требование из Методических указаний:
Требования к форматированию В XML-структуре подписи между элементами не допускается наличие текстовых узлов, в том числе переводов строки.
santuarioIgnoreLineBreaks(true);
private static final String IGNORE_LINE_BREAKS_FIELD = "ignoreLineBreaks";
/**
* Apache Santuario privileged switch IgnoreLineBreaks property
*
* @param mode
*/
private void santuarioIgnoreLineBreaks(Boolean mode) {
try {
Boolean currMode = mode;
AccessController.doPrivileged(new PrivilegedExceptionAction<Boolean>() {
public Boolean run() throws Exception {
Field f = XMLUtils.class.getDeclaredField(IGNORE_LINE_BREAKS_FIELD);
f.setAccessible(true);
f.set(null, currMode);
return false;
}
});
} catch (Exception e) {
LOGGER.warning("santuarioIgnoreLineBreaks " ExceptionUtils.getFullStackTrace(e));
}
}
Делается это в привилегированном блоке AccessController.doPrivileged
и через reflection, из-за особенности реализации свойства ignoreLineBreaks в Santuario.
Просто через настройку системного свойства:
System.setProperty("org.apache.xml.security.ignoreLineBreaks", "true");
не работает.
Через настройку опции JVM:
-Dcom.sun.org.apache.xml.internal.security.ignoreLineBreaks=true
работает.
Если взглянуть на код класса org.apache.xml.security.utils.XMLUtils, то можно увидеть, что поле ignoreLineBreaks статическое, инициализируется в привилегированном блоке из системного свойства «org.apache.xml.security.ignoreLineBreaks».
private static boolean ignoreLineBreaks =
AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
public Boolean run() {
return Boolean.valueOf(Boolean.getBoolean
("org.apache.xml.security.ignoreLineBreaks"));
}
}).booleanValue();
public static boolean ignoreLineBreaks() {
return ignoreLineBreaks;
}
Такая реализация приводит к невозможности гибко настроить в одном Java процессе для части методов игнорировать перевод строк, а для другой части не игнорировать.
Т.е., если одно приложение выполняет подписи XMLDsig, СМЭВ 2 и СМЭВ 3, все XML документы, обработанные Santuario должны на выходе лишиться перевода строк.
С этим свойством, конечно, возникает вопрос к Apache Santuario:
Подпись (signature)
Класс Signature (java.security.Signature) используется для цифровой подписи данных. Когда данные подписаны, цифровая подпись создается из этих данных. Таким образом, подпись отделена от данных.
Цифровая подпись создается путем создания дайджеста сообщения (хеша) из данных и шифрования этого дайджеста сообщения с помощью закрытого ключа устройства, лица или организации, которая должна подписать данные. Дайджест зашифрованного сообщения называется цифровой подписью.
Для создания экземпляра Signature, вызывается метод Signature.getInstance (…):
Signature signature = Signature.getInstance("SHA256WithDSA");
Подпись данных
Чтобы подписать данные, вы должны инициализировать экземпляр подписи в режиме подписи вызывая метод initSign(…), передавая закрытый ключ для подписи данных. Пример инициализации экземпляра подписи в режиме подписи:
signature.initSign(keyPair.getPrivate(), secureRandom);
После инициализации экземпляра подписи, его можно использовать для подписи данных. Это делается вызовом метода update(), передавая данные для подписи в качестве параметра. Можно вызывать метод update() несколько раз, чтобы дополнить данные для создании подписи. После передачи всех данных в метод update(), вызывается метод sign() для получения цифровой подписи. Вот как это выглядит:
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
signature.update(data);
byte[] digitalSignature = signature.sign();
Подпись сообщений смэв 3
Для подписи документов СМЭВ 3 все готово.
Код подписания выглядит следующим образом:
Полный пример подписи и проверки
SecureRandom secureRandom = new SecureRandom();
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
KeyPair keyPair = keyPairGenerator.generateKeyPair();
Signature signature = Signature.getInstance("SHA256WithDSA");
signature.initSign(keyPair.getPrivate(), secureRandom);
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
signature.update(data);
byte[] digitalSignature = signature.sign();
Signature signature2 = Signature.getInstance("SHA256WithDSA");
signature2.initVerify(keyPair.getPublic());
signature2.update(data);
boolean verified = signature2.verify(digitalSignature);
System.out.println("verified = " verified);
Проблемы. хэш не совпадает
Внимание!
Для отладки использовался пример конверта СМЭВ 3 SendRequestRequestNoAttach.xmlИз него был удален элемент ds:Signature с целью подписать сообщение заново и сверить с оригиналом.
Несмотря на то, что метод подписи и трансформация SmevTransformSpi, взятая из Методических указаний, отрабатывали, на выходе был подписанный документ, подпись которого при онлайн-проверке на портале СМЭВ 3 трактовалась как
ЭП-ОВ не подтверждена: Ошибка проверки ЭП: Нарушена целостность ЭП
Почему
не совпадал с оригинальным примером:
Для диагностики причин в класс SmevTransformSpi в метод process был добавлен свой XMLEventWriter.
ByteArrayOutputStream baos = new ByteArrayOutputStream();
XMLEventWriter bdst =
outputFactory.get().createXMLEventWriter(baos, ENCODING_UTF_8);
для параллельного анализа всех этапов трансформации.
Нормализованный элемент XML, на который требуется поставить подпись, выглядел следующим образом:
Поиск решения показал, что, во-первых
, нормализованный документ может выглядеть на самом деле иначе и соответственно его хэш будет другой и возможно правильный.
Во-вторых, привел в GitHub, где был выложен класс SmevTransformSpi более старой версии.
Старая версия класса трансформации выдала следующий нормализованный документ:
С ним хэш стал совпадать, а подпись успешно проходить валидацию.
Сравнение версий класса SmevTransformSpi показала, что помимо добавленных в новой реализации дополнительных функций логирования и диагностики в debug режиме:
if (logger.isDebugEnabled()) {
debugStream = new DebugOutputStream(argDst);
dst = outputFactory.get().createXMLEventWriter(debugStream, ENCODING_UTF_8);
} else {
dst = outputFactory.get().createXMLEventWriter(argDst, ENCODING_UTF_8);
}
Класс из Методических указаний не содержит нужную строчку, или содержит опечатку:
Отсутствует строка:
prefixMappingStack.pop();
, которая удаляет первый объект из стека с префиксами
Stack<List<Namespace>> prefixMappingStack = new Stack<List<Namespace>>();
, что приводило к неверной работе SmevTransformSpi.
Добавление этой строки в новую версию SmevTransformSpi.java решило проблему.
Проверка версии и работоспособности программного обеспечения java.
Данную проверку можно выполнить в браузере с поддержкой плагина Java. На данный момент таким браузером остается только Internet Explorer версии и 8 и выше.
Проверка подписи
Чтобы проверить подпись, нужно инициализировать экземпляр подписи в режиме проверки путем вызова метода initVerify(…), передавая в качестве параметра открытый ключ, который используется для проверки подписи. Пример инициализации экземпляра подписи в режиме проверки выглядит:
Signature signature = Signature.getInstance("SHA256WithDSA");
signature.initVerify(keyPair.getPublic());
После инициализации в режиме проверки в метод update() передаются данные, которые подписаны подписью. Вызов метода verify(), возвращает значение true или false в зависимости от того, можно ли проверить подпись или нет. Вот как выглядит проверка подписи:
byte[] data2 = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
signature2.update(data2);
boolean verified = signature2.verify(digitalSignature);
Проверка подписи сообщения смэв 3
Код проверки подписи выглядит следующим образом:
Проверка электронной подписи в java bouncy castle
p7s-файл это PKCS7 хранилище данных. В него есть определенная структура:
- Указан хеш-алгоритм, который применялся над изначальными данными, которые подписываются
- Сама подпись в виде бинарных данных
- X509 сертификаты. В них находятся публичные ключи, которые используются для проверки подписи
Слышал, что нужно брать публичный ключ(откуда?), который расшифровывает файл подписи(как?)
Про публичный ключ я написал выше. Неправильно говорить, расшифровывает файл подписи. В PKCS7 файле ничего не зашифровано. А проверка подписи, это просто функция ValidateSignature(message, signature, publicKey)
, которая возвращает true/false
.
Судя по всему, сначала мы берем файл, применяем алгоритм хэширования(в данном случае ГОСТ Р 34.11-94) через Bouncy Castle получаем хэш, который впоследствии должен быть сравнен
PKCS7 может содержать хеш подписанного сообщения, а может и не содержать. В зависимости от этого, вы сравниваете их.
Пример кода проверки подписи:
byte[] data = Files.readAllBytes(Paths.get("file.p7s"));
data = Base64.getDecoder().decode(data);
// парсим массив байт
CMSSignedData cmsSignedData = new CMSSignedData(data);
// получаем список сертификатов, которые содержатся в файле
Store<X509CertificateHolder> certs = cmsSignedData.getCertificates();
// проходимся по списку подписей, обычно одна подпись, но стандарт поддерживает множество подписей
for (SignerInformation si: cmsSignedData.getSignerInfos())
{
// получаем идентификатор сертификата, которым подписаны данные
SignerId signerId = si.getSID();
// ищем сертификат в коллекции по идентификатору
Collection certCollection = certs.getMatches(signerId);
// выбираем первый сертификат, здесь не должно быть больше одного
Iterator certIt = certCollection.iterator();
X509CertificateHolder certHolder = (X509CertificateHolder) certIt.next();
// создаем Verifier на основании сертификата, Verifier использует публичный ключ сертификата
SignerInformationVerifier verifier = new JcaSimpleSignerInfoVerifierBuilder().build(certHolder);
// проверяем подпись
boolean signatureOK = si.verify(verifier);
System.out.println(signatureOK);
}
Пример файла с подписью и RSA-сертификатом:
MIII3wYJKoZIhvcNAQcCoIII0DCCCMwCAQExDTALBglghkgBZQMEAgEwEwYJKoZIhvcNAQcBoAYEBHRleHSgggUcMIIFGDCCAwACCQCnluSKwW2kxjANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJVQTETMBEGA1UECBMKU29tZS1TdGF0ZTENMAsGA1UEBxMES3lpdjEMMAoGA1UEChMDRGV2MQ0wCwYDVQQDEwRJZ29yMB4XDTE3MDUyMjE0MTU1NloXDTIwMDIxNjE0MTU1NlowTjELMAkGA1UEBhMCVUExEzARBgNVBAgTClNvbWUtU3RhdGUxDTALBgNVBAcTBEt5aXYxDDAKBgNVBAoTA0RldjENMAsGA1UEAxMESWdvcjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOWJPDEgufpH6vsSxBcOYFPZYVJdGDd5S5pb4flSodJhXzhOLeyL3gkaU8r1VgIpAU3IUrdq8n4HyIdq8w9ImshFzoZH6yz7xjwvuciuyLiyVq7ViO VtNSEzKuIZ77WIBEIiBbvsFpqRejIxu6DVQTileKKoaGYEPig 74mXpi2ImJ0h0qv0Idr11qpCVP4B5Ix4ROvriyaWimuYIvqGWG8z9pKqrKSre2QqMa81HMGNu3fhElaqchaT ylqljJk IV1a2iLkSRTeQpaqEZNGjZ7firtgqa2zAlo4qDaekNylHB7Gj84Tlr9gauze/NTArAXJUNGEs8pufZcykwDR0NbMFriurLUhDKRc 2jeA6Ar EB0/8qEZXiua 2iHZPqeNusC0IdXOwIJGRXUOC/0AOJStjQ5tQxz4t2H3u8sSc5OM j44m7f7fT9Ep/VIqhR3gwhRj3k07LLn/FEm5ScccPR8QJ A3NsHtoKjPN4iWwCAz9s3LZudB5nns0W3A9nfGd08FXhG9zG1rqqhMW4KYELyt8yWWSuxx9CdjDgbibHDEbyK4PEHH6Vetgq16f5PnOrhkM1bRJTU6aMYbJcbpRpMlLk/1qbZwWXdoDEBf0WPD7FlrKkGEmEKm0uoYvgSKnT33Z8 q/IWPxceySs1kMD Xy5pIUX1f9LKnR1tAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAKbtSbkY3yRnP4Oj9AlnIp2UsQjOv/iyEgJpZaP6Q0OxShBJ4MhyCzERBhH8YDcbwS4l MkeMlJZNPw5n1heJFswuGS3oDYoI7D/DZUthTjTo7JSajuXHliIXSOFYWR4chPYpmBvx3rlAlpWxoHGY0ugFQ19BNUGECxSZOAxlCCNEo035P1iJTnrIVx c6ZTIPA62B nPgBVxmShRrzxk5c2kgm062ABsVpIc5cmhZOR1JWLgkRUceLXRBwdoTTuhyNito1Kxc8X1D0RXa1luFALTEu8y5JqO486UScvTpCjVVGqshB5mgPtZtZZGcKy2xcib6hEY7HzQ2JP1L7c5KSxeDZEp5AG5VLWsEi3Sanhg5kZ4sK6hkvJ9N/g2jAAhG3a0i3zUyGA/oltLiFZJ4A6pQDwqlYoLwIVbQkZxIr6fsVyuDpn A1f4H4YHde83UtLI2JKfL0ST5HOhW2UdfvAvFouF2/WqoM2j5ZnVfQMCk6I7rYemgjK2eUYJPgRb5WBFdG2VlS7mtpsZgv55KVV4M0CWIYMsmEPttDkJdy dyBfS7vDm/bvO5dF4QovVG0eddyHgfCK1GTnO47zddgDX2s2tNnAw tjNKnetLJLwdpoCqkr1IFUyxJQy5lTS7TdaYP4xJbsQZfyJJu/KSWSZbMFT/PbMj o918CO9qGMYIDgTCCA30CAQEwWzBOMQswCQYDVQQGEwJVQTETMBEGA1UECBMKU29tZS1TdGF0ZTENMAsGA1UEBxMES3lpdjEMMAoGA1UEChMDRGV2MQ0wCwYDVQQDEwRJZ29yAgkAp5bkisFtpMYwCwYJYIZIAWUDBAIBoIH6MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTE3MTExOTIwMjcxMFowLwYJKoZIhvcNAQkEMSIEIJgtnj65lvVZ5jP00ZTe83YdkJ9aO2R9GoUf6tZ8MsnRMIGOBgkqhkiG9w0BCQ8xgYAwfjALBglghkgBZQMEASowCAYGKoUDAgIJMAgGBiqFAwICFTALBglghkgBZQMEARYwCwYJYIZIAWUDBAECMAoGCCqGSIb3DQMHMA4GCCqGSIb3DQMCAgIAgDANBggqhkiG9w0DAgIBQDAHBgUrDgMCBzANBggqhkiG9w0DAgIBKDANBgkqhkiG9w0BAQEFAASCAgDhLa3K4PsuAolbesMOoB3kPDh Z3PcMHCaQJAF/zZCJf s4NivBz1cHvcVgbETk0pEUD3FEWQk1QATwjf7k1w8 EOYq2oXYKWnBrkZPQIB8Z Hkr2CKAgsyiGIxRz3nXjGZol yDdr/mAaV2qXZn17T0PcnI6KAu2CMVE4fgmy5xMg8IhYJFiwyEDRutJiUuMRCaA8lTcOoLCyrT vBG f7jXI0tqSqp5ojM RSydtxGIZIiBSaYoD5rN1PmK/qtNgUIGhuQFu7l/KbPpkfE65uAwg8Xd4C8bswO9BsRqf1wYdQN4BludgoRUNq8QHHMnl9m3fJrg3z57Dm/J93crX2fDJsJIxyffPFX86yNDlEHOSdyPLaFKA rqS8glTiu7W5KVICCLBI/Fv2FhhDP 9gwGyGtT0mv CvNSd7L0xfQKmt10FkDz2zBNyynZvmW9D0q8VQ1HvQkUuyBggilcwCpyBy5v70PSxtTUq/s6ybJi4L4XoRmypG74rLQLzK6m6IQpr5s1IjCI7YIAz6RV3u4cKEiJDu2sXMlaZIGfMdCnzwppJ1UsPbpB2kmY 5cYapc1lI5MdRM5020qaoXC9R3pOCvJnY3Fd7qvW1KVI14cnx37ZocIEANDPU51VittEGWcf3aCcNeEhAKzSYPz fqw4ES4ClwvuVrcHQOio w==
Расширение криптографии java
Java cryptography API предоставляется так называемым расширением Java Сryptography Extension(JCE). JCE уже давно является частью платформы Java. Изначально JCE был отделен от Java из-за того, что в США действовали экспортные ограничения на технологии шифрования.
Поэтому самые стойкие алгоритмы шифрования не были включены в стандартную платформу Java. Эти более надежные алгоритмы шифрования можно применять, если ваша компания находится в США, но в остальных случаях придется применять более слабые алгоритмы или реализовывать свои собственные алгоритмы шифрования и подключать их к JCE.
С 2021 года правила экспорта алгоритмов шифрования в США были значительно ослаблены и в большей части мира можно пользоваться международными стандартами шифрования через Java JCE.
Архитектура криптографии Java
Java Cryptography Architecture (JCA) — название внутреннего дизайна API криптографии в Java. JCA структурирован вокруг нескольких основных классов и интерфейсов общего назначения. Реальная функциональность этих интерфейсов обеспечивается поставщиками.
Также можно реализовать и подключить свои собственные провайдеры, но вы должны быть осторожны с этим. Правильно реализовать шифрование без дыр в безопасности сложно! Если вы не знаете, что делаете, вам, вероятно, лучше использовать встроенный поставщик Java или использовать надежного поставщика, такого как Bouncy Castle.
Результаты
Подписание конвертов СМЭВ 3 выполняется успешно.
Сообщения проходят проверку на портале Электронного правительства Госуслуги
И в собственном приложении:
Туннель
Относительно новый способ, работает по принципу прокси, основная идея в том, что особым образом сформированный POST-запрос при отправке на сервер проходит через туннель, тот подписывает данные из этого запроса, подставляет подписанные данные в поле запроса вместо данных и все отправляется дальше на сервер и туда уже приходят данные с ЭЦП. Принцип довольно простой, но эффективный. У этого способа имеется
. Подробнее можно почитать
. Чтобы все это работало, на клиенте собственно должен быть установлен этот туннель, хотя можно запустить и с флешки.
Как видите, решения есть, хотя каждое из них имеет определенные ограничения и оговорки, я намеренно не касался таких проблем как работа в различных операционных системах, поддержка ГОСТов, работа на мобильных платформах — здесь проблем еще больше, а ведь это тоже очень важно.
Хранилище ключей (key store)
Java KeyStore — это база данных, которая может содержать ключи. Java KeyStore представлен классом KeyStore (java.security.KeyStore). Хранилище ключей может содержать ключи следующих типов:
- Закрытые ключи (Private keys)
- Открытые ключи и сертификаты(Public keys certificates)
- Секретные ключи (Secret keys)
Закрытый и открытый ключи используются в асимметричном шифровании. Открытый ключ может иметь связанный сертификат. Сертификат — это документ, удостоверяющий личность человека, организации или устройства, претендующего на владение открытым ключом.
Сертификат обычно имеет цифровую подпись проверяющей стороны в качестве доказательства.Секретные ключи используются в симметричном шифровании.Класс KeyStore довольно сложный, поэтому он описан более подробно далее в отдельной главе по Java KeyStore.
Java Keytool — это инструмент командной строки, который может работать с файлами Java KeyStore. Keytool может генерировать пары ключей в файл KeyStore, экспортировать сертификаты и импортировать сертификаты в KeyStore и некоторые другие функции. Keytool поставляется с установкой Java. Keytool более подробно описан далее в отдельной главе по Java Keytool.
Шифрование или дешифрование данных
После инициализации шифра вы можете начать шифрование или расшифровку данных вызовом методов update() или doFinal(). Метод update() используется, если вы шифруете или расшифровываете фрагмент данных. Метод doFinal() вызывается, когда вы шифруете последний фрагмент данных или если блок данных, который вы передаете в doFinal(), является единичным набором данных для шифрования.
Пример шифрования данных с помощью метода doFinal():
byte[] plainText = "abcdefghijklmnopqrstuvwxyz".getBytes("UTF-8");
byte[] cipherText = cipher.doFinal(plainText);
Чтобы расшифровать данные, нужно передать зашифрованный текст(данные) в метод doFinal() или doUpdate().