На Хабре есть великолепная статья "Побег из Крипто Про. Режиссерская версия, СМЭВ-edition", но наступил 2019 год и все УЦ стали выдавать ЭЦП по ГОСТ 34.10-2012 вместо ГОСТ 34.10-2001.
Под катом рассказ как можно модифицировать свой софт на Bouncy Castle для поддержки работы с ключами по новым гостам.
Disclaimer
О юридических тонкостях подписывания документов через Bouncy Castle и прочих СКЗИ я ничего не знаю и общаться не готов. Перед использованием кода в продакшене проконсультируйтесь с юристом.
А зачем это вообще надо? хорошо написанно оригинальной статье. Повторяться не буду.
Получение ключа с Токена
Все известные мне УЦ выдают ключи с сертификатами на подобных токенах. На токен записан контейнер КриптоПро с приватным ключом и сертификатом. При экспорте ключа через CryptoPro CSP он экспортируется в особый «КриптоПро pfx» не совместимый ни с чем.
Просьбы выдать ключ в стандартном pfx или любом другом типовом контейнере УЦ игнорируют.
Если кто знает УЦ выдающие подписи в стандартных контейнерах поделитесь координатами в комментариях. Хороших людей не стыдно попиарить.
Для преобразования контейнера КриптоПро в стандартный pfx мы как и в оригинальной статье будем использовать P12FromGostCSP. Старые взломанные версии не работают с ключами по 2012 Госту. Идем на сайт авторов и покупаем новую.
Итак мы получили стандартный pfx с ключом и сертификатом.
Обновление Bouncy Castle
Обновляем Bouncy Castle до 1.60 Старые версии могут не поддерживать алгоритмы ГОСТ 2012.
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.60</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.60</version>
</dependency>
Инициализация Bouncy Castle
static {
BouncyCastleProvider bcProvider = new BouncyCastleProvider();
String name = bcProvider.getName();
Security.removeProvider(name); // remove old instance
Security.addProvider(bcProvider);
}
Разбор pfx
KeyStore keyStore = KeyStore.getInstance("PKCS12", "BC");
ByteArrayInputStream baos = new ByteArrayInputStream(pfxFileContent);
keyStore.load(baos, password.toCharArray());
Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
if (keyStore.isKeyEntry(alias )) {
Key key = keyStore.getKey(alias , keyPassword.toCharArray());
java.security.cert.Certificate certificate = keyStore.getCertificate(alias );
addKeyAndCertificateToStore((PrivateKey)key, (X509Certificate)certificate);
}
}
Алиасы обязательно изменяем. Утилита P12FromGostCSP задает всегда один и тот же алиас «csp_exported» и при обработке уже второго ключа будут проблемы.
Для удобства работы ключ из pfx необходимо загрузить в стандартный Java KeyStore и дальше работать только с ним.
Загрузка KeyStore
FileInputStream is = new FileInputStream(keystorePath);
keystore = KeyStore.getInstance(KeyStore.getDefaultType());
char[] passwd = keystorePassword.toCharArray();
keystore.load(is, passwd);
Сохранение ключа с сертификатом в KeyStore
public void addKeyAndCertificateToStore(PrivateKey key, X509Certificate certificate) {
synchronized (this) {
keystore.setKeyEntry(alias.toLowerCase(), key, keyPassword.toCharArray(), new X509Certificate[] {certificate});
FileOutputStream out = new FileOutputStream(keystorePath);
keystore.store(out, keystorePassword.toCharArray());
out.close();
}
}
Загрузка ключей и сертификатов из KeyStore
Enumeration<String> aliases = keystore.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
if (keystore.isKeyEntry(alias)) {
Key key = keystore.getKey(alias, keyPassword.toCharArray());
keys.put(alias.toLowerCase(), key); //any key,value collection
Certificate certificate = keystore.getCertificate(alias);
if (certificate instanceof X509Certificate)
certificates.put(alias.toLowerCase(), (X509Certificate) certificate); //any key,value collection
}
}
Подпись файла
CMSProcessableByteArray msg = new CMSProcessableByteArray(dataToSign);
List certList = new ArrayList();
certList.add(cert);
Store certs = new JcaCertStore(certList);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
ContentSigner signer = new org.bouncycastle.operator.jcajce.JcaContentSignerBuilder("GOST3411WITHECGOST3410-2012-256").setProvider("BC").build(privateKey);
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().setProvider("BC").build()).build(signer, certificate));
gen.addCertificates(certs);
CMSSignedData sigData = gen.generate(msg, false);
byte[] sign = sigData.getEncoded(); //result here
Есть вариант JcaContentSignerBuilder(«GOST3411WITHECGOST3410-2012-512») для 512 битовых ключей. Мои УЦ выдают 256 битные ничего не спрашивая и игнорируют уточняющие вопросы.
Проверка подписи
byte[] data = ...; //signed file data
byte[] signature = ...;//signature
boolean checkResult = false;
CMSProcessable signedContent = new CMSProcessableByteArray(data);
CMSSignedData signedData;
try {
signedData = new CMSSignedData(signedContent, signature);
} catch (CMSException e) {
return SIGNATURE_STATUS.ERROR;
}
SignerInformation signer;
try {
Store<X509CertificateHolder> certStoreInSing = signedData.getCertificates();
signer = signedData.getSignerInfos().getSigners().iterator().next();
Collection certCollection = certStoreInSing.getMatches(signer.getSID());
Iterator certIt = certCollection.iterator();
X509CertificateHolder certHolder = (X509CertificateHolder) certIt.next();
X509Certificate certificate = new JcaX509CertificateConverter().getCertificate(certHolder);
checkResult = signer.verify(new JcaSimpleSignerInfoVerifierBuilder().build(certificate));
} catch (Exception ex) {
return SIGNATURE_STATUS.ERROR;
}
Проверка подписи полностью аналогична проверке по Госту 2001 года. Можно ничего не менять.
Резюме
В результате всех вышеописанных действий мы получили относительно легий способ избавиться от тяжкой ноши Крипто Про в 2019 году.
Автор: BugM