Введение

Если вы Java-разработчик, которому для личных нужд нужно поднять TLS 1.3 на ГОСТ или подписать документ CAdES, создать свой тестовый УЦ — эта статья для вас. Все примеры в этой статье одинаково работают что на JVM 21, что на Android 12+ — тот же самый API, без единого отличия в коде.

crypto-gost —   реализация открытых стандартов ГОСТ (RFC) на Java 21+ и Android 12+, а также сопутствующая обвязка вокруг них, позволяющая в полной мере использовать ГОСТ в инфраструктуре Java.

Данная библиотека не является сертифицированной и не предназначена для работы там, где требуются сертифицированные средства криптографии. Библиотека ничего не гарантирует, включая отсутствие ошибок и правильной реализации стандартов. Библиотека написана для моих личных проектов в академических целях. Использование библиотеки для любых иных проектов лицензия не запрещает, но каждому разработчику необходимо руководствоваться действующим законодательством, юридическими нормами для вашей конкретной организации, ситуации и проекта.

Библиотека состоит из следующих основных модулей:

  • crypto-gost-core — криптографическое ядро алгоритмов ГОСТ: Стрибог, Кузнечик, ГОСТ Р 34.10-2012, JCA-провайдер.

  • crypto-gost-pkix — ГОСТ-инфраструктура PKIX: OID-реестр, CMS SignedData/EnvelopedData/SignedAndEnvelopedData, извлечение и формирование сертификатов из PKCS#7/.p7b, X.509-сертификаты, OCSP, CRL, CSR (PKCS#10), PKCS#12, CAdES-BES/T/C/X-Long Type 1, TSP, Countersignature.

  • crypto-gost-tls13 — Реализация протокола TLS 1.3 (RFC 8446) с поддержкой российских криптографических алгоритмов согласно RFC 9367.

  • crypto-gost-jsse — реализация JSSE-провайдера для Java.

Проблематика

Я считаю, что безопасность относится к фундаментальным свойствам любой ИТ-системы и является частью инфраструктурного уровня. Она должна быть бесплатной, быть простой для подключения, удобной для разработчиков, с минимальными рисками неверного использования.

Для государственных структур или коммерческих компаний есть обязательные требования по приобретению и использованию сертифицированных средств криптографии, а также требования по их встраиванию и наличию на это соответствующих лицензий.

Но что делать простым Java-разработчикам, которым в личных целях необходимо написать обычный сайт, поднять тестовый стенд или админку, сделать несложный защищенный обмен данными? Во всех этих задачах нужен как минимум TLS, без этого никуда. Из коробки, разработчику в большом количестве доступны только зарубежные алгоритмы шифрования и подписи. А реализация отечественного ГОСТа доступна в существенно ограниченном виде. В текущих условиях стандарты ГОСТ в области криптографии стали актуальнее по многочисленным причинам. К таким причинам можно отнести недавний запрет на выдачу SSL/TLS- сертификатов для доменной зоны .ru от зарубежных компаний. Или привнесенные уязвимости в пакетах и библиотеках к популярным языкам программирования. В текущих условиях нет невозможности доверять программным средствам использующим зарубежную криптографию из-за отсутствия контроля над ними.

Какие пути есть у Java-разработчика по использованию ГОСТ для этих целей?

  • Bouncycastle. Этот вариант несет все риски использования зарубежных технологий, необходимость контроля обновлений. Библиотека развивает реализацию алгоритмов и стандартов ГОСТ по остаточному принципу. В качестве примера развития ГОСТ по остаточному принципу приведу CVE-2025-14813 для режима шифрования CTR. Атакующий может восстановить открытый текст без знания ключа. Уязвимы все версии BC-JAVA от 1.59 до 1.83; Реализация отечественных стандартов и RFC идет с явным запозданием и непонятной степенью чистоты реализации.
    О дополнительных рисках использования Bouncycastle именно для ГОСТ-алгоритмов я привел главу в документации к проекту.

  • OpenSSL+gost-engine+JNI. Этот вариант несет для Java-разработчика ад сборки и платформенной зависимости, несовместимости разных версий openssl в области ГОСТ;

  • Покупка за личные средства платных крипто-провайдеров JCP, установка платформо-зависимых компонентов в каждую ОС и далее поверх них установки Java-обертки. Можно, но дорого и есть юридические нюансы, которые нужно соблюдать.

  • Использовать малоизвестные библиотеки, предоставляющие очень ограниченный набор базовых крипто-операций. На этом далеко не уедешь, кроме примитивных задач.

Есть еще задачи, вне зависимости от языка программирования и выбранной библиотеки: генерация и отзыв сертификатов, генерация ключей шифрования, передача ключевых данных, шифрование и подпись документов по стандартам, защищенный обмен по сети, которые нужны для решения практических задач. Это всё должно быть у разработчика из коробки.

Все вышеописанные пути прикручивания ГОСТа к проекту сильно уступают тем возможностям, которые зарубежные алгоритмы из ОС и программных средств предоставляют разработчику.

crypto-gost является попыткой создания того же уровня комфорта и функциональности использования ГОСТ "из коробки" для Java-разработчиков. Библиотека написана в академических целях на чистой Java, позволяет построить любую ГОСТ-инфраструктуру as-code не выходя из IDE. Библиотека не требует никаких зависимостей в ОС, кроме собственно JVM. Она поддерживает все актуальные стандарты, необходимые для решения практических задач.

Про лицензию кода библиотеки

Если кратко, то лицензия проекта позволяет использовать код в любых целях, в рамках действующего законодательства, с сохранением моего авторства. Библиотека создана только мной, на мои сбережения в академических целях и не принадлежит никакой компании. Я написал её для личных нужд в период, когда искал работу.

Основные принципы и философия crypto-gost

При проектировании, реализации и аудите кода я пользовался следующими императивами:

  • Только ГОСТ-алгоритмы и стандарты , никаких зарубежных алгоритмов;

  • Безопасность — главный приоритет. Constant-time операции, строгая гигиена затирания ключевого материала, защита от downgrade/timing/truncation атак. Никаких старых стандартов. Поэтому только TLS 1.3, т.к. в TLS 1.2 разработчику есть возможность ошибиться при настройке, понизив тем самым безопасность.

    • fail-closed-принцип на security-критичных неоднозначностях. Если есть нарушение протокола → fatal alert + немедленное закрытие. Никогда не «продолжать с дефолтом».

    • No secrets in debug — никаких секретов в выводе. Ключевой материал (private keys, traffic keys, master secrets, PSK, nonce, session IDs) не должен появляться: в toString() / toDebugString() / логгерах; в сообщениях исключений.

    • Stateless криптооперации, где возможно.

    • Там, где constant-time недостижим на JVM или требует неоправданных затрат, решение принимается осознанно и документируется в ADR с явной фиксацией модели угроз.

    • Библиотека должна иметь защиту от удаленных атак, но не от co-located, когда злоумышленник может исполнять свой код на той же машине.

  • Вторая по значимости характеристика — performance, но не за счет безопасности. Регулярный аудит кода и тесты на давление на GC или лишние аллокации на горячем пути.

  • Готовность к JDK 21 virtual threads.

  • Разработчику должно быть удобно. Все должно быть единообразно и симметрично (например симметричный API).

  • Всё, что нужно разработчику должно быть доступно из API. Никаких переключений типа: в openssl мы сертификаты генерим, парсим сертификат сторонней библиотекой, а в нашем коде мы только шифруем. Генерация сертификатов, выпуск CRL, создание CMS-контейнера, TLS-соединение — всё из одного API, без переключения между инструментами.

  • Совместимость (интероперабельность) с такими инструментами как: OpenSSL, Bouncycastle, отечественные коммерческие крипто-провайдеры. Реализация должна быть совместима, т.к. интеграция с внешними средствами важный параметр.

  • Библиотека должна иметь исчерпывающую документацию для разработчика.

Инфраструктура ГОСТ-сертификатов as code

В этом разделе мы посмотрим какие средства предоставляет crypto-gost разработчику. Единственный пререквизит: подключить maven-зависимость библиотеки к проекту.

Сравните это с тем, как сейчас у вас делается в проектах? Сколько пререквизитов и инструкций по настройке окружения вы должны выполнить, прежде чем получить аналогичный результат?

Больше примеров кода по PKIX инфраструктуре ГОСТ можно найти в репозитории.

Генерация ключей и сертификатов

import org.rssys.gost.api.KeyGenerator;
import org.rssys.gost.api.KeyPair;
import org.rssys.gost.pkix.cert.GostCertificate;
import org.rssys.gost.pkix.cert.GostCertificateBuilder;
import org.rssys.gost.pkix.cert.GostDnParser;
import org.rssys.gost.signature.ECParameters;

ECParameters params = ECParameters.tc26a256();

// 1. Корневой CA — самоподписанный, pathLen=null (без ограничения глубины)
KeyPair rootKp = KeyGenerator.generateKeyPair(params);
byte[] rootDn = GostDnParser.encodeDn("CN=My Root CA,O=MyOrg,C=RU");
GostCertificate rootCert = GostCertificateBuilder.create(params, rootDn)
        .publicKey(rootKp.getPublic())
        .randomSerial()
        .notBeforeNow()
        .validForYears(15)
        .keyUsage(GostCertificateBuilder.KeyUsage.KEY_CERT_SIGN)
        .basicConstraints(true, null)
        .sign(rootKp.getPrivate());

// 2. Промежуточный CA — подписан корнем, pathLen=0 (может подписывать
// только конечные сертификаты, не других CA)
KeyPair intermediateKp = KeyGenerator.generateKeyPair(params);
byte[] intermediateDn = GostDnParser.encodeDn("CN=My Intermediate CA,O=MyOrg,C=RU");
GostCertificate intermediateCert = GostCertificateBuilder.create(params, intermediateDn)
        .publicKey(intermediateKp.getPublic())
        .randomSerial()
        .notBeforeNow()
        .validForYears(5)
        .basicConstraints(true, 0)
        .issuerDn(rootDn)
        .sign(rootKp.getPrivate());

// 3. Серверный сертификат — подписан промежуточным CA
KeyPair serverKp = KeyGenerator.generateKeyPair(params);
GostCertificate serverCert = GostCertificateBuilder.create(params,
                GostDnParser.encodeDn("CN=example.com,O=MyOrg,C=RU"))
        .publicKey(serverKp.getPublic())
        .randomSerial()
        .notBeforeNow()
        .validForYears(1)
        .sanDns("example.com")
        .keyUsage(GostCertificateBuilder.KeyUsage.DIGITAL_SIGNATURE)
        .extendedKeyUsage(GostOids.EXT_SERVER_AUTH)
        .issuerDn(intermediateDn)
        .sign(intermediateKp.getPrivate());
  • basicConstraints(true, null) у корня vs basicConstraints(true, 0) у промежуточного — это реальное ограничение длины цепочки: pathLen=0 у intermediate буквально запрещает ему подписывать других CA, только конечные сертификаты. Один из самых частых источников ошибок при ручной сборке PKI через openssl — забыть это ограничение.

  • issuerDn(...) связывает уровни явно — каждый следующий сертификат ссылается на DN родителя, а подписывается его закрытым ключом (.sign(rootKp.getPrivate())/.sign(intermediateKp.getPrivate())) — цепочка выстраивается через два независимых, но согласованных вызова.

  • .sanDns("example.com") на серверном сертификате — то самое место, без которого verifyHostname() на клиентской стороне не заработает.

Экспорт и импорт ключей и сертификатов (PEM/DER/PKCS12)

Чаще всего разработчик не генерирует для своего проекта ключевые данные, а получает ключи и сертификаты извне. Давайте рассмотрим, как сохранить и загрузить ключи, сертификаты и проверить их актуальность?

import org.rssys.gost.pkix.cert.ChainValidator;
import org.rssys.gost.pkix.cert.GostCertificate;
import org.rssys.gost.pkix.cert.GostPkcs12Builder;
import org.rssys.gost.pkix.cert.GostPkcs12Loader;
import org.rssys.gost.pkix.cert.PkixException;
import org.rssys.gost.signature.PrivateKeyParameters;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;

// ============================================================
// Сериализация ключей и сертификатов
// ============================================================

// Код ниже базируется на примере выше генерации ключей и сертификатов

// PEM ↔ DER: экспорт и импорт
String serverPem = serverCert.toPem();
byte[] serverDer = serverCert.getEncoded();

// Библиотека сама разберется это PEM или DER, избавляя разработчика от "игр с 
// конвертацией" сертификатов в разные форматы
GostCertificate fromPem =
        GostCertificate.fromPemOrDer(serverPem.getBytes(StandardCharsets.US_ASCII));
GostCertificate fromDer = GostCertificate.fromDer(serverDer);

// Байт-в-байт равенство после round-trip
System.out.println("PEM round-trip идентичен: "
        + Arrays.equals(serverCert.getEncoded(), fromPem.getEncoded()));
System.out.println("DER round-trip идентичен: "
        + Arrays.equals(serverCert.getEncoded(), fromDer.getEncoded()));

//PKCS#12: сохранение ключа + полной цепочки для хранения/переноса 
byte[] pfx = GostPkcs12Builder.create()
        .key(serverKp.getPrivate())
        .certificate(serverCert)
        .caCertificate(intermediateCert)   // caCertificate() — аккумулятор,
        .caCertificate(rootCert)           // можно вызывать несколько раз подряд
        .password("changeit".toCharArray())
        .friendlyName("server-cert")
        .build();

GostPkcs12Loader.Result loaded =
        GostPkcs12Loader.load(pfx, "changeit".toCharArray(), /* allowJdkFallback */ false);
PrivateKeyParameters loadedKey = loaded.getPrivateKey();
List<GostCertificate> loadedChain = loaded.getCertificateChain();
System.out.println("Из PKCS12: ключ + цепочка из " + loadedChain.size() + " сертификатов");

// ============================================================
// ChainValidator — проверка цепочек сертификатов 
// ============================================================

// Позитивный кейс: полная цепочка, корень в списке доверенных
List<GostCertificate> fullChain = List.of(serverCert, intermediateCert, rootCert);
ChainValidator.validateChain(fullChain, List.of(rootCert.getPublicKey()));
System.out.println("Цепочка валидна — исключения не было");

// Негативный кейс: пропущен intermediate
// Частая реальная ошибка конфигурации TLS-сервера: отдали leaf и root,
// забыли intermediate. Интуитивно ждёшь DN_MISMATCH (issuer у serverCert —
// DN промежуточного CA, а не root), но в реализации проверка подписи
// стоит раньше проверки DN — до сравнения DN исполнение не доходит.
try {
    ChainValidator.validateChain(
            List.of(serverCert, rootCert),
            List.of(rootCert.getPublicKey()));
    System.out.println("Не должно было пройти!");
} catch (PkixException e) {
    System.out.println("Отклонено, как и ожидалось: " + e.reason()); // SIGNATURE_INVALID
}

// Негативный кейс: корень не в списке доверенных
// Цепочка технически целая и подписи все верны, но доверяем только
// intermediate, а не настоящему корню — валидатор не «догадывается»
// прошагать выше, он требует явного доверия именно к root.
try {
    ChainValidator.validateChain(fullChain, List.of(intermediateCert.getPublicKey()));
    System.out.println("Не должно было пройти!");
} catch (PkixException e) {
    System.out.println("Отклонено, как и ожидалось: " + e.reason()); // ROOT_NOT_SIGNED
}

Проверка отзыва сертификатов (CRL/OCSP)

В примере выше мы загрузили ключ для сервера и проверили цепочку сертификатов. А как узнать, что сертификат не отозван?

import org.rssys.gost.pkix.cert.CrlVerifier;
import org.rssys.gost.pkix.cert.GostCrlBuilder;
import org.rssys.gost.pkix.cert.GostOcspResponseBuilder;
import org.rssys.gost.pkix.cert.OcspVerifier;
import org.rssys.gost.pkix.cert.PkixException;
import org.rssys.gost.pkix.cert.ReasonCode;

// Валидная цепочка ещё не значит «доверяю»

// ============================================================
// CRL и OCSP — методы проверки статуса сертификатов.
// ============================================================

// ChainValidator ответил "да, цепочка подлинная". Но CA мог отозвать
// serverCert уже после выпуска (утечка ключа, замена и т.д.) —
// цепочка при этом остаётся математически валидной. Проверка отзыва —
// отдельный, самостоятельный шаг доверия.

// CRL: "позитивный" кейс — сертификат не в списке отзыва
byte[] emptyCrl = GostCrlBuilder.create(intermediateDn)
        .thisUpdateNow()
        .nextUpdateDays(7)
        .withCrlNumber(1)
        .sign(intermediateKp.getPrivate());

// Клиент проверяет
CrlVerifier.verify(
        emptyCrl,
        serverCert.getSerialNumber(),
        intermediateKp.getPublic(),
        serverCert.getIssuerDnBytes());
System.out.println("CRL: сертификат не отозван — исключения не было");

// CRL: тот же CA публикует новый список, теперь serverCert в нём отозван
byte[] crlWithRevocation = GostCrlBuilder.create(intermediateDn)
        .thisUpdateNow()
        .nextUpdateDays(7)
        .withCrlNumber(2)
        .addRevoked(serverCert.getSerialNumber(), "20250601000000Z")
        .sign(intermediateKp.getPrivate());

// Клиент проверяет
try {
    CrlVerifier.verify(
            crlWithRevocation,
            serverCert.getSerialNumber(),
            intermediateKp.getPublic(),
            serverCert.getIssuerDnBytes());
    System.out.println("Не должно было пройти!");
} catch (PkixException e) {
    System.out.println("CRL: отклонено — " + e.reason()); // REVOKED
}

// OCSP: сертификат не отозван, синхронно и без скачивания всего CRL-списка
byte[] ocspGood = GostOcspResponseBuilder.create(serverCert.getSerialNumber())
        .issuerDn(intermediateDn)
        .good()
        .sign(intermediateKp.getPrivate());

// Клиент проверяет
OcspVerifier.verify(ocspGood, serverCert.getSerialNumber(), intermediateCert);
System.out.println("OCSP: статус good — исключения не было");

// OCSP: тот же CA отвечает "отозван", причина — компрометация ключа
byte[] ocspRevoked = GostOcspResponseBuilder.create(serverCert.getSerialNumber())
        .issuerDn(intermediateDn)
        .revoked("20250601000000Z", ReasonCode.KEY_COMPROMISE)
        .sign(intermediateKp.getPrivate());

// Клиент проверяет
try {
    OcspVerifier.verify(ocspRevoked, serverCert.getSerialNumber(), intermediateCert);
    System.out.println("Не должно было пройти!");
} catch (PkixException e) {
    System.out.println("OCSP: отклонено — " + e.reason()); // REVOKED
}
  • CRL и OCSP подписаны intermediateKp— потому что именно intermediate CA выпустил сертификат. OcspVerifier.verify(..., issuerCert) принимает сам сертификат intermediateCert, а не голый ключ — верификатор сам вычисляет issuerNameHash/issuerKeyHash из него.

  • CrlVerifier.verify и OcspVerifier.verify — единая точка входа и для "структура не валидна" и "сертификат отозван". Это одно и то же checked-исключение PkixException с разным Reason (REVOKED против, скажем, SIGNATURE_INVALID для подделанного CRL/OCSP-ответа). «Не доверяю» в библиотеке всегда выражается одним и тем же способом, а не разными исключениями/кодами возврата в разных проверках.

Для проверки статуса сертификатов онлайн можно воспользоваться безопасными фетчерами, встроенными в crypto-gost:

import org.rssys.gost.jsse.crl.JdkHttpCrlFetcher;
import org.rssys.gost.jsse.ocsp.JdkHttpOcspFetcher;

// URL берём из проверяемого сертификата
String[] cdpUris = serverCert.getCdpUris();    // CRLDistributionPoints
String[] ocspUris = serverCert.getOcspUris();  // AIA: OCSP-responder'ы (без caIssuers)

// Скачиваем онлайн CRL и проверяем 
if (cdpUris != null && cdpUris.length > 0) {
    byte[] crlDer = new JdkHttpCrlFetcher().fetch(cdpUris[0]);
    CrlVerifier.verify(
            crlDer,
            serverCert.getSerialNumber(),
            intermediateKp.getPublic(),
            serverCert.getIssuerDnBytes());
    System.out.println("CRL скачан и проверен: " + cdpUris[0]);
}

// Запрашиваем OCSP-ответ онлайн
if (ocspUris != null && ocspUris.length > 0) {
    byte[] ocspDer = new JdkHttpOcspFetcher()
            .fetch(serverCert.getEncoded(), intermediateCert.getEncoded(), ocspUris[0]);
    OcspVerifier.verify(ocspDer, serverCert.getSerialNumber(), intermediateCert);
    System.out.println("OCSP-ответ получен и проверен: " + ocspUris[0]);
}

URI извлечены из пока недоверенного сертификата. Атакующий, контролирующий выпуск сертификата, может вписать любой URI: внутренние адреса (192.168.x.x, 127.0.0.1), file://, ldap:// и т.д. — прямой SSRF-вектор.

Если фетчить URL из сертификата вручную (через HttpClient напрямую, а не через JdkHttpCrlFetcher/JdkHttpOcspFetcher), то нужно самому: проверить схему (только http/https), заблокировать приватные диапазоны после DNS-резолва, ограничить таймаут и размер ответа. Штатные фетчеры библиотеки (JdkHttpCrlFetcher, JdkHttpOcspFetcher) делают эти проверки автоматически через SsrfGuard.isPrivateAddress().
Остаточный риск: адрес проверяется до запроса, но реальное соединение делает HttpClient, который заново резолвит DNS — то есть между проверкой и соединением есть окно для DNS-rebinding.

Генерация CSR-запроса в УЦ и обработка ответа

Для разработки сайта часто приходится иметь дело со сторонним УЦ, который выдает TLS- сертификаты. Рассмотрим пример цепочки от запроса до ответа:

import org.rssys.gost.jca.spec.GostDerCodec;
import org.rssys.gost.pkix.cert.GostCsrBuilder;
import org.rssys.gost.pkix.cert.GostCsrParser;
import org.rssys.gost.pkix.cert.GostExtensionParser;

import java.util.Arrays;

// ============================================================
// CSR — заявитель формирует запрос, УЦ его обрабатывает
// ============================================================

// Сторона заявителя: генерируем ключ и CSR
KeyPair applicantKp = KeyGenerator.generateKeyPair(params);
byte[] csrDer = GostCsrBuilder.create(
                "CN=new-service.example.com,O=MyOrg,C=RU", applicantKp.getPublic())
        .sanDns("new-service.example.com", "www.new-service.example.com")
        .keyUsage(GostCertificateBuilder.KeyUsage.DIGITAL_SIGNATURE)
        .sign(applicantKp.getPrivate());

System.out.println("CSR сформирован, отправляем в УЦ:");
System.out.println(GostCsrParser.fromDer(csrDer).toPem());

// Сторона УЦ: CSR может прийти откуда угодно
// Сторона УЦ: разбираем CSR
GostCsrParser csr = GostCsrParser.fromDer(csrDer);

// Proof-of-possession: доказывает, что заявитель владеет закрытым ключом,
// соответствующим публичному ключу в CSR — а не просто скопировал чужой
// открытый ключ в запрос от своего имени.
if (!csr.verifySelf()) {
    // Библиотека сама залогирует WARNING с subject перед этим throw —
    // здесь не нужно дублировать сообщение, только решить, что делать дальше
    throw new IllegalArgumentException("CSR отклонён: proof-of-possession не прошёл");
}
System.out.println("PoP подтверждён, subject=" + csr.getSubjectDn());

// УЦ решает, что из запроса принять как есть (SAN — обычно да),
// а что назначить самостоятельно (issuer, срок действия, keyUsage —
// заявитель не должен диктовать эти поля УЦ)
GostExtensionParser.ExtensionsResult requested = csr.getExtensions();

// УЦ выпускает сертификат
GostCertificate issuedCert = GostCertificateBuilder.create(params, csr.getSubjectDn())
        .publicKey(csr.getPublicKey())
        .randomSerial()
        .notBeforeNow()
        .validForYears(1)
        .sanDns(requested.sanDnsNames)              // из CSR
        .keyUsage(GostCertificateBuilder.KeyUsage.DIGITAL_SIGNATURE) // назначает УЦ, не заявитель
        .extendedKeyUsage(GostOids.EXT_SERVER_AUTH)  // тоже назначает УЦ, не заявитель
        .issuerDn(intermediateDn)
        .sign(intermediateKp.getPrivate());

System.out.println("Сертификат выпущен: " + issuedCert.getSubjectDn());

// Ответ уходит обратно заявителю

// Сторона заявителя: получили сертификат, проверяем, что он "наш"
boolean sameKey = Arrays.equals(
        GostDerCodec.subjectPublicKeyPointBytes(issuedCert.getPublicKey()),
        GostDerCodec.subjectPublicKeyPointBytes(applicantKp.getPublic()));
if (!sameKey) {
    throw new IllegalStateException("УЦ вернул сертификат с чужим ключом!");
}

//Сертификат валиден?
ChainValidator.validateChain(
        List.of(issuedCert, intermediateCert, rootCert),
        List.of(rootCert.getPublicKey()));

System.out.println("Сертификат валиден, содержит наш ключ — принимаем");

TLS 1.3: сервер и клиент

Библиотека содержит исчерпывающую документацию, как поднять TLS 1.3 для Jetty 12, Tomcat 11, Undertow 2, Spring boot 3.4, Netty 4.2. Также есть статья в документации к библиотеке как поднять mTLS 1.3 для всей цепочки: клиент + Angie + Java server. Рассмотрим простой пример организации TLS 1.3 между клиентом и сервером.

import org.rssys.gost.api.KeyGenerator;
import org.rssys.gost.api.KeyPair;
import org.rssys.gost.jsse.GostSsl;
import org.rssys.gost.jsse.GostSslException;
import org.rssys.gost.jsse.crl.CrlPolicy;
import org.rssys.gost.jsse.crl.JdkHttpCrlFetcher;
import org.rssys.gost.jsse.ocsp.JdkHttpOcspFetcher;
import org.rssys.gost.pkix.cert.GostCertificate;
import org.rssys.gost.pkix.cert.GostCertificateBuilder;
import org.rssys.gost.pkix.cert.GostPkcs12Builder;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLSocket;
import java.security.cert.X509Certificate;

// ============================================================
// Подготовка: сертификат под localhost + сохранение в PKCS12
// ============================================================
// Для сетевой демонстрации нужен сертификат с SAN=localhost.
// serverCert из PKI-главы выпущен на example.com — переиспользуем тот же
// ключ serverKp, но перевыпускаем сертификат под localhost. В проде здесь
// был бы реальный домен, а не localhost.
GostCertificate localhostCert = GostCertificateBuilder.create(params, "CN=localhost,O=MyOrg,C=RU")
        .publicKey(serverKp.getPublic())
        .randomSerial()
        .notBeforeNow()
        .validForYears(1)
        .sanDns("localhost")
        .keyUsage(GostCertificateBuilder.KeyUsage.DIGITAL_SIGNATURE)
        .issuerDn(intermediateDn)
        .sign(intermediateKp.getPrivate());

// .certificate(certData, keyData) (голый DER) несёт только один
// сертификат — подойдет, только если сертификат сервера подписан  root'ом.
// У нас intermediate CA, поэтому нужен PKCS12: caCertificate() —
// аккумулятор, добавляет обе ступени цепочки в контейнер, который
// сервер целиком отдаст клиенту во время handshake.
byte[] serverPfx = GostPkcs12Builder.create()
        .key(serverKp.getPrivate())
        .certificate(localhostCert)
        .caCertificate(intermediateCert)
        .caCertificate(rootCert)
        .password("changeit".toCharArray())
        .friendlyName("server-cert")
        .build();

// ============================================================
// Сервер: PKCS12 + CA-доверие + автоматическая проверка CRL
// ============================================================
SSLContext serverCtx = GostSsl.builder()
        .certificate(serverPfx, "changeit".toCharArray())
        .trustCa(rootCert.getEncoded())
        .crlPolicy(CrlPolicy.IF_CDP_PRESENT) // Автоматика по загрузе и использованию CRL
        .crlFetcher(new JdkHttpCrlFetcher())
        .buildServerContext();

try (SSLServerSocket serverSocket = GostSsl.serverSocket(8443, serverCtx)) {
    Thread.ofVirtual().start(() -> {
        try (SSLSocket accepted = (SSLSocket) serverSocket.accept()) {
            accepted.startHandshake();
            System.out.println("Соединение принято: "
                    + accepted.getSession().getCipherSuite());
        } catch (Exception e) {
            e.printStackTrace();
        }
    });

    // ============================================================
    // Клиент: проверка сервера
    // ============================================================
    SSLContext clientCtx = GostSsl.builder()
            .trustCa(rootCert.getEncoded())
            .buildClientContext();

    try (SSLSocket socket = GostSsl.socket("localhost", 8443, clientCtx)) {
        System.out.println("Handshake завершён: " + socket.getSession().getProtocol());
    }

    // ============================================================
    // Клиент: та же проверка, но с явной автоматикой OCSP/CRL
    // ============================================================
    // Тот же механизм, что был в PKI-главе, только теперь срабатывает
    // автоматически внутри handshake, а не по ручному вызову
    // CrlVerifier/OcspVerifier — разработчику достаточно один раз
    // объявить политику при сборке контекста.
    SSLContext clientCtxWithRevocationCheck = GostSsl.builder()
            .trustCa(rootCert.getEncoded())
            .ocsp(true)                              // ждать OCSP staple в handshake
            .ocspFetcher(new JdkHttpOcspFetcher())    // fallback, если сервер staple не прислал
            .crlPolicy(CrlPolicy.IF_CDP_PRESENT)
            .crlFetcher(new JdkHttpCrlFetcher())      // SSRF-защита
            .buildClientContext();

    try (SSLSocket socket = GostSsl.socket("localhost", 8443, clientCtxWithRevocationCheck)) {
        System.out.println("Handshake с проверкой отзыва завершён: "
                + socket.getSession().getProtocol());
    }
    // Если у пира отозван сертификат — handshake просто не завершится
}

// ============================================================
// Взаимная аутентификация (mTLS)
// ============================================================
// Клиентский сертификат — issuedCert из CSR-главы, тоже выпущен
// intermediate CA, поэтому клиенту тоже нужна PKCS12 сохранение с цепочкой.
byte[] clientPfx = GostPkcs12Builder.create()
        .key(applicantKp.getPrivate())
        .certificate(issuedCert)
        .caCertificate(intermediateCert)
        .caCertificate(rootCert)
        .password("changeit".toCharArray())
        .friendlyName("client-cert")
        .build();

// Контекст для сервера
SSLContext mtlsServerCtx = GostSsl.builder()
        .certificate(serverPfx, "changeit".toCharArray())
        .trustCa(rootCert.getEncoded())
        .needClientAuth(true)   // без этого клиентский сертификат не запрашивается вообще
        .buildServerContext();

// Контекст для клиента
SSLContext mtlsClientCtx = GostSsl.builder()
        .certificate(clientPfx, "changeit".toCharArray())
        .trustCa(rootCert.getEncoded())
        .buildClientContext();

// Сервер: цикл обработки соединений
try (SSLServerSocket mtlsServerSocket = GostSsl.serverSocket(8444, mtlsServerCtx)) {
    Thread.ofVirtual().start(() -> {
        try (SSLSocket accepted = (SSLSocket) mtlsServerSocket.accept()) {
            accepted.startHandshake();
            var peerCerts = accepted.getSession().getPeerCertificates();
            System.out.println("Клиент проверен: "
                    + ((X509Certificate) peerCerts[0]).getSubjectX500Principal());
        } catch (Exception e) {
            e.printStackTrace();
        }
    });

  // Клиент соединяется с сервером
    try (SSLSocket socket = GostSsl.socket("localhost", 8444, mtlsClientCtx)) {
        System.out.println("mTLS handshake завершён: " + socket.getSession().getProtocol());
    }
}

// ============================================================
// Fail-closed примеры
// ============================================================

// Негативный кейс: клиент доверяет чужому корню 
KeyPair otherRootKp = KeyGenerator.generateKeyPair(params);
byte[] otherRootDer = GostCertificateBuilder.create(params, "CN=Other Root,O=OtherOrg,C=RU")
        .publicKey(otherRootKp.getPublic())
        .randomSerial().notBeforeNow().validForYears(1)
        .basicConstraints(true, null)
        .sign(otherRootKp.getPrivate())
        .getEncoded();

SSLContext wrongTrustCtx = GostSsl.builder().trustCa(otherRootDer).buildClientContext();
try (SSLSocket socket = GostSsl.socket("localhost", 8443, wrongTrustCtx)) {
    System.out.println("Не должно было подключиться!");
} catch (GostSslException e) {
    System.out.println("Отклонено, как и ожидалось: " + e.getClass().getSimpleName()
            + " (" + e.getCause().getClass().getSimpleName() + ")");
    // GostSslException (SSLHandshakeException)
}

// Негативный кейс 2: mTLS, клиент без сертификата, сервер его требует
SSLContext noCertClientCtx = GostSsl.builder().trustCa(rootCert.getEncoded()).buildClientContext();
try (SSLSocket socket = GostSsl.socket("localhost", 8444, noCertClientCtx)) {
    System.out.println("Не должно было подключиться!");
} catch (GostSslException e) {
    System.out.println("Отклонено, как и ожидалось: " + e.getClass().getSimpleName()
            + " (" + e.getCause().getClass().getSimpleName() + ")");
}

Шифрование и подпись документов

Если вам необходимо зашифровать или подписать документ, то для этих задач в библиотеке есть реализация соответствующих RFC для ГОСТ алгоритмов.

Базовая CMS-подпись документа

Это пример CMS SignedData (RFC 5652 + RFC 7091) с подписью ГОСТ Р 34.10-2012 и хэшированием Стрибог-256 (ГОСТ Р 34.11-2012):

import org.rssys.gost.pkix.cms.CmsSignedDataBuilder;
import org.rssys.gost.pkix.cms.CmsSignedDataVerifier;
import org.rssys.gost.pkix.cms.VerifiedSignedData;
import org.rssys.gost.pkix.cert.PkixException;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;

// ============================================================
// CMS SignedData — подпись документа
// ============================================================
byte[] document = "Договор №42 от 01.01.2026".getBytes(StandardCharsets.UTF_8);

// --- Инкапсулированная подпись: данные внутри CMS-контейнера ---
// Подписываем той же identity, что и в примере TLS-сервера в прошлой главе —
// serverKp/localhostCert. .addCertificate(intermediateCert) обязателен:
// без него верификатор увидит в CMS только один сертификат (leaf),
// не найдёт издателя и цепочка оборвётся на INCOMPLETE_CHAIN.
byte[] signedEncapsulated = CmsSignedDataBuilder.create()
        .data(document)
        .addSigner(serverKp.getPrivate(), localhostCert)
        .addCertificate(intermediateCert)
        .build();

VerifiedSignedData result = CmsSignedDataVerifier.verifyAny(signedEncapsulated, rootCert);
System.out.println("Подписант: " + result.signerCertificate().getSubjectDn());
System.out.println("Данные совпадают: " + Arrays.equals(document, result.data()));

// --- Detached-подпись: сам документ остаётся снаружи ---
// Нужна, когда документ должен читаться системой, не понимающей CMS —
// например, XML документ, а CMS рядом лежит отдельным .sig-файлом.
byte[] signedDetached = CmsSignedDataBuilder.create()
        .data(document)
        .addSigner(serverKp.getPrivate(), localhostCert)
        .addCertificate(intermediateCert)
        .detached(true)
        .build();

VerifiedSignedData detachedResult = CmsSignedDataVerifier.verifyAny(signedDetached, rootCert);
System.out.println("Detached: данные НЕ хранятся в CMS = " + detachedResult.data()); // null — данные не хранятся

// Что будет, если забыть промежуточный сертификат
byte[] brokenChain = CmsSignedDataBuilder.create()
        .data(document)
        .addSigner(serverKp.getPrivate(), localhostCert)
        // .addCertificate(intermediateCert) — забыли
        .build();
try {
    CmsSignedDataVerifier.verifyAny(brokenChain, rootCert);
    System.out.println("Не должно было пройти!");
} catch (PkixException e) {
    System.out.println("Отклонено: " + e.reason()); // INCOMPLETE_CHAIN
}

CMS-шифрование документа

Ниже пример шифрования документа CMS EnvelopedData (RFC 5652) с шифрованием Кузнечик CTR-ACPKM (ГОСТ Р 34.12-2015) и обёртыванием сессионного ключа через VKO ГОСТ Р 34.10-2012 / KExp15 согласно RFC 9189.

import org.rssys.gost.pkix.cms.CmsEnvelopedDataBuilder;
import org.rssys.gost.pkix.cms.CmsEnvelopedDataDecryptor;
import org.rssys.gost.pkix.cms.CmsKeyWrap;
import org.rssys.gost.pkix.cms.Kexp15CmsKeyWrap;
import org.rssys.gost.pkix.cert.PkixException;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;

// ============================================================
// CMS EnvelopedData — шифрование для получателя
// ============================================================

byte[] document = "Реквизиты счёта: ...".getBytes(StandardCharsets.UTF_8);

// CmsKeyWrap — stateless-объект алгоритма обёртывания ключа,
// не содержит секретов. В зашифрованный результат попадает только
// его OID ("1.2.643.7.1.1.7.2.1") — отправитель и получатель создают
// свой экземпляр независимо, сам объект по сети не передаётся.
CmsKeyWrap keyWrap = new Kexp15CmsKeyWrap();

// --- Шифрование: используем открытый ключ получателя ---
// Внутри: генерируется случайный CEK (ключ шифрования данных),
// документ шифруется на нём Кузнечиком, а сам CEK оборачивается
// через VKO ГОСТ Р 34.10-2012 на открытом ключе issuedCert.
byte[] enveloped = CmsEnvelopedDataBuilder.create()
        .data(document)
        .addRecipient(issuedCert) // при шифровании нужен только сертификат получателя
        .keyWrap(keyWrap)
        .build();

// --- Расшифрование: используем закрытый ключ получателя ---
byte[] decrypted = CmsEnvelopedDataDecryptor.decrypt(
        enveloped, applicantKp.getPrivate(), issuedCert, keyWrap);

System.out.println("Расшифровано верно: " + Arrays.equals(document, decrypted));

// --- С имитовставкой: для контроля целостности документа ---
// .withOmac() добавляет проверку подлинности (Encrypt-then-MAC) —
// без него шифрование обеспечивает конфиденциальность, но не защищает от подмены данных
byte[] envelopedWithMac = CmsEnvelopedDataBuilder.create()
        .data(document)
        .addRecipient(issuedCert) // сертификат получателя
        .keyWrap(keyWrap)
        .withOmac()
        .build();

// Подмена одного байта должна обнаруживать подмену данных
byte[] tampered = Arrays.copyOf(envelopedWithMac, envelopedWithMac.length);
tampered[tampered.length / 2] ^= 0xFF;

try {
    CmsEnvelopedDataDecryptor.decrypt(tampered, applicantKp.getPrivate(), issuedCert, keyWrap);
    System.out.println("Не должно было пройти!");
} catch (PkixException e) {
    System.out.println("Подмена обнаружена: " + e.reason()); // MAC не совпал — fail-closed
}

Шифрование + подпись документа

Для операций подписи и шифрования документов порядок вложенности даёт разные гарантии: "аудит без расшифрования" или "скрытый подписант".

Отдельного стандарта на комбинацию подписи и шифрования нет — это тот же RFC 5652, применённый дважды: один ContentInfo становится содержимым другого. Алгоритмическая часть внутри CMS (ГОСТ-подпись — RFC 4490, ГОСТ-шифрование — RFC 9189) остаётся такой же, как в предыдущих примерах — просто использована одновременно, в двух слоях одной структуры.

import org.rssys.gost.pkix.cms.CmsSignedAndEnvelopedData;
import org.rssys.gost.pkix.cms.CmsSignedDataVerifier;
import org.rssys.gost.pkix.cms.VerifiedSignedData;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;

// ============================================================
// signThenEncrypt vs encryptThenSign — порядок меняет гарантии
// ============================================================
byte[] contract = "Условия сделки: ...".getBytes(StandardCharsets.UTF_8);

// --- sign-then-encrypt: SignedData внутри EnvelopedData ---
// Подпись скрыта от постороннего наблюдателя. Перехвативший CMS-блоб
// без закрытого ключа получателя не узнает даже, КТО подписал документ и не увидит сам документ.
byte[] signedThenEncrypted = CmsSignedAndEnvelopedData.signThenEncrypt(
        contract, serverKp.getPrivate(), localhostCert, issuedCert);

// signThenEncrypt не встраивает intermediateCert в SignedData —
// поэтому для сборки цепочки его нужно передать явно, вместе с root,
// как доверенные CA (не сам localhostCert — это конечный сертификат,
// не издатель).
VerifiedSignedData r1 = CmsSignedAndEnvelopedData.decryptAndVerify(
        signedThenEncrypted, applicantKp.getPrivate(), issuedCert,
        intermediateCert, rootCert);
System.out.println("sign-then-encrypt: " + r1.signerCertificate().getSubjectDn());

// --- encrypt-then-sign: EnvelopedData внутри SignedData ---
// Подпись видна снаружи. Кто угодно может проверить авторство,
// не имея закрытого ключа для расшифрования. Сам документ не виден.
byte[] encryptedThenSigned = CmsSignedAndEnvelopedData.encryptThenSign(
        contract, serverKp.getPrivate(), localhostCert, issuedCert);

VerifiedSignedData r2 = CmsSignedAndEnvelopedData.verifyAndDecrypt(
        encryptedThenSigned, applicantKp.getPrivate(), issuedCert,
        intermediateCert, rootCert);
System.out.println("encrypt-then-sign: " + Arrays.equals(contract, r2.data()));

// Демонстрация "аудита без ключа": внешний слой encrypt-then-sign — обычный
// SignedData, его можно проверить тем же CmsSignedDataVerifier,
// вообще без закрытого ключа получателя.
VerifiedSignedData outerOnly = CmsSignedDataVerifier.verifyAny(
        encryptedThenSigned, intermediateCert, rootCert);
System.out.println("Подписант виден снаружи: " + outerOnly.signerCertificate().getSubjectDn());

Долговременная подпись BES→T→C→X-Long

Как сделать электронную подпись документа доказуемой не только сейчас, но и через годы? Юридически значимые документы (договоры, налоговая отчётность) должны оставаться доказуемыми годами или десятилетиями, а вся поддерживающая инфраструктура (сертификаты, CRL-серверы, OCSP-респондеры, даже сами TSA) — по природе своей временная.

Для этого существует лестница: BESTCX-Long.

BES (Basic Electronic Signature) — это по сути то же самое, что мы делали в примере выше, плюс вызов .withCAdES(). BES доказывает: "этот документ подписан тем, у кого есть вот этот закрытый ключ". Но не доказывает "когда".

Проблема: если через полгода ключ подписанта скомпрометируют (украдут, или сам подписант заявит "у меня ключ украли месяц назад, эта подпись не моя") — по одной только BES-подписи невозможно доказать, что подпись была поставлена до компрометации, а не после ей же самой, задним числом. Юридически это слабая подпись.

T (Timestamp) — решает проблему "когда". Независимая третья сторона (TSA — служба штампов времени) ставит собственную подпись поверх хэша подписи документа, удостоверяя: "эта конкретная подпись существовала уже в такой-то момент времени". Теперь можно доказать, что подпись стоит раньше даты компрометации ключа.

Новая проблема: чтобы подпись была юридически чистой, проверяющий должен убедиться, что на момент подписи сертификат подписанта не был отозван — не "валиден ли он сейчас", а "был ли валиден тогда". Для этого нужны CRL/OCSP-данные, актуальные на дату подписи.

C (Complete) — встраивает не сами данные CRL/OCSP, а ссылки на них (хэши, указывающие, какие именно записи нужны).

Проблема: это всё ещё ссылки, а не сами данные — чтобы ими воспользоваться, нужно, чтобы исходный сервер CDP/OCSP всё ещё существовал и отвечал через 5-10 лет. Часто это не так — серверы выключают, домены истекают, инфраструктура УЦ меняется.

X-Long (Extended Long) — последний шаг: вместо ссылок прямо внутрь подписи вкладываются все необходимые данные — весь сертификат цепочки, весь CRL, весь OCSP-ответ, какими они были на момент подписи. Теперь подпись полностью самодостаточна: через 20 лет, когда исходный TSA/CDP/OCSP-сервер давно не отвечает, у проверяющего всё равно есть всё необходимое прямо внутри CAdES-блоба — не нужно никуда ходить в сеть.

Ниже приведен пример лестницы в коде:

import org.rssys.gost.pkix.cms.CAdESExtender;
import org.rssys.gost.pkix.cms.CAdESSignerResult;
import org.rssys.gost.pkix.cms.CmsSignedDataBuilder;
import org.rssys.gost.pkix.cms.VerifiedCAdESData;
import org.rssys.gost.pkix.cert.GostCrl;
import org.rssys.gost.pkix.cert.GostCrlBuilder;
import org.rssys.gost.pkix.cert.GostOcspResponse;
import org.rssys.gost.pkix.cert.GostOcspResponseBuilder;
import org.rssys.gost.pkix.tsp.JdkHttpTspTransport;

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;

// ============================================================
// CAdES-BES -> CAdES-T -> CAdES-X Long Type 1
// ============================================================

// --- Шаг 1: CAdES-BES — базовая подпись ---
byte[] document = "Договор №42, версия для долговременного хранения"
        .getBytes(StandardCharsets.UTF_8);

byte[] cadesBes = CmsSignedDataBuilder.create()
        .data(document)
        .addSigner(serverKp.getPrivate(), localhostCert)
        .withCAdES()    // signingCertificateV2 — привязка к конкретному сертификату
        .build();
System.out.println("CAdES-BES: " + cadesBes.length + " байт");

// --- Шаг 2: CAdES-T — метка времени от реального TSA ---
String tsaUrl = "http://pki.tsaserver.ru/tsp2012/tsp.srf";

GostCertificate sertumCa = GostCertificate.fromDer(
        Files.readAllBytes(Path.of("sertum-ca.der")));
GostCertificate guc2022 = GostCertificate.fromDer(
        Files.readAllBytes(Path.of("guc2022.der")));
GostCertificate[] tsaTrusted = {sertumCa, guc2022};

byte[] cadesT = CAdESExtender.addTimestamp(
        cadesBes, tsaUrl, new JdkHttpTspTransport(), tsaTrusted);
System.out.println("CAdES-T: " + cadesT.length + " байт (было " + cadesBes.length + ")");

// --- Шаг 3: доказательства отзыва на момент подписи ---
// CRL проверяет intermediateCert — его мог отозвать корень (rootCert).
// CRL выпускается тем же CA, что выдал intermediateCert, — корнем.
byte[] crlDer = GostCrlBuilder.create(rootCert.getSubjectDnBytes())
        .thisUpdateNow()
        .nextUpdateDays(30)
        .withCrlNumber(1)
        .sign(rootKp.getPrivate());
GostCrl crl = new GostCrl(crlDer);

// OCSP проверяет localhostCert — его мог отозвать intermediate.
// Ответ подписан тем же CA, что выдал сертификат, — intermediate.
byte[] ocspDer = GostOcspResponseBuilder.create(localhostCert.getSerialNumber())
        .issuerDn(intermediateCert.getSubjectDnBytes())
        .good()
        .sign(intermediateKp.getPrivate());
GostOcspResponse ocsp = new GostOcspResponse(ocspDer);

// --- Шаг 4: CAdES-X Long Type 1 — материализуем всё для будущего ---
// Списки crls/ocspList параллельны chain[i]: для каждого сертификата
// цепочки — своё доказательство отзыва (или null, если не нужно).
// chain[0] = localhostCert  → ocspList[0] = OCSP от intermediate
// chain[1] = intermediateCert → crls[1]    = CRL от root
// chain[2] = rootCert       → оба null     — trust anchor, не проверяется
List<GostCertificate> chain = Arrays.asList(localhostCert, intermediateCert, rootCert);
List<GostCrl> crls = Arrays.asList(null, crl, null);
List<GostOcspResponse> ocspList = Arrays.asList(ocsp, null, null);

// tspUrl=null — метка времени уже встроена на шаге 2, повторный вызов
// TSP не нужен. embedXLongType1 пропустит addTimestamp при tspUrl==null.
byte[] cadesXLong = CAdESExtender.embedXLongType1(
        cadesT, chain, crls, ocspList, null, null);
System.out.println("CAdES-X Long: " + cadesXLong.length + " байт");

// --- Шаг 5: верификация «через 10 лет» ---
// Всё внутри: цепочка, CRL, OCSP, метка времени. Ни CDP, ни OCSP-респондер
// не нужны — даже если к тому моменту инфраструктура УЦ давно не отвечает.
VerifiedCAdESData result = CAdESExtender.verifyCAdESXLong(
        cadesXLong, rootCert, sertumCa, guc2022);

System.out.println("Проверено: " + new String(result.data(), StandardCharsets.UTF_8));
for (CAdESSignerResult sr : result.signers()) {
    System.out.println("Подписант: " + sr.signerCertificate().getSubjectDn());
    System.out.println("Меток времени: " + sr.timestamps().size());
}

Заключение

В процессе реализации различных частей библиотеки я не раз убедился, что криптография не терпит дилетантов. В ней миллионы нюансов, которые просто в голову не могут прийти рядовому разработчику. Мне стало очевидно, зачем требуются сертифицированные средства, которые проходят аудит и написаны людьми с соответствующей подготовкой. Ошибиться в криптографии легко, цена этому может быть высока. И тем не менее я решил создать открытый инструмент для себя, который со временем может пройти обкатку на личных проектах, приобрести необходимый уровень зрелости и надежности. Надеюсь вы найдете его полезным, хотя бы для академических целей.

Почему я написал эту статью? Библиотека может представлять академический интерес, т.к. при реализации кода я столкнулся со множеством неочевидных уязвимостей ( обнаружить которые мне помогли различные автоматизированные средства) и нашел пути их устранения. Много ловушек может ожидать разработчиков, если они захотят повторить эксперимент по реализации: в парсерах, цепочках проверки доверия, организации запросов, математике с большими числами.

Разработчикам и специалистам по информационной безопасности могут быть интересны подходы к fuzzing-тестированию для поиска уязвимостей, защита от DoS, методики и подходы к кросс-функциональному тестированию, фиксации архитектурных решений и модели угроз.

Начинающим математикам могут быть интересны подходы по умножению чисел и приемы организации защищенных свойств кода в среде исполнения. По моему мнению зрелость библиотеки достигла того уровня, чтобы ей можно было пользоваться для личных проектов. Дальше только проверка временем и аудит сообщества. Чем больше посмотрят глаз на код библиотеки, тем будет больше гарантий и доверия к коду для меня как автора.

Дальнейшее развитие библиотеки ожидается, когда появятся открытые стандарты на пост-квантовые алгоритмы и возможно, отдельный модуль DTLS 1.3.

Сама библиотека, код, тесты, примеры кода и документы могут служить неплохим подспорьем для тех кто изучает ГОСТ. Удачи в изучении!