Побег из Крипто Про. Режиссерская версия, СМЭВ-edition
Эта статья посвящена тому, как перестать использовать Крипто Про и перейти на Bouncy Castle в девелоперском/тестовом окружении.
В начале статьи будет больше про СМЭВ и его клиент, в конце — больше про конвертирование ключей с готовой копипастой, чтобы можно было начать прямо сейчас.
Картинка для привлечения внимания:
И сразу же ответ:
Коненчо, сбежать с Крипто Про невозможно, потому что это оплот российской криптографии. Он удовлетовряет требованиям компетентных органов, он прописан в договоры и контраты, ему доверяет вся страна, и так далее. Поэтому все нижеописанное относится девелоперскому или тестовому окружению, где мы сами себе хозяева.
Недавно мне нужно было разобраться, как написать сервис, работающий с Системой Межведомственного Электронного Взаимодействия.
Все написанное представляет собой просто результат небольшого исследования, максимально абстрагированный от выполненной работы. И даже приблизительно угадать что-то о реально принятых решениях невозможно, я проверял.
На технологическом портале СМЭВ3 лежат исходники клиента, но вот незадача — они гвоздями прибиты к КриптоПро. Вордовский файл с инструкцией, приложенный к исходникам, утверждает это самым прямым образом. Да и все равно, исходные ключи у нас тоже в формате КриптоПро. Исходя из production это нормально, а вот для тестового окружения жутко неудобно. Хотелось бы от этого избавиться.
На сайте есть две версии — «актуальная» и «рекомендуемая». Почему они так, и почему актуальная версия не рекомендуется, а рекомендуемая не актуальна — какая-то дилемма копирайтера :) Дальше речь о том клиенте который «актуальный».
Строго говоря, использовать его нельзя, потому что в архиве исходников нету текста лицензии, и поэтому непонятно, под какой лицензией должна распространяться производная работа. Я позвонил по телефону поддержки, написанному на портале, написал на почту, пару недель наблюдал как моя заявка летает между уровнями техподдержки и исполняющими организациями, и в результате воз и ныне там:
Задача не выполнена, но выполнена и закрыта, изумительно. Ладно, черт с ними…
Несмотря на невозможность использовать его у себя непосредственно в коде, это отличный тестовый пример. Дело в том, что в методических рекомендациях СМЭВа без поллитры не разобраться, и готовый живой код дает отличный буст к пониманию.
Основная претензия к документации — это канцеляризмы и скудное описание в интернете.
Помните мем про копирайтера, который из абзаца сделал одно предложение в несколько слов? Для документации СМЭВа это имеет место быть, например вот цепочка рефакторинов для одной произовльно взятой фразы:
»2. ИС потребителя направляет в СМЭВ межведомственный запрос;»
»2. ИС потребителя направляет в СМЭВ запрос;»
»2. ИС потребителя направляет запрос;»
»2. потребитель направляет запрос;»
»2. запрос потребителя;»
Короче, наличие готовой реализации — это добро.
- Он есть и работает
- Судя по исходникам BoncyCastle, токарищи из КриптоПро поучаствовали в его развитии, добавив алгоритмы по ГОСТ
- Если у вас много разработчиков и виртуальных машин, покупать лицензию не очень хочется
- Проприетарщина, поэтому все описанные ниже баги исправить нельзя. Точнее можно (дизассемблер стреляет без промаха), но незаконно и с потерей всех гарантий — не вариант
- На Java 8 под OSX завести не удалось (никакую версию КП JCP). Скорей всего это исправят довольно скоро, т.к. представители отреагировали на мой пост в Фейсбуке
- Вообще, на OSX завести не удалось. Гуй админки — полурабочий, сыпет ошибками, куски гуя не работают.
- На линуксе тоже есть баги в интерфейсе
- Когда-то давно установщик на Windows писал в консоли крокозябры (не проверял на новых версиях — может, пофиксили)
- Установка патчингом дистрибутива джавы. Ящетаю, что установка софта методом патчинга джавы — это зло в последней инстанции, за это суд по правам человека должен назначать шестикратный расстрел с повешанием
- Не каждую джаву можно пропатчить, для выяснения магической комбинации нужно серьезно упороться. Тут важно, что мы стараемся разрабатывать на самых новых версиях джавы, с пылу-с жару, и тестируем на новых версия (на момент написания статьи — JDK9), так что ограничения на версию джавы — это безумие как оно есть
- Способы инсталляции и запуска админки — лютый треш (это надо видеть)
В качестве альтернативы в тестовом окружении я предалагю использовать Bouncy Castle с контейнером PKCS12 или JKS. Это открытое, свободное и бесплатное ПО.
К чести разработчиков Крипто Про, похоже, они принимали участие в его разработке.
Код написан довольно дружелюбно для расширения, поэтому можно просто взять за основу класс KeyStoreWrapperJCP, и аналогично написать KeyStoreWrapperBouncyCastlePKCS12, KeyStoreWrapperBouncyCastleJKS.
Переписать DigitalSignatureFactory, так, чтобы он на вход начал принимать путь до криптоконтейнера на файловой системе и пароль от него (для КриптоПро это просто не нужно). Там есть свич, который проверяет тип криптопровайдера, в него надо дописать дополнительно два кейса, для имен типа BOUNCY_JKS и BOUNCY_PKCS12 и навешать использование соответсвующих KeyWrapper и вызов initXmlSec.
В initXmlSec нужно дописать
1) возможность принимать вообще любой провайдер, а не только криптопро (это просто удобно)
2) Security.addProvider (new BouncyCastleProvider ());
3) для XMLDSIG_SIGN_METHOD сделать свич: если КриптоПро, то алгоритм называется «GOST3411withGOST3410EL», а если BouncyCastle алгоритм называется «GOST3411WITHECGOST3410».
Ну вроде как и все. Если бы была известна лицензия на этот смэв-клиент, я бы приложил конкретный код под Apache License 2, а так это просто список идей.
Ах да, тут есть один интересный момент. В сети множество советов, касающихся СМЭВа, заключающихся в ручном парсинге кусков XMLек, и прочим закатом солнца вручную, но это не наш метод (и не метод, который использовали создатели клиента)
Замес в том, что сгенерить самоподписанные ключи по госту легко (копипаста на SO ищется за секунды). А вот подписать — нет, ибо по мнению интернет-школьников якобы Bouncycastle не поддерживает xml-подпись. Конкретней, при работе с Apache Santuario, XMLSignature из santuario-xmlsec не понимает что использовать для обработки метода «xmldsig-more#gostr34102001-gostr3411» при вызове xmlSignature.sign (privateKey).
Отдельная хохма в том, что IntelliJ IDEA Community глючит при попытке отдебажить xmlsec, бросая step in отладчика в неверное место верных исходников. Я попробовал все разумные версии Идеи, поэтому понимать как это работает надо вслепую, написуя тактические письма в Спортлото. Это не в укор Идее, не существует идеальных инструментов, просто фактор повлиявший на скорость понимания вопроса.
Чтобы это заработало, нужно:
1) Заимплементить реализацию SignatureAlgorithmSpi из Apache Santuario. В GetEngineUri вернуть строку: «http://www.w3.org/2001/04/xmldsig-more#gostr34102001-gostr3411» (или что там у вас). Это ключевая магия. Совершенно ничего умного этот класс делать не должен.
Инициализация раз за всю жизнь приложения (н-р в синглтон-бине спринга):
2) Загрузить провайдер, чтобы не патчить JDK:
Security.addProvider(new BouncyCastleProvider());
3) Впердолить в рантайм только что написанный класс:
String algorithmClassName = "fully qualified name класса реализующего SignatureAlgorithmSpi";
Class.forName(algorithmClassName);
SignatureAlgorithm.providerInit();
SignatureAlgorithm.register("http://www.w3.org/2001/04/xmldsig-more#gostr34102001-gostr3411", algorithmClassName);
4) Достучаться до маппингов алгоритмов JCE:
String ns = "http://www.xmlsecurity.org/NS/#configuration";
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
Element rootElement = document.createElementNS(ns, "JCEAlgorithmMappings");
Element algorithms = document.createElementNS(ns, "Algorithms");
5) Замапить метод на алгоритм:
Element aElem = document.createElementNS(NameSpace, "Algorithm");
aElem.setAttribute("URI", "http://www.w3.org/2001/04/xmldsig-more#gostr34102001-gostr3411");
aElem.setAttribute("Description", "GOST R 34102001 Digital Signature Algorithm with GOST R 3411 Digest");
aElem.setAttribute("AlgorithmClass", "Signature");
aElem.setAttribute("RequirementLevel", "OPTIONAL");
aElem.setAttribute("JCEName", "GOST3411WITHECGOST3410");
algorithms.appendChild(aElem);
6) Применить маппинги:
org.apache.xml.security.Init.init();
JCEMapper.init(rootElement);
6) PROFIT!
После этого XMLSignature резко начинает понимать этот метод, и начнет делать xmlSignature.sign.
Если у вас в начале статьи на стене висит OpenSSL, когда-нибудь он точно выстрелит.
Так что да, это важный момент, необходимый для осуществления дальнейшего текста.
- Так как у нас Крипто Про, и на Маке оно не взлетает, нам понадобится виртуальная машина с Windows (можно даже Windows XP), и установленной Криптой
- Установить как можно более актуальную версию OpenSSL (так, чтобы в ней уже была поддержка ГОСТ):
https://wiki.openssl.org/index.php/Binaries
https://slproweb.com/products/Win32OpenSSL.html - Проверить, что в установленной версии есть файл gost.dll
- В установленном OpenSSL найти файл openssl.cfg
- В самое начало файла добавить строчку:
openssl_conf = openssl_def
-
В самый конец файла добавить строчки:
[openssl_def] engines = engine_section [engine_section] gost = gost_section [gost_section] engine_id = gost dynamic_path = ./gost.dll default_algorithms = ALL CRYPT_PARAMS = id-Gost28147-89-CryptoPro-A-ParamSet
- PROFIT
Если у нас уже есть настоящие (не самоподписанные) ключи, то совершенно некисло было бы проверить их в действии. Да, мы говорим о тестовых целях, но таки доверяй —, но проверяй!
Если поставить винду в виртуальную машину, накатить туда Крипто Про, установить ключи и попробовать их экспортировать, то обнаруживаем удивительную вещь: в экспортере не работает экспорт в PKCS12, а все остальные направления в экспортере заблокированы (англ. «grayed out»).
Гуглю ошибку, и что же мы видим на официальном форуме?
https://www.cryptopro.ru/forum2/default.aspx? g=posts&t=2425
«От Алексея Писинина был получен ответ:
Добрый день. PKCS12 не соответствует требованиям безопасности ФСБ в части хранения закрытых ключей. В теории, закрытые ключи должны храниться на так называемых «съемных» носителях. Собственно, по этой причине и не работает экспорт.»
Я правильно это читаю как, что у них гуй для экспорта есть, но бизнес-логики к нему нету?!
Ппоэтому каких галочек ни нащелкай — всегда будет выпадать ошибка на последнем шаге гуевого мастера?!
Какой же стыд.
Dear God,
Please kill them all.
Love, Greg.
Можно очень долго мучиться, пытаясь засучив C++ вычитать ключ из контейнера с помощью OpenSSL и такой-то матери. Я честно пытался, и разбился об задачу как корабль об скалы (по крайней мере, это задачка больше чем на 1 день для человека, который давно таким не занимался). На просторе интернетов мы не единственные, кто разбился об те же скалы: http://gigamir.net/techno/pub903517
Вот тут наступает момент «это радость со слезами на глазах». Некая контора под названием Лисси Софт, всего за 2 тыщи рублей отдает нам гениальную утилиту P12FromGostCSP. Ее создатели таки победили ту проблему, которую не осилило сообщество, и она выдирает ключи в PFX. Радость — потому что она работает.
Со слезами — потому что это проприетарщина, и она фиг знает как работает.
На картинке Ричард Столлман как бы удивляется и спрашивает: «неужели вы боретесь с проприетарщиной с помощью другой проприетарщины?»
Так что целиком инструкция по перегону ключей выглядит как-то так:
Так как мы деламем все это в тестовых целях, теперь мы подходим к кульминации и начинаем сами себе выдавать ключи.
Как выдать ключи в формате Крипто Про, я особо не заморачивался, потому что это просто не нужно в рамках текущей задачи. Но на всякий случай, существует сервис выдачи таких ключей: http://www.cryptopro.ru/certsrv/
- Все действия производить на Windows (подойдет виртуальная машина) с установленным Крипто Про CSP;
- Открыть сайт и перейти к выдаче ключа;
- Нужно выбрать пункт «Сформировать ключи и отправить запрос на сертификат» и нажать кнопку «Дальше»;
- Щелкнуть по ссылке «Создать и выдать запрос к этому ЦС»;
- Заполнить необходимые поля;
- Нажат кнопку «Выдать»;
- Установить сертификат.
Этот вопрос широко представлен в интернете, поэтому можно сразу смотреть Stackoverflow:
http://stackoverflow.com/questions/14580340/generate-gost-34–10–2001-keypair-and-save-it-to-some-keystore
Идея в том, что раз уж мы все равно используем Bouncy Castle, то им же можем и сгенерить ключ.
Этот код не самый идеальный, но дает реально работающую реализацию (на практике у меня в результате получилось несколько объемных классов, чтобы сделать удобный интерфейс)
Security.addProvider( new org.bouncycastle.jce.provider.BouncyCastleProvider() );
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance( "ECGOST3410", "BC" );
keyPairGenerator.initialize( new ECGenParameterSpec( "GostR3410-2001-CryptoPro-A" ) );
KeyPair keyPair = keyPairGenerator.generateKeyPair();
org.bouncycastle.asn1.x500.X500Name subject = new org.bouncycastle.asn1.x500.X500Name( "CN=Me" );
org.bouncycastle.asn1.x500.X500Name issuer = subject; // self-signed
BigInteger serial = BigInteger.ONE; // serial number for self-signed does not matter a lot
Date notBefore = new Date();
Date notAfter = new Date( notBefore.getTime() + TimeUnit.DAYS.toMillis( 365 ) );
org.bouncycastle.cert.X509v3CertificateBuilder certificateBuilder = new org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder(
issuer, serial,
notBefore, notAfter,
subject, keyPair.getPublic()
);
org.bouncycastle.cert.X509CertificateHolder certificateHolder = certificateBuilder.build(
new org.bouncycastle.operator.jcajce.JcaContentSignerBuilder( "GOST3411withECGOST3410" )
.build( keyPair.getPrivate() )
);
org.bouncycastle.cert.jcajce.JcaX509CertificateConverter certificateConverter = new org.bouncycastle.cert.jcajce.JcaX509CertificateConverter();
X509Certificate certificate = certificateConverter.getCertificate( certificateHolder );
KeyStore keyStore = KeyStore.getInstance( "JKS" );
keyStore.load( null, null ); // initialize new keystore
keyStore.setEntry(
"alias",
new KeyStore.PrivateKeyEntry(
keyPair.getPrivate(),
new Certificate[] { certificate }
),
new KeyStore.PasswordProtection( "entryPassword".toCharArray() )
);
keyStore.store( new FileOutputStream( "test.jks" ), "keystorePassword".toCharArray()
В принципе, это не особо нужно, потому что у нас уже есть простой и удобный способ выдавать JKS, а JKS для Java это самое что ни на есть родное решение. Но для полноты картины, пусть будет.
- Подготовить OpenSSL с ГОСТом по инструкции (есть в этой статье).
- Сделать Cerificate Signing Request + приватный ключ (вписать нужные данные о ключе!):
openssl req -newkey gost2001 -pkeyopt paramset:A -passout pass:aofvlgzm -subj "/C=RU/ST=Moscow/L=Moscow/O=foo_bar/OU=foo_bar/CN=developer/emailAddress=olegchiruhin@gmail.com" -keyout private.key.pem -out csr.csr
- Подписать приватным ключом (на Windows эту операцию нужно делать с правами Administrator, иначе свалится с ошибкой «unable to write 'random state'»):
openssl x509 -req -days 365 -in csr.csr -signkey private.key.pem -out crt.crt
- Получить публичный ключ:
openssl x509 -inform pem -in crt.crt -pubkey -noout > public.key.pem
- GOST2001-md_gost94 hex (если надо):
openssl.exe dgst -hex -sign private.key.pem message.xml
- MIME application/x-pkcs7-signature (если надо):
openssl smime -sign -inkey private.key.pem -signer crt.crt -in message.xml
- Превратить pem в pkcs12:
openssl pkcs12 -export -out private.key.pkcs12 -in private.key.pem -name "alias"
В результате всех вышеописанных действий мы получили относительно легий способ избавиться от тяжкой ноши Крипто Про.
В дальнейшем хотелось бы продолжить борьбу за выпил до финальной победы: оформить все утилиты, генераторы ключей, самописные смэв-клиенты итп в виде одного репозитория на Гитхабе. Еще, очень хотелось бы получить права на модификацию и распространение под пермиссивной лицензией официального клиента СМЭВ. Тогда половина этой статьи была бы просто не нужна, и проблема решалась бы скачиванием нужного кода с Гитхаба.