Введение
crypto-gost — криптографическая библиотека алгоритмов ГОСТ на Java.
Данная библиотека не является сертифицированной и не предназначена для работы там, где требуются сертифицированные средства криптографии.
Поддерживаемые алгоритмы ГОСТ:
-
ГОСТ Р 34.11-2012 — хэш-функция «Стрибог» 256 и 512 бит (RFC 6986).
-
ГОСТ Р 34.12-2015 — блочный шифр «Кузнечик», ключ 256 бит.
-
ГОСТ Р 34.13-2015 — режимы шифрования CBC, CFB, CTR (ГАММА), OFB; имитовставка (CMAC).
-
ГОСТ Р 34.10-2012 — электронная подпись 256 и 512 бит (RFC 7091).
-
HMAC-Стрибог — RFC 7836 (HMAC-Streebog-256 и HMAC-Streebog-512).
-
MGM (Multilinear Galois Mode) — AEAD-режим для Кузнечика, RFC 9058. Совместим с OpenSSL.
Дополнительные алгоритмы (не ГОСТ):
- SCrypt — функция выработки ключа на основе пароля (RFC 7914).
Создавая эту библиотеку я хотел устранить следующие недостатки Bouncycastle:
-
Иметь криптографическую библиотеку на чистой Java без зависимостей.
-
Только ГОСТ-алгоритмы без лишнего кода, который относится к западным стандартам и не будет требовать аудита;
-
Более высокую скорость шифрования алгоритмом “Кузнечик”.
-
Готовые методы: потоковое шифрование + имитозащита для шифрования файлов или сокетов.
-
Удобные обертки, не требующие глубоких знаний в криптографии.
-
По возможности, устранить недостатки реализации электронной подписи ГОСТ или уязвимости в Bouncycastle.
-
Доступную с открытым кодом и нашей лицензией библиотеку для разработчиков.
Выполнена кросс-валидация на совместимость с openssl (ГОСТ) и Bouncycastle.
Зачем эта библиотека?
Представленная ниже информация является моим частным мнением и оно может являться ошибочным.
Криптобиблиотека может содержать ошибки, которые мне неизвестны (я прилагаю все усилия, чтобы такие найти и закрыть), все риски по её использованию вы берете на себя.
Если вам нужна сильная криптография и гарантии её правильной работы - найдите профессионалов и доверьтесь им.
Разработчикам иногда требуется российская криптография для защиты данных. Для личных проектов или для целей локальной разработки в тестовой среде не всегда возможно использовать сертифицированные средства. Если у вас есть личный проект на нескольких серверах, то для установки в них российской криптографии придется приобрести лицензии. Де-факто “стандартом” в области криптографии для Java для разработчиков стала библиотека Bouncycastle. Этой библиотеке много лет, есть обширная документация и книги, которые я считаю очень полезно почитать.
Однако при использовании Bouncycastle имменно с российской криптографией я столкнулся со следующими трудностями:
-
Очень медленное шифрование/расшифрование по ГОСТ “Кузнечик”.
-
Есть проблемы с безопасностью реализации, некоторые из которых могут носить критический характер (об этом далее);
-
Если мне нужны только ГОСТ-алгоритмы, то 95% остального кода библиотеки мне не нужно.
-
Нужен контроль изменений в новых версиях, как в плане безопасности, так и производительности.
-
Безопасность даже для личных проектов очень важна. Авторы развивают прежде всего западные алгоритмы и устраняют недостатки в них.
Классический алгоритм электронной подписи на эллиптических кривых (ECDSA) при каждой подписи требует некое число k, которое нужно использовать при подписи совместно с закрытым ключом. Некоторрые авторы реализуют ECDSA со случайным k, что требует криптографически качественного генератора случайных чисел (ГСЧ) при каждой подписи. А если ваш сайт или приложение работают в облачной среде, то неизвестно как настроена виртуализация и проброс энтропии в виртуальную машину для выработки случайных чисел k, нужных для каждой подписи.
Хочу отметить некоторые недавние взломы на этой почве:
-
Sony PlayStation 3 (2010) — использовали константный
kпри подписи прошивок. Два уравнения с однимk→ система из двух уравнений → закрытый ключ восстанавливается элементарно. -
Android Bitcoin-кошельки (2013) — java.security.SecureRandom на Android имел дефект инициализации. Повторяющиеся
kу разных пользователей привели к массовой краже биткоинов.
Отсюда можно сделать вывод, что если вы взяли Bouncycastle или любую другую библиотеку, то нужно иметь в виду, что:
-
повтор числа
k→ полная компроментация закрытого ключа; -
предсказуемый
k→ тоже компроментация закрытого ключа; -
слабый ГСЧ(RNG) в облаке или специфических условиях → катастрофа.
-
утечки ключа через атаки по таймингу (timing) или по сторонним каналам (side channels). Да, для криптографии нельзя в лоб писать умножение/деление больших чисел, т.к. это создаёт риск утечки закрытого ключа при наличии доступа к времени выполнения операций (особенно если ваш сервис можно дергать по API). Нужна специальная математика, защитные техники, строгий контроль, чего рядовой разработчик просто не знает, отсюда и обоснование сертификации средсв СКЗИ для серьезных проектов и корректности их встраивания.
SecureRandom в Java сегодня — криптографически надёжный. Основная угроза — не алгоритм, а окружение (энтропия в VM), которую использует BouncyCastle.
Изначально, часть кода этой библиотеки основана на BouncyCastle 1.83. Но сейчас код полностью переработан: исправлены недочёты оригинала, арифметика эллиптических кривых приведена в сторону безопасности, число k выполнено детерминированным (не путать с константным) по RFC 6979 вместо SecureRandom, ускорены алгоритмы шифрования. Реализация приведена в полное соответствие стандартам ГОСТ и RFC. В данной реализации библиотеке упала скорость электронной подписи в угоду безопасности.
Улучшения в безопасности электронной подписи
В таблице приведена сравнительная оценка безопасности электронной подписи между двумя библиотеками. Там где указано constant time(CT) это положительное свойство. Злоумышленник может посылать разные данные и запросы. Если криптографические операции выполняются за одно и то же время, то такое свойство реализации не дает злоумышленнику никаких данных. Отсутствие constant time свойства у операций, это серьезный риск утечки закрытого ключа.
| Свойство | crypto-gost (текущая версия) |
BouncyCastle 1.83 |
|---|---|---|
|
Арифметика поля (ГОСТ-кривые) |
Защищено |
|
|
Координаты точки |
Проективные Якоби, |
ECFieldElement.Fp на BigInteger, координатная система Якоби`. |
|
Timing side-channel арифметики поля |
Защищено |
Уязвимо |
|
Скалярное умножение секретного k |
Защищено |
Уязвимо |
|
Timing side-channel скалярного умножения |
Защищено |
Уязвимо |
|
Инверсия в поле при |
z(p−2) mod p, CT ladder (теорема Ферма) |
|
|
Timing side-channel инверсии z → k |
Защищено |
Уязвимо |
|
Генерация эфемерного ключа k |
Защищено |
|
|
Устойчивость к атакам на RNG |
Защищено |
Зависит от качества источника энтропии в окружении. |
|
Интерпретация хэша (RFC 7091 §5.3) |
LSB-first ( |
Требует отдельной проверки для каждой версии библиотеки |
|
Зачистка ключевого материала |
Выполняется |
Частично |
|
Условная редукция (constant-time) |
constant-time-маска без ветвления в |
Не применимо ( |
|
Вычисление маски бесконечности |
|
Не применимо |
|
Shamir’s trick при верификации подписи |
|
|
|
Совместимость JVM |
JDK 11+ |
JDK 8+ |
Данная реализация crypto-gost претендует на более защищенный уровень по нескольким параметрам, который к тому же не зависит от качества ДСЧ (RNG) в облаке/окружении при подписи сообщений.
Данное утверждение требует подтверждения с помощью независимого аудита.
Нужно отметить существенное падение скорости подписи и проверки. Что лучше: безопасность vs скорость? Данная библиотека поставила целью преследовать безопасность, как наивысший критерий. Какой смысл в быстрой подписи, если любая операция ведет к компроментации закрытого ключа?
Методика бенчмарков описана ниже.
Результаты бенчмарков:
Улучшения в шифровании
Методика бенчмарков описана ниже. Результаты сравнения crypto-gost и BouncyCastle 1.83
Результаты:
Установка
Клонируйте репозиторий. Выполните сборку и установку в локальный .m2: mvn package install.
Затем, добавьте зависимость в pom.xml:
<dependency>
<groupId>org.rssys</groupId>
<artifactId>crypto-gost</artifactId>
<version>0.1.0</version>
</dependency>
Высокоуровневый API
Библиотека предоставляет два независимых API для работы:
-
org.rssys.gost.api— высокоуровневый API, не требует регистрации провайдера. Код лаконичнее, ориентирован на пользователя. -
org.rssys.gost.jca— JCA/JCE-совместимый API, интегрируется со стандартной инфраструктурой Java.
Все статические методы потокобезопасны.
Генерация ключей
Управление симметричными ключами
Настройка провайдера (только для JCA API)
Перед использованием JCA-методов необходимо зарегистрировать провайдер один раз:
import java.security.Security;
import org.rssys.gost.jca.RssysGostProvider;
Security.addProvider(new RssysGostProvider());
Генерация симметричного ключа
Генерация случайного 256-битного ключа для шифра “Кузнечик” (ГОСТ Р 34.12-2015). По умолчанию, как источник энтропии используется класс java.security.SecureRandom, который в JVM имеет хорошие криптографические свойства.
Через org.rssys.gost.api
import org.rssys.gost.api.KeyGenerator;
import org.rssys.gost.cipher.SymmetricKey;
SymmetricKey key = KeyGenerator.generateSymmetricKey();
try {
// использование ключа
} finally {
key.destroy(); // обнуляет ключевой материал в памяти
}
Через JCA (org.rssys.gost.jca)
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
KeyGenerator kg = KeyGenerator.getInstance("Kuznyechik", "RssysGostProvider");
SecretKey key = kg.generateKey();
try {
// использование ключа
} finally {
key.destroy(); // обнуляет ключевой материал в памяти
}
Импорт и экспорт симметричного ключа
Симметричный ключ хранится как сырые байты byte[].
Никогда не храните симметричный ключ в открытом виде. Для хранения используйте шифрование ключа другим ключом (KEK) или вывод из пароля через scrypt (см. ниже).
org.rssys.gost.api
Экспорт ключа в byte[]
import org.rssys.gost.cipher.SymmetricKey;
SymmetricKey key = ...; // выработан или загружен ранее
// getKey() возвращает defensive copy — мутация результата не влияет на ключ внутри объекта
byte[] rawKey = key.getKey(); // 32 байта
try {
// сохранить rawKey в зашифрованном хранилище ...
} finally {
java.util.Arrays.fill(rawKey, (byte) 0); // обнулить после использования
}
Импорт ключа из byte[]
import org.rssys.gost.cipher.SymmetricKey;
byte[] rawKey = ...; // 32 байта, загружены из хранилища
// Конструктор делает defensive copy — мутация rawKey после не влияет на ключ
SymmetricKey key = new SymmetricKey(rawKey);
try {
// использование ключа ...
} finally {
key.destroy(); // обнуляет ключ внутри объекта
java.util.Arrays.fill(rawKey, (byte) 0); // обнулить исходный массив
}
Вывод ключа из пароля (scrypt, RFC 7914)
import org.rssys.gost.api.KeyGenerator;
import org.rssys.gost.cipher.SymmetricKey;
// Соль должна быть случайной и уникальной для каждого пользователя/объекта
// Минимальная рекомендуемая длина соли: 16 байт
byte[] password = "секретный пароль".getBytes(StandardCharsets.UTF_8);
byte[] salt = new byte[16];
new java.security.SecureRandom().nextBytes(salt); //
SymmetricKey key = KeyGenerator.deriveKey(password, salt); //
try {
// использование ключа ...
} finally {
key.destroy();
java.util.Arrays.fill(password, (byte) 0); //
}
-
Соль не является секретом — её можно хранить рядом с зашифрованными данными. Главное требование: соль уникальна для каждого объекта шифрования.
-
Параметры по умолчанию: N=524288, r=8, p=1. Операция ресурсоёмкая — не вызывайте в цикле и не блокируйте UI-поток.
-
deriveKeyне обнуляет массив пароля — это ответственность вызывающего кода.
JCA (org.rssys.gost.jca)
Экспорт ключа в byte[]
import org.rssys.gost.jca.key.GostSecretKey;
GostSecretKey key = ...; // выработан или загружен ранее
// getEncoded() возвращает defensive copy
byte[] rawKey = key.getEncoded(); // 32 байта, null если ключ уничтожен
try {
// сохранить rawKey в зашифрованном хранилище ...
} finally {
if (rawKey != null) java.util.Arrays.fill(rawKey, (byte) 0);
}
Импорт ключа из byte[] через SecretKeyFactory (рекомендуется)
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.SecretKeySpec;
byte[] rawKey = ...; // 32 байта, загружены из хранилища
SecretKeySpec spec = new SecretKeySpec(rawKey, "Kuznyechik");
SecretKeyFactory skf = SecretKeyFactory.getInstance("Kuznyechik", "RssysGostProvider");
SecretKey key = skf.generateSecret(spec);
try {
// использование ключа ...
} finally {
key.destroy();
java.util.Arrays.fill(rawKey, (byte) 0);
}
Импорт ключа через прямой конструктор GostSecretKey
import org.rssys.gost.jca.key.GostSecretKey;
byte[] rawKey = ...; // 32 байта, загружены из хранилища
// algorithm: "Kuznyechik", "HmacGOST3411-2012-256", "HmacGOST3411-2012-512", "CMAC-Kuznyechik"
GostSecretKey key = new GostSecretKey("Kuznyechik", rawKey);
try {
// использование ключа ...
} finally {
key.destroy();
java.util.Arrays.fill(rawKey, (byte) 0);
}
Конвертация между SymmetricKey и GostSecretKey
import org.rssys.gost.cipher.SymmetricKey;
import org.rssys.gost.jca.key.GostSecretKey;
// api/ → JCA: SymmetricKey → GostSecretKey
SymmetricKey symKey = ...; // из api/
GostSecretKey jcaKey = new GostSecretKey("Kuznyechik", symKey); //
// JCA → api/: GostSecretKey → SymmetricKey
GostSecretKey jcaKey2 = ...; // из JCA
SymmetricKey symKey2 = jcaKey2.toSymmetricKey(); //
- Конвертация создаёт копию ключевых байт. Уничтожение одного объекта не влияет на другой — вызывайте
destroy()на обоих.
Шифрование данных
Все алгоритмы используют шифр Кузнечик (ГОСТ Р 34.12-2015) с ключом 256 бит. Ключи считаются выработанными на предыдущем шаге (см. раздел «Управление симметричными ключами»).
Выбор алгоритма
В таблице приведены только некоторые режимы, рекомендуемые для начала, обеспечивающие безопасную работу. Под API в таблице понимается или org.rssys.gost.api или org.rssys.gost.jca
| Сценарий | Доступность в API | Алгоритм в API / JCA |
|---|---|---|
|
Шифрование блока данных аутентификацией и контролем целостности, |
|
|
|
Шифрование блока данных |
|
|
|
Потоковое шифрование (файл или сокет) с аутентификацией и контролем целостности |
|
|
|
Файл или сокет без аутентификации |
|
|
|
Шифрование буфера с аутентификацией целостности |
|
|
Шифрование буфера
org.rssys.gost.api
AuthenticatedCipher — CTR + CMAC (ГОСТ Р 34.13-2015) с аутентификацией и контролем целостности данных - рекомендуется для большинства задач.
import org.rssys.gost.api.AuthenticatedCipher;
import org.rssys.gost.cipher.SymmetricKey;
import org.rssys.gost.util.AuthenticationException;
SymmetricKey key = ...; // выработан ранее
byte[] plaintext = "открытые данные".getBytes(StandardCharsets.UTF_8);
// Шифрование: IV генерируется автоматически
// Формат пакета: [IV (8 байт)] [CMAC от открытого текста (8 байт)] [шифртекст]
byte[] packet = AuthenticatedCipher.seal(plaintext, key);
// Расшифрование
try {
byte[] decrypted = AuthenticatedCipher.open(packet, key);
} catch (AuthenticationException e) {
// CMAC не совпал: данные повреждены или подменены
} finally {
key.destroy();
}
MgmCipher — AEAD без AAD (Как альтернатива AuthenticatedCipher, там тоже Кузнечик, но режим не строго ГОСТ)
import org.rssys.gost.api.MgmCipher;
import org.rssys.gost.cipher.SymmetricKey;
import org.rssys.gost.util.AuthenticationException;
SymmetricKey key = ...; // выработан ранее
byte[] plaintext = "открытые данные".getBytes(StandardCharsets.UTF_8);
// Шифрование — ICN генерируется автоматически, шифр "Кузнечик"
// Формат пакета: [ICN (16 байт)] [шифртекст] [тег (16 байт)]
byte[] packet = MgmCipher.seal(plaintext, key);
// Расшифрование
try {
byte[] decrypted = MgmCipher.open(packet, key);
} catch (AuthenticationException e) {
// Тег не совпал: данные повреждены или подменены
}
MgmCipher — AEAD с AAD (ассоциированные данные), режим для специфических задач.
import org.rssys.gost.api.MgmCipher;
import org.rssys.gost.cipher.SymmetricKey;
import org.rssys.gost.util.AuthenticationException;
SymmetricKey key = ...; // выработан ранее
byte[] plaintext = "открытые данные".getBytes(StandardCharsets.UTF_8);
byte[] aad = "заголовок сообщения".getBytes(StandardCharsets.UTF_8);
// см. ниже про AAD
// Шифрование: AAD аутентифицируется, но не шифруется и не включается в пакет
byte[] packet = MgmCipher.seal(plaintext, key, aad);
// Расшифрование: AAD должен совпадать с переданным при шифровании
try {
byte[] decrypted = MgmCipher.open(packet, key, aad);
} catch (AuthenticationException e) {
// Тег не совпал: данные или AAD повреждены / подменены
} finally {
key.destroy();
}
AAD — любые данные (заголовок, метаданные, идентификатор сессии), которые нужно аутентифицировать вместе с шифртекстом, но не шифровать.
Cipher — CTR без аутентификации
import org.rssys.gost.api.Cipher;
import org.rssys.gost.cipher.SymmetricKey;
SymmetricKey key = ...; // выработан ранее
byte[] plaintext = "данные".getBytes(StandardCharsets.UTF_8);
// Шифрование: IV (8 байт) генерируется автоматически и включается в начало результата
// Формат результата: [IV (8 байт)] [шифртекст]
byte[] encrypted = Cipher.encrypt(plaintext, key, Cipher.Mode.CTR);
// Расшифрование: IV извлекается автоматически из начала массива
byte[] decrypted = Cipher.decrypt(encrypted, key, Cipher.Mode.CTR);
key.destroy();
Режим CTR без аутентификации не защищает от подмены данных. Если целостность важна — используйте AuthenticatedCipher (ГОСТ Р 34.13-2015) или MgmCipher (Р 1323565.1.026-2019, рекомендация).
JCA (org.rssys.gost.jca)
Kuznyechik/CTR/NoPadding — без аутентификации и контроля целостности данных
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import org.rssys.gost.jca.key.GostSecretKey;
GostSecretKey key = ...; // выработан ранее
byte[] plaintext = "данные".getBytes(StandardCharsets.UTF_8);
// Шифрование: IV (8 байт) генерируется автоматически
Cipher cipher = Cipher.getInstance("Kuznyechik/CTR/NoPadding", "RssysGostProvider");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] ciphertext = cipher.doFinal(plaintext);
byte[] iv = cipher.getIV(); //
// Расшифрование
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
byte[] decrypted = cipher.doFinal(ciphertext);
key.destroy();
- IV необходимо передать получателю вместе с
ciphertext. NOTE: Режим CTR без аутентификации не защищает от подмены данных. Если целостность важна — используйтеAuthenticatedCipher/AuthenticatedStreamизorg.rssys.gost.api(ГОСТ Р 34.13-2015) илиKuznyechik/MGM/NoPadding(Р 1323565.1.026-2019).
Kuznyechik/MGM/NoPadding — AEAD без AAD
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import org.rssys.gost.jca.key.GostSecretKey;
GostSecretKey key = ...; // выработан ранее
byte[] plaintext = "открытые данные".getBytes(StandardCharsets.UTF_8);
// Шифрование: ICN генерируется автоматически
Cipher cipher = Cipher.getInstance("Kuznyechik/MGM/NoPadding", "RssysGostProvider");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] ciphertext = cipher.doFinal(plaintext); // [шифртекст][тег (16 байт)]
byte[] icn = cipher.getIV(); //
// Расшифрование
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(icn));
try {
byte[] decrypted = cipher.doFinal(ciphertext);
} catch (javax.crypto.AEADBadTagException e) {
// Тег не совпал: данные повреждены или подменены
} finally {
key.destroy();
}
- ICN необходимо передать получателю вместе с
ciphertext. В отличие отMgmCipher.seal, JCA не включает ICN в выходной буфер автоматически.
Kuznyechik/MGM/NoPadding — AEAD с AAD
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import org.rssys.gost.jca.key.GostSecretKey;
GostSecretKey key = ...; // выработан ранее
byte[] plaintext = "открытые данные".getBytes(StandardCharsets.UTF_8);
byte[] aad = "заголовок".getBytes(StandardCharsets.UTF_8);
// Шифрование
Cipher cipher = Cipher.getInstance("Kuznyechik/MGM/NoPadding", "RssysGostProvider");
cipher.init(Cipher.ENCRYPT_MODE, key);
cipher.updateAAD(aad); //
byte[] ciphertext = cipher.doFinal(plaintext);
byte[] icn = cipher.getIV();
// Расшифрование
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(icn));
cipher.updateAAD(aad); //
try {
byte[] decrypted = cipher.doFinal(ciphertext);
} catch (javax.crypto.AEADBadTagException e) {
// Тег не совпал
} finally {
key.destroy();
}
updateAADдолжен вызываться доdoFinal. AAD при шифровании и расшифровании должен совпадать.
Потоковое шифрование (файл / сокет)
org.rssys.gost.api
AuthenticatedStream — предназначен для потокового шифрования сокета/файла с аутентификацией и контролем целостности данных (рекомендуется для большинства задач)
import org.rssys.gost.api.AuthenticatedStream;
import org.rssys.gost.cipher.SymmetricKey;
import org.rssys.gost.util.AuthenticationException;
import java.io.*;
import java.nio.file.*;
SymmetricKey key = ...; // выработан ранее
// Шифрование файла
Path src = Path.of("plaintext.bin");
Path encrypted = Path.of("encrypted.bin");
try (InputStream in = Files.newInputStream(src);
OutputStream raw = Files.newOutputStream(encrypted);
OutputStream out = AuthenticatedStream.sealing(raw, key)) { //
in.transferTo(out);
}
// Расшифрование файла
Path decrypted = Path.of("decrypted.bin");
try (InputStream raw = Files.newInputStream(encrypted);
InputStream in = AuthenticatedStream.opening(raw, key); //
OutputStream out = Files.newOutputStream(decrypted)) {
in.transferTo(out);
} catch (IOException e) {
if (e.getCause() instanceof AuthenticationException) {
// CMAC чанка не совпал: файл повреждён или подменён
}
throw e;
} finally {
key.destroy();
}
-
sealingзаписывает заголовок потока и шифрует данные чанками по 64 КБ. Каждый чанк содержит собственный IV и CMAC — усечение потока обнаруживается автоматически. -
openingпроверяет CMAC каждого чанка до возврата данных вызывающему.
Cipher.encryptingStream / decryptingStream — CTR без аутентификации.
import org.rssys.gost.api.Cipher;
import org.rssys.gost.cipher.SymmetricKey;
import java.io.*;
import java.nio.file.*;
SymmetricKey key = ...; // выработан ранее
// Шифрование: IV (8 байт) записывается в поток автоматически первым
Path src = Path.of("plaintext.bin");
Path encrypted = Path.of("encrypted.bin");
try (InputStream in = Files.newInputStream(src);
OutputStream raw = Files.newOutputStream(encrypted);
OutputStream out = Cipher.encryptingStream(raw, key, Cipher.Mode.CTR)) {
in.transferTo(out);
}
// Расшифрование: IV читается из потока автоматически
Path decrypted = Path.of("decrypted.bin");
try (InputStream raw = Files.newInputStream(encrypted);
InputStream in = Cipher.decryptingStream(raw, key, Cipher.Mode.CTR);
OutputStream out = Files.newOutputStream(decrypted)) {
in.transferTo(out);
} finally {
key.destroy();
}
CTR без аутентификации не обнаруживает повреждение или подмену данных. Для потоков с требованием целостности используйте AuthenticatedStream.
JCA (org.rssys.gost.jca)
CipherOutputStream / CipherInputStream — CTR без аутентификации и контроля целостности данных.
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.spec.IvParameterSpec;
import org.rssys.gost.jca.key.GostSecretKey;
import java.io.*;
import java.nio.file.*;
GostSecretKey key = ...; // выработан ранее
Cipher cipher = Cipher.getInstance("Kuznyechik/CTR/NoPadding", "RssysGostProvider");
// Шифрование: IV генерируется автоматически, записываем его явно перед шифртекстом
Path src = Path.of("plaintext.bin");
Path encrypted = Path.of("encrypted.bin");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] iv = cipher.getIV(); //
try (InputStream in = Files.newInputStream(src);
OutputStream raw = Files.newOutputStream(encrypted)) {
raw.write(iv); //
try (OutputStream out = new CipherOutputStream(raw, cipher)) {
in.transferTo(out);
}
}
// Расшифрование: читаем IV явно из начала потока
Path decrypted = Path.of("decrypted.bin");
try (InputStream raw = Files.newInputStream(encrypted)) {
byte[] ivRead = raw.readNBytes(iv.length); //
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(ivRead));
try (InputStream in = new CipherInputStream(raw, cipher);
OutputStream out = Files.newOutputStream(decrypted)) {
in.transferTo(out);
}
} finally {
key.destroy();
}
- JCA не управляет IV автоматически в потоковом API — приложение отвечает за запись и чтение IV из потока.
Для потоков с аутентификацией целостности используйте AuthenticatedStream из org.rssys.gost.api.
JCA MGM (Kuznyechik/MGM/NoPadding) не подходит для больших потоков — он буферизует весь plaintext в памяти до вызова doFinal.
Хэш-функция Стрибог (ГОСТ Р 34.11-2012)
Стрибог — криптографическая хэш-функция без ключа. Два варианта: 256 бит (32 байта) и 512 бит (64 байта).
Библиотека предоставляет два независимых API:
-
org.rssys.gost.api— прямой API, не требует регистрации провайдера. -
org.rssys.gost.jca— JCA/JCE-совместимый API.
Результат big-endian (MSB first), совместим с OpenSSL. RFC 6986 приводит тест-векторы в ГОСТ-нотации (right-to-left) — байты будут в обратном порядке при прямом сравнении.
org.rssys.gost.api
Блочный режим — данные целиком в памяти
import org.rssys.gost.api.Digest;
byte[] data = "данные".getBytes(StandardCharsets.UTF_8);
// Стрибог-256 (ГОСТ Р 34.11-2012), вывод 32 байта
byte[] hash256 = Digest.digest256(data);
// Стрибог-512 (ГОСТ Р 34.11-2012), вывод 64 байта
byte[] hash512 = Digest.digest512(data);
Инкрементальный режим — данные из файла или сокета без накопления в памяти
import org.rssys.gost.api.Digest;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
// Стрибог-256
Digest h256 = new Digest(Digest.Algorithm.STREEBOG_256);
byte[] buf = new byte[8192];
int n;
try (InputStream in = Files.newInputStream(Path.of("data.bin"))) {
while ((n = in.read(buf)) != -1) {
h256.update(buf, 0, n);
}
}
byte[] hash256 = h256.digest();
// Стрибог-512
Digest h512 = new Digest(Digest.Algorithm.STREEBOG_512);
try (InputStream in = Files.newInputStream(Path.of("data.bin"))) {
while ((n = in.read(buf)) != -1) {
h512.update(buf, 0, n);
}
}
byte[] hash512 = h512.digest();
Экземпляр Digest не является потокобезопасным. Создавайте отдельный экземпляр на каждый поток.
JCA (org.rssys.gost.jca)
Блочный режим
import java.security.MessageDigest;
// Стрибог-256
MessageDigest md256 = MessageDigest.getInstance("GOST3411-2012-256", "RssysGostProvider"); //
byte[] hash256 = md256.digest(data);
// Стрибог-512
MessageDigest md512 = MessageDigest.getInstance("GOST3411-2012-512", "RssysGostProvider");
byte[] hash512 = md512.digest(data);
- Алиасы:
"Streebog-256","1.2.643.7.1.1.2.2"для 256-бит;"Streebog-512","1.2.643.7.1.1.2.3"для 512-бит.
Инкрементальный режим — данные из файла или сокета
import java.security.MessageDigest;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
MessageDigest md256 = MessageDigest.getInstance("GOST3411-2012-256", "RssysGostProvider");
byte[] buf = new byte[8192];
int n;
try (InputStream in = Files.newInputStream(Path.of("data.bin"))) {
while ((n = in.read(buf)) != -1) {
md256.update(buf, 0, n);
}
}
byte[] hash256 = md256.digest();
HMAC-Стрибог (RFC 7836)
HMAC — код аутентификации сообщений на основе хэш-функции. Требует симметричный ключ. Два варианта: HMAC-Стрибог-256 (32 байта) и HMAC-Стрибог-512 (64 байта).
org.rssys.gost.api
Блочный режим
import org.rssys.gost.api.Digest;
import org.rssys.gost.api.CmacApi;
import org.rssys.gost.cipher.SymmetricKey;
SymmetricKey key = ...; // выработан ранее
byte[] data = "данные".getBytes(StandardCharsets.UTF_8);
// HMAC-Стрибог-256 (RFC 7836), вывод 32 байта
byte[] mac256 = Digest.hmac256(data, key);
// HMAC-Стрибог-512 (RFC 7836), вывод 64 байта
byte[] mac512 = Digest.hmac512(data, key);
// Проверка целостности — constant-time сравнение (защита от timing-атак)
boolean ok = CmacApi.verifyMac(expected, mac256);
Инкрементальный режим — данные из файла или сокета
import org.rssys.gost.api.Digest;
import org.rssys.gost.api.CmacApi;
import org.rssys.gost.cipher.SymmetricKey;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
SymmetricKey key = ...; // выработан ранее
// HMAC-Стрибог-256
Digest m256 = new Digest(Digest.Algorithm.HMAC_256, key);
byte[] buf = new byte[8192];
int n;
try (InputStream in = Files.newInputStream(Path.of("data.bin"))) {
while ((n = in.read(buf)) != -1) {
m256.update(buf, 0, n);
}
}
byte[] mac256 = m256.digest();
// HMAC-Стрибог-512
Digest m512 = new Digest(Digest.Algorithm.HMAC_512, key);
try (InputStream in = Files.newInputStream(Path.of("data.bin"))) {
while ((n = in.read(buf)) != -1) {
m512.update(buf, 0, n);
}
}
byte[] mac512 = m512.digest();
// Проверка целостности
boolean ok = CmacApi.verifyMac(expected, mac256);
key.destroy();
Экземпляр Digest не является потокобезопасным. Создавайте отдельный экземпляр на каждый поток.
JCA (org.rssys.gost.jca)
Блочный режим
import javax.crypto.Mac;
import org.rssys.gost.jca.key.GostSecretKey;
GostSecretKey key = ...; // выработан ранее
byte[] data = "данные".getBytes(StandardCharsets.UTF_8);
// HMAC-Стрибог-256
Mac mac256 = Mac.getInstance("HmacGOST3411-2012-256", "RssysGostProvider"); //
mac256.init(key);
byte[] result256 = mac256.doFinal(data);
// HMAC-Стрибог-512
Mac mac512 = Mac.getInstance("HmacGOST3411-2012-512", "RssysGostProvider");
mac512.init(key);
byte[] result512 = mac512.doFinal(data);
key.destroy();
- Алиасы:
"HMAC-Streebog-256","1.2.643.7.1.1.4.1"для 256-бит;"HMAC-Streebog-512","1.2.643.7.1.1.4.2"для 512-бит.
Инкрементальный режим — данные из файла или сокета
import javax.crypto.Mac;
import org.rssys.gost.jca.key.GostSecretKey;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
GostSecretKey key = ...; // выработан ранее
Mac mac256 = Mac.getInstance("HmacGOST3411-2012-256", "RssysGostProvider");
mac256.init(key);
byte[] buf = new byte[8192];
int n;
try (InputStream in = Files.newInputStream(Path.of("data.bin"))) {
while ((n = in.read(buf)) != -1) {
mac256.update(buf, 0, n);
}
}
byte[] result256 = mac256.doFinal();
key.destroy();
CMAC-Кузнечик (ГОСТ Р 34.13-2015)
CMAC — имитовставка на основе блочного шифра Кузнечик. Требует симметричный ключ. Полный тег: 16 байт (128 бит = размер блока). Допускается усечение тега.
CMAC строится на блочном шифре (Кузнечик, ГОСТ Р 34.12-2015). HMAC строится на хэш-функции (Стрибог, ГОСТ Р 34.11-2012). Это разные алгоритмы с разной областью применения.
org.rssys.gost.api
Блочный режим
import org.rssys.gost.api.CmacApi;
import org.rssys.gost.cipher.SymmetricKey;
SymmetricKey key = ...; // выработан ранее
byte[] data = "данные".getBytes(StandardCharsets.UTF_8);
// Полный тег CMAC, 16 байт
byte[] tag = CmacApi.cmac(data, key);
// Усечённый тег, 8 байт (первые 8 байт полного тега)
byte[] tag8 = CmacApi.cmac(data, key, 8);
// Проверка целостности — constant-time сравнение (защита от timing-атак)
boolean ok = CmacApi.verifyMac(expected, tag);
key.destroy();
Инкрементальный режим — данные из файла или сокета
import org.rssys.gost.api.CmacApi;
import org.rssys.gost.cipher.SymmetricKey;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
SymmetricKey key = ...; // выработан ранее
CmacApi cmac = new CmacApi(key);
byte[] buf = new byte[8192];
int n;
try (InputStream in = Files.newInputStream(Path.of("data.bin"))) {
while ((n = in.read(buf)) != -1) {
cmac.update(buf, 0, n);
}
}
byte[] tag = cmac.digest(); // 16 байт
// Проверка целостности
boolean ok = CmacApi.verifyMac(expected, tag);
key.destroy();
Экземпляр CmacApi не является потокобезопасным. Создавайте отдельный экземпляр на каждый поток.
JCA (org.rssys.gost.jca)
Блочный режим
import javax.crypto.Mac;
import org.rssys.gost.jca.key.GostSecretKey;
GostSecretKey key = ...; // выработан ранее
byte[] data = "данные".getBytes(StandardCharsets.UTF_8);
Mac mac = Mac.getInstance("CMAC-Kuznyechik", "RssysGostProvider");
mac.init(key);
byte[] tag = mac.doFinal(data);
key.destroy();
Инкрементальный режим — данные из файла или сокета
import javax.crypto.Mac;
import org.rssys.gost.jca.key.GostSecretKey;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
GostSecretKey key = ...; // выработан ранее
Mac mac = Mac.getInstance("CMAC-Kuznyechik", "RssysGostProvider");
mac.init(key);
byte[] buf = new byte[8192];
int n;
try (InputStream in = Files.newInputStream(Path.of("data.bin"))) {
while ((n = in.read(buf)) != -1) {
mac.update(buf, 0, n);
}
}
byte[] tag = mac.doFinal();
key.destroy();
Выбор алгоритма хеширования и кодов аутентификации
| Сценарий | Алгоритм | Доступность в API |
|---|---|---|
|
Контрольная сумма данных без ключа |
|
|
|
Аутентификация данных, ключ на основе хэша |
|
|
|
Аутентификация данных, ключ Кузнечика (ГОСТ Р 34.13-2015) |
|
|
|
Проверка целостности (constant-time) |
|
|
Электронная подпись ГОСТ Р 34.10-2012
Электронная подпись — алгоритм ГОСТ Р 34.10-2012 (RFC 7091).
-
Хэш выбирается автоматически по кривой: Стрибог-256 для 256-битных кривых, Стрибог-512 для 512-битных.
-
Нонс k генерируется детерминированно по RFC 6979 §3.2 (HMAC-Стрибог) — одно сообщение + один ключ всегда дают одну подпись.
-
Формат подписи:
r ∥ sbig-endian, без DER/ASN.1. Длина: 64 байта (256-бит кривые) или 128 байт (512-бит кривые).
Библиотека предоставляет два независимых API:
-
org.rssys.gost.api— прямой API, не требует регистрации провайдера. -
org.rssys.gost.jca— JCA/JCE-совместимый API.
org.rssys.gost.api
Подпись и верификация
import org.rssys.gost.api.KeyGenerator;
import org.rssys.gost.api.KeyPair;
import org.rssys.gost.api.Signature;
import org.rssys.gost.signature.ECParameters;
byte[] data = "сообщение для подписи".getBytes(StandardCharsets.UTF_8);
// Генерация ключевой пары — кривая CryptoPro-A (256 бит)
KeyPair pair = KeyGenerator.generateKeyPair(ECParameters.cryptoProA());
try {
// Подпись: хеш Стрибог-256 вычисляется автоматически базируясь на параметрах кривой
// Формат: r ∥ s big-endian, длина 64 байта
byte[] signature = Signature.sign(data, pair.getPrivate());
// Верификация
boolean valid = Signature.verify(data, signature, pair.getPublic());
// valid => true
// Подмена данных обнаруживается
byte[] tampered = data.clone();
tampered[0] ^= 0x01;
boolean invalid = Signature.verify(tampered, signature, pair.getPublic());
// invalid => false
} finally {
pair.getPrivate().destroy(); // обнуляет d в памяти
}
Импорт закрытого ключа из byte[] и подпись
import org.rssys.gost.api.Signature;
import org.rssys.gost.signature.ECParameters;
import org.rssys.gost.signature.PrivateKeyParameters;
import java.math.BigInteger;
byte[] dBytes = ...; // 32 байта закрытого ключа, big-endian
ECParameters params = ECParameters.cryptoProA();
PrivateKeyParameters priv = new PrivateKeyParameters(new BigInteger(1, dBytes), params);
try {
byte[] signature = Signature.sign(data, priv); // хеш нужной битности вычисляется автоматически
// ...
} finally {
priv.destroy();
java.util.Arrays.fill(dBytes, (byte) 0); // очистить исходный массив
}
Импорт открытого ключа из координат и верификация
import org.rssys.gost.api.Signature;
import org.rssys.gost.signature.ECParameters;
import org.rssys.gost.signature.ECPoint;
import org.rssys.gost.signature.PublicKeyParameters;
import java.math.BigInteger;
byte[] qxBytes = ...; // 32 байта координаты Qx, big-endian
byte[] qyBytes = ...; // 32 байта координаты Qy, big-endian
ECParameters params = ECParameters.cryptoProA();
ECPoint q = ECPoint.affine(new BigInteger(1, qxBytes), new BigInteger(1, qyBytes), params);
PublicKeyParameters pub = new PublicKeyParameters(q, params);
boolean valid = Signature.verify(data, signature, pub);
Получение открытого ключа из закрытого: Q = d·G
import org.rssys.gost.api.Signature;
import org.rssys.gost.signature.PrivateKeyParameters;
import org.rssys.gost.signature.PublicKeyParameters;
PrivateKeyParameters priv = ...; // загружен ранее
try {
// Вычисляет Q = d·G — скалярное умножение базовой точки
PublicKeyParameters pub = Signature.derivePublicKey(priv);
// Полученный ключ можно использовать для верификации
boolean valid = Signature.verify(data, signature, pub);
} finally {
priv.destroy();
}
JCA (org.rssys.gost.jca)
Генерация ключевой пары, подпись и верификация
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.spec.ECGenParameterSpec;
byte[] data = "сообщение для подписи".getBytes(StandardCharsets.UTF_8);
// Генерация ключевой пары — кривая tc26-gost-A-256 (256 бит)
KeyPairGenerator kpg = KeyPairGenerator.getInstance("ECGOST3410-2012", "RssysGostProvider");
kpg.initialize(new ECGenParameterSpec("tc26-gost-A-256")); //
KeyPair pair = kpg.generateKeyPair();
try {
// Подпись — хеш Стрибог-256 вычисляется автоматически базируясь на параметрах кривой
Signature signer = Signature.getInstance("ECGOST3410-2012-256", "RssysGostProvider"); //
signer.initSign(pair.getPrivate());
signer.update(data);
byte[] signature = signer.sign(); // 64 байта
// Верификация
Signature verifier = Signature.getInstance("ECGOST3410-2012-256", "RssysGostProvider");
verifier.initVerify(pair.getPublic());
verifier.update(data);
boolean valid = verifier.verify(signature);
// valid => true
} finally {
pair.getPrivate().destroy();
}
-
Поддерживаемые имена кривых:
"cryptopro-A","cryptopro-B","cryptopro-C","tc26-gost-A-256"(256 бит);"tc26-gost-A-512","tc26-gost-B-512","tc26-gost-C-512"(512 бит).
Инициализация по размеру:kpg.initialize(256)→cryptopro-A,kpg.initialize(512)→tc26-gost-A-512. -
Для 512-битных кривых используйте
"ECGOST3410-2012-512".
Алиасы: OID "1.2.643.7.1.1.3.2" (256 бит), "1.2.643.7.1.1.3.3" (512 бит).
Импорт закрытого ключа из PKCS#8 DER и подпись
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
byte[] pkcs8Der = ...; // DER-кодирование в формате PKCS#8 PrivateKeyInfo
KeyFactory kf = KeyFactory.getInstance("ECGOST3410-2012", "RssysGostProvider");
PrivateKey priv = kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8Der));
try {
Signature signer = Signature.getInstance("ECGOST3410-2012-256", "RssysGostProvider");
signer.initSign(priv);
signer.update(data);
byte[] signature = signer.sign();
// ...
} finally {
priv.destroy();
}
Импорт открытого ключа из X.509 DER и верификация
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
byte[] x509Der = ...; // DER-кодирование в формате X.509 SubjectPublicKeyInfo
KeyFactory kf = KeyFactory.getInstance("ECGOST3410-2012", "RssysGostProvider");
PublicKey pub = kf.generatePublic(new X509EncodedKeySpec(x509Der));
Signature verifier = Signature.getInstance("ECGOST3410-2012-256", "RssysGostProvider");
verifier.initVerify(pub);
verifier.update(data);
boolean valid = verifier.verify(signature);
Экспорт ключей в DER для хранения или передачи
import org.rssys.gost.jca.key.GostECPrivateKey;
import org.rssys.gost.jca.key.GostECPublicKey;
// Экспорт открытого ключа — X.509 SubjectPublicKeyInfo DER
byte[] pubDer = pair.getPublic().getEncoded(); //
// Экспорт закрытого ключа — PKCS#8 PrivateKeyInfo DER
// ВНИМАНИЕ: хранить только в зашифрованном виде
byte[] privDer = pair.getPrivate().getEncoded(); //
try {
// сохранить privDer ...
} finally {
java.util.Arrays.fill(privDer, (byte) 0); // очистить после использования
pair.getPrivate().destroy();
}
getEncoded()возвращаетnullесли ключ уничтожен (destroy()был вызван).
Поддерживаемые кривые
| Имя кривой | Стандарт | Бит | Хэш подписи |
|---|---|---|---|
|
|
RFC 4357 §11.2 |
256 |
Стрибог-256 |
|
|
RFC 4357 §11.3 |
256 |
Стрибог-256 |
|
|
RFC 4357 §11.4 |
256 |
Стрибог-256 |
|
|
RFC 7836, Appendix A.2 |
256 |
Стрибог-256 |
|
|
RFC 7836, Appendix A.1 |
512 |
Стрибог-512 |
|
|
RFC 7836 |
512 |
Стрибог-512 |
|
|
RFC 7836 |
512 |
Стрибог-512 |
Что даёт RFC 6979
Детерминированный k через HMAC-DRBG (HMAC-Стрибог) решает проблему плохого ГСЧ радикально: k = HMAC-DRBG(закрытый ключ, хеш сообщения).
Свойства:
-
Два разных сообщения → два разных k (хеши разные → DRBG даёт разный вывод).
-
Одно сообщение подписывается всегда одним k → никакой вариативности нет.
-
kне предсказуем без знания закрытого ключа (HMAC — псевдослучайная функция). -
Не зависит от качества системного ГСЧ.
Замечания по безопасности API
-
Очистка ключей —
SymmetricKey,PrivateKeyParameters,ECDSASignerреализуютjavax.security.auth.Destroyable. Вызывайтеdestroy()в блокеfinally. -
Сравнение MAC — используйте
verifyMac()вместоArrays.equals().verifyMacиспользуетMessageDigest.isEqual()— constant-time. -
IV — каждый вызов
encrypt()без явного IV генерирует новый случайный IV. Повторное использование IV с одним ключом в CTR/OFB нарушает конфиденциальность. -
Thread-safety — статические методы всех классов API потокобезопасны.
Замечания по безопасности JCA
-
Закрытые ключи —
PrivateKeyParametersреализуетDestroyable. После использования вызывайте((Destroyable) privateKey).destroy(). -
ICN в MGM — повтор ICN с тем же ключом полностью компрометирует защиту. Используйте
ENCRYPT_MODEбез явного ICN: он генерируется черезSecureRandomавтоматически. -
AAD —
updateAADдолжен вызываться доupdate/doFinal. Порядок важен. -
AEADBadTagException — не кэшируйте данные из потока расшифрования до получения этого исключения или успешного завершения.
Раздел для разработчиков
Для доработки библиотеки необходимо установить OpenJDK 11+, maven.
-
mvn test— запуск тестов. -
mvn package— получение дистрибутива библиотеки. -
mvn install— установка дистрибутива библиотеки в локальный .m2.
В папке bench созданы примеры кросс-верификации crypto-gost, BouncyCastle 1.83 и OpenSSL.
Методики бенчмарков
Все тесты производились на ОС AltLinux p11 x86_64 с установленным OpenSSL+ГОСТ.
Бенчмарк электронной подписи ГОСТ Р 34.10-2012
Расположение: папка bench/signature.
Что измеряется
Пропускная способность (ops/s) операций подписания и верификации по ГОСТ Р 34.10-2012 на двух группах кривых:
-
256-бит — кривая CryptoPro-A (RFC 4357)
-
512-бит — кривая TC26-A-512 (RFC 7836)
Что сравнивается
crypto-gost (низкоуровневый API: ECDSASigner + Streebog256/512) против BouncyCastle (ECGOST3410_2012Signer). Обе библиотеки используют одну и ту же ключевую пару.
crypto-gost использует детерминированный нонс k (RFC 6979 + HMAC-Стрибог), BouncyCastle — случайный SecureRandom. Это влияет на скорость подписания, но не верификации.
Что не входит в измерение
Объекты подписи (ECDSASigner, ECGOST3410_2012Signer) создаются в @Setup и переиспользуются через init().
Измеряется только hash-then-sign / hash-then-verify без накладных расходов на аллокации объектов.
Параметры JMH
-
5 итераций прогрева × 2 с.
-
5 итераций замера × 2 с.
-
3 форка JVM.
Итого ~60 с на один режим (256/512) × операцию (sign/verify). Три форка устраняют JIT-зависимости между запусками.
Как воспроизвести
make build # сборка uber-jar
make bench-256 plot # 256-бит бенчмарк + график
make bench-512 plot-512 # 512-бит бенчмарк + график
make bench-all # оба бенчмарка
Как читать графики
Гистограммы показывают среднее значение ops/s, планки погрешностей — 99.9% доверительный интервал JMH.
Если планки двух столбцов перекрываются — разница статистически незначима.
Кузнечик
Расположение: Каталог bench/kuznyechik
Запуск: make bench
Что измеряется: Пропускная способность симметричного шифрования Кузнечик (ГОСТ Р 34.12-2015) в режимах CTR и CFB при разных размерах буфера: 1 КБ, 10 КБ, 100 КБ, 1 МБ. Результат — МБ/с обработанных данных.
Что сравнивается: crypto-gost (низкоуровневый API: Ctr/Cfb + processBytes) против BouncyCastle (SICBlockCipher/CFBBlockCipher + processBytes). Обе библиотеки используют один и тот же ключ и IV, созданные через KeyGenerator.generateSymmetricKey().
Что не входит в измерение: Создание объектов шифра и ключевое расписание вынесены в @Setup — они выполняются один раз до замера. Бенчмарк измеряет только время обработки данных, без накладных расходов на аллокации.
Как обеспечивается честность:
-
IV инкрементируется на каждой итерации — JIT не может кешировать одинаковый результат.
-
Входные данные читаются из бинарного файла со случайным содержимым (генерируется dd if=/dev/urandom).
-
Оба соперника переинициализируют шифр перед каждым вызовом одинаково.
Параметры JMH:
5 итераций прогрева × 2 с + 5 итераций замера × 2 с + 3 форка JVM. Итого ~60 секунд на один режим. Три форка устраняют JIT-зависимости между запусками, 5 итераций дают надёжный доверительный интервал (99.9% CI выводится автоматически JMH).
Как воспроизвести: make data # генерация тестовых файлов make build # сборка uber-jar make bench # запуск обоих бенчмарков make plot # построение графиков (требует gnuplot)
Как читать графики: Гистограммы показывают среднее значение MB/s, планки погрешностей — 99.9% доверительный интервал JMH. Если планки двух столбцов перекрываются — разница статистически незначима.
Методики кросс-валидации
Каталог bench/signature.
Кросс-валидация электронной подписи ГОСТ Р 34.10-2012 с BouncyCastle
Цель
Подтвердить совместимость реализации crypto-gost с BouncyCastle 1.83: подписи, выработанные одной библиотекой, должны успешно верифицироваться другой.
Структура
Каждый тест двунаправленный:
-
crypto-gost → bc— crypto-gost подписывает, BC верифицирует. -
bc → crypto-gost— BC подписывает, crypto-gost верифицирует.
Покрытие: все 7 стандартных кривых ГОСТ Р 34.10-2012:
|
Кривая |
Размер |
Стандарт |
|
TC26-A-256 |
256-бит |
RFC 7836 |
|
CryptoPro-A |
256-бит |
RFC 4357 |
|
CryptoPro-B |
256-бит |
RFC 4357 |
|
CryptoPro-C |
256-бит |
RFC 4357 |
|
TC26-A-512 |
512-бит |
RFC 7836 |
|
TC26-B-512 |
512-бит |
RFC 7836 |
|
TC26-C-512 |
512-бит |
RFC 7836 |
По 100 случайных сообщений на кривую (итого 1400 проверок).
Как воспроизвести
make cross-validate-256 # все 256-бит кривые
make cross-validate-512 # все 512-бит кривые
make cross-validate-all # все 7 кривых
Ожидаемый результат
All 1400 cross-validation checks PASSED. ✓
Кросс-валидация электронной подписи ГОСТ Р 34.10-2012 с OpenSSL (ГОСТ)
Цель
Подтвердить совместимость реализации crypto-gost с OpenSSL 3.3.х (ГОСТ): подписи, выработанные библиотекой, должны успешно верифицироваться независимой реализацией OpenSSL..
Структура
Направление: crypto-gost подписывает → OpenSSL верифицирует.
Покрытие: 3 стандартных кривых ГОСТ Р 34.10-2012:
|
Кривая |
Размер |
Стандарт |
|
CryptoPro-A |
256-бит |
RFC 4357 |
|
CryptoPro-B |
256-бит |
RFC 4357 |
|
CryptoPro-C |
256-бит |
RFC 4357 |
Ограничение: TC26-A-256 и 512-бит кривые не поддерживаются engine gost на данной системе.
Алгоритм одного теста:
-
Генерируется случайный закрытый ключ (openssl rand -hex 32) и случайное сообщение (1 КБ из /dev/urandom).
-
SignatureTool (crypto-gost) вычисляет публичный ключ Q=d·G и экспортирует его в DER SubjectPublicKeyInfo.
-
SignatureTool подписывает сообщение — формат r||s (big-endian).
-
Подпись конвертируется в формат OpenSSL engine gost: s||r.
-
OpenSSL dgst -streebog256 -engine gost -verify верифицирует.
-
Tamper-тест: XOR первого байта подписи — OpenSSL должен отклонить.
Формат подписи (важно для совместимости): crypto-gost : r_BE || s_BE OpenSSL gost: s_BE || r_BE
Скрипт выполняет перестановку автоматически.
Как воспроизвести
make cross-validate-openssl # 3 кривых 256 бит, требует OpenSSL engine gost
Ожидаемый результат
Сводка:
Всего проверок: 60
Пройдено: 60
Ошибок: 0
Статус: УСПЕХ
Кузнечик
Расположение: Каталог bench/kuznyechik
Запуск:
-
make cross-validate# crypto-gost ↔ BouncyCastle. -
make cross-validate-openssl# crypto-gost ↔ OpenSSL (требует OpenSSL 3.x с ГОСТ).
Цель: Подтвердить совместимость реализации Кузнечика в crypto-gost с независимыми реализациями (BouncyCastle 1.83 и OpenSSL 3.x), а также дополнительно подтвердить корректность алгоритма через три независимых источника.
Структура: Две независимые кросс-валидации:
-
crypto-gost ↔ BouncyCastle 1.83 (KuznyechikCrossValidation.java)
Каждый тест двунаправленный:
-
gost→bc: crypto-gost шифрует → BC расшифровывает → сравниваем с исходником.
-
bc→gost: BC шифрует → crypto-gost расшифровывает → сравниваем с исходником.
Режимы: CTR, CBC/PKCS7, CBC/NoPad, CFB, OFB, MGM (только roundtrip, BC не поддерживает MGM).
Размеры данных: 0, 1, 16, 255, 10000 байт — покрывают пустые данные, 1 байт, ровно один блок, некратный блоку, большой буфер.
Дополнительно: тест со случайными данными и случайными IV через SecureRandom для CTR и CFB — исключает зависимость от предсказуемых паттернов.
-
-
crypto-gost ↔ OpenSSL 3.x (cross-validate-openssl.sh)
Использует KuznyechikTool — CLI-обёртку над crypto-gost API — как мост между Java и OpenSSL.
Каждый тест двунаправленный:
-
openssl → crypto-gost: OpenSSL шифрует файл → KuznyechikTool расшифровывает → сравнение.
-
crypto-gost → openssl: KuznyechikTool шифрует файл → OpenSSL расшифровывает → сравнение.
Ключ и IV генерируются случайно через openssl rand на каждый тест.
Режимы: CTR, CBC/PKCS7, CBC/NoPad, CFB, OFB.
Совместимость CTR IV: ГОСТ Р 34.13-2015 определяет CTR IV как 8 байт, счётчик = IV || 0x00*8. OpenSSL kuznyechik-ctr принимает 16-байтный IV и интерпретирует его как начальное значение счётчика. Совместимость обеспечена: crypto-gost использует IV8 || 0x00*8, OpenSSL получает тот же 16-байтный вектор — результаты шифрования совпадают.
-
Методика кросс-валидации: хэш-функции и кода аутентификации (имитовставки)
Кросс-валидация подтверждает корректность реализации путём сравнения результатов crypto-gost с двумя независимыми реализациями: BouncyCastle 1.83 и OpenSSL 3.3.x с ГОСТ-провайдером.
Покрытие
| Алгоритм | BouncyCastle 1.83 | OpenSSL 3.x |
|---|---|---|
|
Streebog-256 (ГОСТ Р 34.11-2012) |
18 проверок |
5 проверок |
|
Streebog-512 (ГОСТ Р 34.11-2012) |
18 проверок |
5 проверок |
|
HMAC-Streebog-256 (RFC 7836) |
18 проверок |
5 проверок |
|
HMAC-Streebog-512 (RFC 7836) |
18 проверок |
5 проверок |
|
CMAC-Кузнечик (ГОСТ Р 34.13-2015) |
18 проверок |
5 проверок |
|
Итого |
90 проверок |
25 проверок |
Кросс-валидация с BouncyCastle
Для каждого алгоритма и каждого размера сообщения {0, 1, 8, 16, 32, 64, 256, 1024, 65535} байт проверяются два направления:
-
crypto-gost → BC— crypto-gost вычисляет тег/хэш, BouncyCastle проверяет совпадение. -
BC → crypto-gost— BouncyCastle вычисляет тег/хэш, crypto-gost проверяет совпадение.
Используется высокоуровневый API: api/Digest и api/CmacApi.
Кросс-валидация с OpenSSL
Для каждого алгоритма и каждого размера сообщения {0, 1, 16, 255, 1024} байт с уникальным случайным ключом (32 байта, openssl rand -hex 32) проверяется направление:
-
crypto-gost → openssl— crypto-gost вычисляет результат черезMacTool, OpenSSL вычисляет тот же результат независимо, значения сравниваются побайтово.Команды OpenSSL:
# Streebog-256 / Streebog-512
openssl dgst -streebog256 -r <file>
openssl dgst -streebog512 -r <file>
# HMAC-Streebog-256 / HMAC-Streebog-512
openssl mac -digest streebog256 -macopt hexkey:<KEY> HMAC < <file>
openssl mac -digest streebog512 -macopt hexkey:<KEY> HMAC < <file>
# CMAC-Kuznyechik
openssl mac -cipher kuznyechik-cbc -macopt hexkey:<KEY> CMAC < <file>
Требования
-
Java 21+
-
BouncyCastle 1.83 (
bcprov-jdk18on) -
OpenSSL 3.x с GOST-провайдером (поддержка
streebog256,kuznyechik-cbc)
Как воспроизвести
# Сборка
cd bench/mac
make build
# Кросс-валидация с BouncyCastle (лог: results/cross-validate-bc.log)
make cross-validate
# Кросс-валидация с OpenSSL (лог: results/cross-validate-openssl.log)
make cross-validate-openssl
Ожидаемый результат
Все 115 проверок прошли успешно на следующем окружении:
Лицензия
Автор: Михаил Ананьев.
Данный проект распространяется под Открытой лицензией на программное обеспечение “Рэд старс системс”, версия 1.0.
Текст лицензии находится в файле LICENSE или по ссылке.