Для проекта электронного документооборота мне потребовалось сделать подпись алгоритмом ГОСТ. Несмотря на то, что появился он давным-давно, к своему удивлению, я не смог найти в сети ни одного завершённого примера, который бы получал на вход сообщение, ключ и сертификат, а на выходе давал бы подписанное сообщение.
Все найденные примеры или использовали стороннее платное ПО КриптоПро, или не собирались с современными версиями Java, или подписанные сообщения потом не валидировались.
В общем, я потратил много времени, чтобы разобраться, и решил, что полный готовый пример кому-нибудь да пригодится.
Для подписи нам нужны сертификат и приватный ключ.
Мне их передали в формате pfx, из него составные части надо извлечь.
Я всё делал на windows и использовал сборку OpenSsl с поддержкой ГОСТ. Для других ОС, думаю, действия будут аналогичными. В OpenSsl с версии 1.1.0 встроенную поддержку ГОСТ убрали, её надо подключать замороченным способом, который у меня с ходу не взлетел. Поэтому я просто скачал старую версию 1.0.2.
В конфиг 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
Запускаем консоль и вводим команду (без неё у меня конфиг на находился):
set OPENSSL_CONF=C:\папка_с_openssl\bin\openssl.cfg
Экспортируем ключ в формат pkcs12:
openssl pkcs12 -in my.pfx -nocerts -nodes -out my.pem
Переводим ключ в формат pkcs8:
openSSL pkcs8 -in my.pem -topk8 -nocrypt -out key.pk8
Экспортируем сертификат:
openssl pkcs12 -in my.pfx -nokeys -out my.cer
При выполнении команд будет запрошен пароль от pfx, его, разумеется, надо знать.
Для подписывания на Java я использовал библиотеку BouncyCastle, она поддерживает ГОСТ.
У меня проект на Maven, я добавил в pom.xml зависимости:
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.59</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk15on</artifactId> <version>1.59</version> </dependency>
Код метода подписывания:
public static byte[] signWithGost3410(byte[] data, X509Certificate certificate, byte[] encodedPrivateKey) throws Exception { X509Certificate[] certificates = new X509Certificate[1]; certificates[0] = certificate; PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedPrivateKey); KeyFactory keyFactory = KeyFactory.getInstance("ECGOST3410", "BC"); PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec); CMSTypedData msg = new CMSProcessableByteArray(data); Store certStore = new JcaCertStore(Arrays.asList(certificates)); CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); ContentSigner signer = new org.bouncycastle.operator.jcajce.JcaContentSignerBuilder("GOST3411withECGOST3410").setProvider("BC").build(privateKey); gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().setProvider("BC").build()).build(signer, (X509Certificate) certificates[0])); gen.addCertificates(certStore); CMSSignedData sigData = gen.generate(msg, true); return sigData.getEncoded(); }
Код метода чтения ключа из файла:
public static byte[] readEncodedKeyFromPk8File(String filename) throws Exception { byte[] content = Files.readAllBytes(Paths.get(filename)); ArrayList<String> lines = new ArrayList<>(Arrays.asList(new String(content).split("\n"))); lines.remove(0); lines.remove(lines.size() -1); String base64 = String.join("", lines); byte[] encoded = Base64.getDecoder().decode(base64); return encoded; }
Код метода чтения сертификата из файла:
public static X509Certificate readX509CertificateFromCerFile(String filename) throws Exception { CertificateFactory factory = CertificateFactory.getInstance("X.509"); Certificate certificate = factory.generateCertificate(new FileInputStream(filename)); return (X509Certificate) certificate; }
Ну и, наконец, пример подписи:
@Test public void signTest() throws Exception{ Security.addProvider(new BouncyCastleProvider()); byte[] key = readEncodedKeyFromPk8File("key.pk8"); X509Certificate certificate = readX509CertificateFromCerFile("my.cer"); byte[] data = Files.readAllBytes(Paths.get("my.xml")); byte[] signedData = signWithGost3410(data, certificate, key); try(FileOutputStream stream = new FileOutputStream("signed.dat")){ stream.write(signedData); } }
Полученный .dat файл успешно проходит проверку подписи, например, здесь.
Надеюсь, этот пример будет полезен. Если потребуется, перевести на другой язык, думаю, не составит труда.

