В этот статье я расскажу, как подписать произвольное сообщение приватным ключом и сертификатом по алгоритму ГОСТ Р 34.11/34.10-2001 присоединённой (attached) подписью на языке Java.

Для проекта электронного документооборота мне потребовалось сделать подпись алгоритмом ГОСТ. Несмотря на то, что появился он давным-давно, к своему удивлению, я не смог найти в сети ни одного завершённого примера, который бы получал на вход сообщение, ключ и сертификат, а на выходе давал бы подписанное сообщение.

Все найденные примеры или использовали стороннее платное ПО КриптоПро, или не собирались с современными версиями 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 файл успешно проходит проверку подписи, например, здесь.

Надеюсь, этот пример будет полезен. Если потребуется, перевести на другой язык, думаю, не составит труда.

Комментарии (5)


  1. malloydev
    16.04.2018 12:51

    Насколько я помню, сейчас все начинают переходить на ГОСТ Р 34.10-2012, а этот алгоритм поддерживается только на новых версиях gost движка и ставится он только на OpenSSL 1.1.x. Так что в ближайшее время скорее всего вам придется все таки ставить «замороченным способом»


    1. LPDem Автор
      16.04.2018 12:51

      Будем решать проблемы по мере их появления )


  1. malloydev
    16.04.2018 12:55

    До 31 декабря текущего года у вас время есть для изучения этого вопроса)


  1. Balyk
    18.04.2018 09:15

    Очень интересует аналогичный вопрос, но проработанный на netcore 2. Нужен код, который мог бы работать на Windows и Linux.


    1. LPDem Автор
      18.04.2018 09:58

      У BouncyCastle есть пакет для net core, по идее, всё должно быть примерно так же:
      www.nuget.org/packages/BouncyCastle.NetCore