Классическая схема ECDSA критически зависит от качества случайного
числа k, используемого при формировании подписи. Если
k оказывается предсказуемым или повторяется, приватный ключ
восстанавливается через алгебраическое решение системы уравнений
эллиптической кривой. Именно эта проблема стала причиной ряда реальных
компрометаций криптографических систем.
RFC 6979 предлагает замену генерации случайного k на
детерминированный процесс, основанный на HMAC и хэшировании сообщения. В
результате значение k становится функцией приватного ключа
и хэша сообщения, исключая зависимость от внешнего источника
случайности.
В стандартном ECDSA подпись строится по формуле:
kR = k * Gr = x(R) mod ns = 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 заменяет случайное k на детерминированное
значение:
k = HMAC-DRBG(privkey, hash(message))
Свойства подхода:
В 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.
SJCL не предоставляет встроенной поддержки RFC 6979, поэтому детерминированный nonce требуется реализовать вручную на уровне алгоритма.
Алгоритм использует:
dh1Инициализация:
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.
SJCL предоставляет все необходимые примитивы:
sjcl.hash.sha256sjcl.misc.hmacsjcl.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();
}
В SJCL внутренняя реализация ECDSA не предусматривает публичного API
для замены k, поэтому используется подход:
kECDSA подпись строится через операции над кривой:
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 использует собственную реализацию больших чисел:
add, mul, modinverseModВажно учитывать:
nsjcl.bn.fromBitsПроверка остаётся стандартной:
function verify(pub, hash, sig) {
return pub.verify(hash, sig);
}
Поскольку RFC 6979 влияет только на k, проверка не
требует изменений.
Использование RFC 6979 устраняет:
sjcl.randomДополнительные свойства:
При использовании SJCL в реальных системах детерминированный ECDSA обычно интегрируется следующим образом:
Типичная структура:
crypto/
ecdsa.js // RFC6979 signing
keys.js // key management
hash.js // SHA256 wrappers
Комбинация двух источников k разрушает детерминизм и
может вернуть уязвимости.
ECDSA требует приведения хэша к длине порядка кривой.
Все операции должны выполняться строго по модулю n,
иначе подпись становится некорректной.
RFC 6979 требует строгой последовательности обновления состояния DRBG.
Исторические инциденты (включая утечки ключей в криптокошельках и Android-реализациях Bitcoin) демонстрировали:
RFC 6979 был принят как стандартная защита именно против таких сценариев.
При построении системы на SJCL рекомендуется:
sjcl.random в
ECDSAТестовый вектор должен подтверждать:
sign(m, d) == sign(m, d)
для любых повторных запусков при одинаковых входных данных.