Как подписать сообщение алгоритмом ГОСТ Р 34.11/34.10-2001 на Java

    В этот статье я расскажу, как подписать произвольное сообщение приватным ключом и сертификатом по алгоритму ГОСТ Р 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
    • 0
      Насколько я помню, сейчас все начинают переходить на ГОСТ Р 34.10-2012, а этот алгоритм поддерживается только на новых версиях gost движка и ставится он только на OpenSSL 1.1.x. Так что в ближайшее время скорее всего вам придется все таки ставить «замороченным способом»
      • 0
        Будем решать проблемы по мере их появления )
      • 0
        До 31 декабря текущего года у вас время есть для изучения этого вопроса)
        • 0
          Очень интересует аналогичный вопрос, но проработанный на netcore 2. Нужен код, который мог бы работать на Windows и Linux.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое