DSA: классическая подпись

DSA (Digital Signature Algorithm) представляет собой алгоритм электронной подписи, основанный на дискретном логарифмировании в конечных полях. Его ключевая идея заключается в том, что вычисление подписи легко выполняется при наличии закрытого ключа, но практически невозможно восстановить закрытый ключ или подделать подпись без него.

DSA опирается на параметры домена:

  • простое число p
  • простое число q, являющееся делителем p − 1
  • генератор g подгруппы порядка q
  • закрытый ключ x ∈ [1, q − 1]
  • открытый ключ y = g^x mod p

Процесс формирования подписи состоит из двух вычислений:

  • выбор случайного k ∈ [1, q − 1]
  • вычисление r = (g^k mod p) mod q
  • вычисление s = k⁻¹ (H(m) + x·r) mod q

Пара (r, s) и есть цифровая подпись сообщения m.

Проверка выполняется через:

  • w = s⁻¹ mod q
  • u₁ = H(m) · w mod q
  • u₂ = r · w mod q
  • v = ((g^u₁ · y^u₂) mod p) mod q

Подпись считается корректной, если v = r.


Представление ключей в Jsrsasign

Библиотека Jsrsasign работает с криптографическими объектами через ASN.1 и PEM-кодирование. DSA-ключи обычно представлены в формате:

  • PEM (Base64 с заголовками)
  • PKCS#8 для приватных ключей
  • X.509 SubjectPublicKeyInfo для публичных ключей

Внутри Jsrsasign используется пространство имён KEYUTIL и KJUR.crypto.


Генерация DSA ключей

Jsrsasign позволяет создавать ключевые пары с помощью KEYUTIL.generateKeypair.

Пример генерации:

const kp = KEYUTIL.generateKeypair("DSA", 1024);

const prv = kp.prvKeyObj;
const pub = kp.pubKeyObj;

console.log(KEYUTIL.getPEM(prv, "PKCS8PRV"));
console.log(KEYUTIL.getPEM(pub));

Параметр 1024 указывает длину ключа (на практике чаще используют 2048 или выше, хотя DSA исторически ограничен стандартами q = 160/224/256 бит).


Формирование подписи в Jsrsasign

Основной интерфейс для подписи — KJUR.crypto.Signature.

Для DSA используется алгоритм вида SHA256withDSA или SHA1withDSA (устаревший вариант).

const sig = new KJUR.crypto.Signature({ "alg": "SHA256withDSA" });

sig.init(prv);
sig.updateString("Сообщение для подписи");

const signature = sig.sign();
console.log(signature);

Подпись возвращается в формате Base64 или HEX в зависимости от настроек.


Проверка подписи

Для верификации используется тот же класс Signature, но инициализируется публичным ключом.

const sig = new KJUR.crypto.Signature({ "alg": "SHA256withDSA" });

sig.init(pub);
sig.updateString("Сообщение для подписи");

const isValid = sig.verify(signature);
console.log(isValid);

Если сообщение или подпись изменены, результат будет false.


Работа с хешированием

DSA не подписывает данные напрямую. Всегда используется хеш-функция.

Jsrsasign автоматически применяет выбранный алгоритм:

  • SHA-1 (устарел)
  • SHA-256 (рекомендуется)
  • SHA-384 / SHA-512 (зависит от параметров реализации)

Важно учитывать соответствие между длиной q и хеш-функцией:

  • q = 160 бит → SHA-1
  • q = 224 бит → SHA-224
  • q = 256 бит → SHA-256

Несоответствие может привести к ошибкам или снижению криптографической стойкости.


Разбор внутреннего формата подписи

В Jsrsasign подпись DSA кодируется как ASN.1 структура:

DSA-Signature ::= SEQUENCE {
    r INTEGER,
    s INTEGER
}

При экспорте в Base64 получается DER-представление. Это важно при взаимодействии с внешними системами (OpenSSL, Java KeyStore).


Проверка совместимости с OpenSSL

Подписи, созданные Jsrsasign, обычно совместимы с OpenSSL, если:

  • используется одинаковый hash algorithm
  • совпадают параметры p, q, g
  • корректно обработан DER формат

Пример экспорта ключа:

const pem = KEYUTIL.getPEM(pub);

Этот PEM можно импортировать в OpenSSL без дополнительных преобразований.


Типичные ошибки при работе с DSA

1. Использование SHA-1

SHA-1 считается криптографически слабым. Многие реализации уже запрещают его использование.

2. Неправильный размер ключа

DSA требует строгого соответствия между p, q и выбранным алгоритмом хеширования.

3. Повторное использование nonce k

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

Это критическая ошибка реализации алгоритма.

4. Неверная кодировка подписи

Base64 и HEX часто путаются при передаче между системами.


Подпись бинарных данных

Jsrsasign позволяет подписывать не только строки, но и бинарные данные через updateHex:

sig.updateHex("deadbeefcafebabe");
const signature = sig.sign();

Это важно при работе с протоколами, где данные представлены в байтовом виде.


Проверка больших сообщений

Для длинных сообщений применяется потоковое обновление:

sig.init(prv);
sig.updateString("часть 1");
sig.updateString("часть 2");
sig.updateString("часть 3");
const signature = sig.sign();

Хеширование выполняется инкрементально, без загрузки всего сообщения в память сразу.


Взаимодействие с JWK

Jsrsasign поддерживает JSON Web Key формат:

const keyObj = KEYUTIL.getKey(jwk);

DSA может быть представлен в JWK как:

{
  "kty": "DSA",
  "p": "...",
  "q": "...",
  "g": "...",
  "y": "..."
}

Однако поддержка DSA в JWK ограничена в современных спецификациях JWT.


Криптографические ограничения DSA

DSA обладает рядом особенностей:

  • фиксированная структура подписи (r, s)
  • зависимость от случайного числа k
  • невозможность детерминированной подписи (в классическом варианте)
  • ограниченная гибкость по сравнению с ECDSA

Jsrsasign реализует стандартный DSA без модификаций, что обеспечивает совместимость с классическими системами, но не включает современных расширений вроде deterministic nonce (RFC 6979 применяется чаще к ECDSA).


Использование в реальных системах

DSA в Jsrsasign чаще встречается в:

  • легаси-системах PKI
  • старых сертификатах X.509
  • интеграции с Java-экосистемой
  • учебных криптографических примерах

Современные системы чаще переходят на RSA-PSS или ECDSA, однако Jsrsasign сохраняет DSA для обратной совместимости и анализа существующих данных.


Обработка ошибок проверки

При верификации Jsrsasign не выбрасывает исключение при неверной подписи. Вместо этого возвращается логическое значение:

  • true — подпись корректна
  • false — подпись не совпадает

Это позволяет использовать проверку в цепочках условной логики без try/catch.