Использование детерминированного ECDSA (RFC 6979)

Классическая схема ECDSA критически зависит от качества случайного числа k, используемого при формировании подписи. Если k оказывается предсказуемым или повторяется, приватный ключ восстанавливается через алгебраическое решение системы уравнений эллиптической кривой. Именно эта проблема стала причиной ряда реальных компрометаций криптографических систем.

RFC 6979 предлагает замену генерации случайного k на детерминированный процесс, основанный на HMAC и хэшировании сообщения. В результате значение k становится функцией приватного ключа и хэша сообщения, исключая зависимость от внешнего источника случайности.


Проблема случайного nonce в ECDSA

В стандартном ECDSA подпись строится по формуле:

  • выбирается случайное число k
  • вычисляется точка R = k * G
  • r = x(R) mod n
  • s = k⁻¹ (H(m) + r * d) mod n

где:

  • d — приватный ключ
  • H(m) — хэш сообщения
  • G — генератор группы

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

  • s1 = k⁻¹ (h1 + r * d)
  • s2 = k⁻¹ (h2 + r * d)

что позволяет выразить k, а затем и d.


Идея RFC 6979

RFC 6979 заменяет случайное k на детерминированное значение:

k = HMAC-DRBG(privkey, hash(message))

Свойства подхода:

  • одинаковое сообщение → одинаковая подпись
  • отсутствие зависимости от RNG
  • устойчивость к атакам на генератор случайных чисел

SJCL и базовая структура ECDSA

В Stanford JS Crypto Library (SJCL) ECDSA реализован через:

sjcl.ecc.ecdsa

Пример генерации ключей:

const keys = sjcl.ecc.ecdsa.generateKeys(256);

const pub = keys.pub;
const priv = keys.sec;

Подпись:

const hash = sjcl.hash.sha256.hash("message");

const sig = priv.sign(hash);
const ok = pub.verify(hash, sig);

Проблема заключается в том, что внутренний k генерируется через sjcl.random, что не соответствует RFC 6979.


Реализация детерминированного k

SJCL не предоставляет встроенной поддержки RFC 6979, поэтому детерминированный nonce требуется реализовать вручную на уровне алгоритма.

Базовая структура RFC 6979

Алгоритм использует:

  • HMAC-SHA256
  • приватный ключ d
  • хэш сообщения h1

Инициализация:

V = 0x01 0x01 0x01 ...
K = 0x00 0x00 0x00 ...

Далее выполняются обновления:

K = HMAC(K, V || 0x00 || d || h1)
V = HMAC(K, V)

K = HMAC(K, V || 0x01 || d || h1)
V = HMAC(K, V)

После чего генерируется k из V.


Реализация HMAC-DRBG в SJCL

SJCL предоставляет все необходимые примитивы:

  • sjcl.hash.sha256
  • sjcl.misc.hmac
  • sjcl.bn для операций с большими числами

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

function rfc6979_k(privKeyBytes, hashBytes) {
    const HMAC = sjcl.misc.hmac;

    let V = sjcl.codec.hex.toBits("01".repeat(32));
    let K = sjcl.codec.hex.toBits("00".repeat(32));

    const hmac = new HMAC(K, sjcl.hash.sha256);

    const d = privKeyBytes;
    const h1 = hashBytes;

    function updateK(data) {
        return new HMAC(K, sjcl.hash.sha256).encrypt(data);
    }

    K = updateK(sjcl.bitArray.concat(
        sjcl.bitArray.concat(V, sjcl.codec.hex.toBits("00")),
        sjcl.bitArray.concat(d, h1)
    ));

    V = updateK(V);

    K = updateK(sjcl.bitArray.concat(
        sjcl.bitArray.concat(V, sjcl.codec.hex.toBits("01")),
        sjcl.bitArray.concat(d, h1)
    ));

    V = updateK(V);

    function generate() {
        let T = sjcl.bitArray.clamp(V, 256);
        return T;
    }

    return generate();
}

Подмена генерации nonce в ECDSA SJCL

В SJCL внутренняя реализация ECDSA не предусматривает публичного API для замены k, поэтому используется подход:

  • ручное вычисление k
  • ручное вычисление подписи

Формирование подписи вручную

ECDSA подпись строится через операции над кривой:

function signDeterministic(priv, hash) {
    const curve = sjcl.ecc.curves.k256;
    const G = curve.G;
    const n = curve.r;

    const d = priv.get(); // приватный ключ как bn

    const kBits = rfc6979_k(priv.get(), hash);
    const k = sjcl.bn.fromBits(kBits).mod(n);

    const R = G.mult(k);
    const r = R.x.mod(n);

    const e = sjcl.bn.fromBits(hash);

    const kinv = k.inverseMod(n);

    const s = kinv.mul(e.add(r.mul(d))).mod(n);

    return { r, s };
}

Особенности работы с sjcl.bn

SJCL использует собственную реализацию больших чисел:

  • add, mul, mod
  • inverseMod

Важно учитывать:

  • операции всегда в модуле порядка кривой n
  • hash необходимо приводить к bn через sjcl.bn.fromBits

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

Проверка остаётся стандартной:

function verify(pub, hash, sig) {
    return pub.verify(hash, sig);
}

Поскольку RFC 6979 влияет только на k, проверка не требует изменений.


Безопасные свойства детерминированного ECDSA

Использование RFC 6979 устраняет:

  • зависимость от sjcl.random
  • риск повторного nonce
  • атаки на слабые RNG в браузере или Node.js

Дополнительные свойства:

  • воспроизводимость подписи для тестирования
  • одинаковые входы всегда дают одинаковый результат
  • упрощение аудита криптографического кода

Интеграция в архитектуру приложений

При использовании SJCL в реальных системах детерминированный ECDSA обычно интегрируется следующим образом:

  • слой криптографии отделяется от API
  • приватный ключ не покидает изолированную функцию подписи
  • nonce полностью исключается из внешних источников

Типичная структура:

crypto/
  ecdsa.js        // RFC6979 signing
  keys.js         // key management
  hash.js         // SHA256 wrappers

Частые ошибки при реализации

Использование случайного k вместе с RFC 6979

Комбинация двух источников k разрушает детерминизм и может вернуть уязвимости.

Неправильная нормализация hash

ECDSA требует приведения хэша к длине порядка кривой.

Ошибки в mod n

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

Повторное использование V/K без пересчёта

RFC 6979 требует строгой последовательности обновления состояния DRBG.


Связь с практическими атаками

Исторические инциденты (включая утечки ключей в криптокошельках и Android-реализациях Bitcoin) демонстрировали:

  • повтор nonce при генерации через плохой RNG
  • восстановление приватного ключа через линейную алгебру

RFC 6979 был принят как стандартная защита именно против таких сценариев.


Использование SJCL в продакшене с RFC 6979

При построении системы на SJCL рекомендуется:

  • полностью отключить зависимость от sjcl.random в ECDSA
  • использовать только SHA-256 как хэш-функцию
  • фиксировать версию кривой (secp256k1 / k256)
  • тестировать детерминизм подписи на известных векторах RFC 6979

Тестовый вектор должен подтверждать:

sign(m, d) == sign(m, d)

для любых повторных запусков при одинаковых входных данных.