Коротка відповідь. Небезпека чисто-JS криптографії — не в математиці алгоритмів, а в реалізації. Час виконання JavaScript залежить від даних (JIT, збирач сміття, оптимізації рушія), і ця залежність витікає назовні як timing side-channel. У схемах на еліптичних кривих — як-от українська ДСТУ 4145 — найкритичніше місце ще підступніше: одноразове число k. Його повторення чи передбачуваність дозволяє арифметично відновити приватний ключ із самих підписів, без жодного доступу до пристрою. Тому DSTUcrypt не переписує криптографію на JS: уся криптоарифметика виконується перевіреним нативним криптоядром, скомпільованим у WebAssembly, з константним часом, коректним k і ентропією з CSPRNG браузера.
Криптографія — не «просто математика в коді»
Поширена помилка розробника: «алгоритм опубліковано в стандарті, отже, будь-яка реалізація, що проходить тестові вектори, — безпечна». Насправді тестові вектори перевіряють лише коректність — що на вхід X виходить Y. Безпека — це інша властивість: реалізація не повинна витікати секретами через побічні канали, повторювати випадкові значення, покладатися на слабку ентропію.
Сучасні атаки на підпис майже ніколи не «ламають математику». ДСТУ 4145 на еліптичних кривих і геш-функції на кшталт «Купини» (ДСТУ 7564:2014) стійкі на папері. Атакують реалізацію: як саме код виконує скалярне множення, звідки бере випадковість, скільки часу займає операція залежно від бітів ключа. І саме тут чисто-JS реалізації — переписані з нуля ентузіастами, без років промислової експлуатації — програють нативним бібліотекам у найважливішому.
Timing side-channel: коли час виконання видає біти ключа
Timing-атака — це відновлення секрету за часом виконання операцій. Якщо обчислення з бітом ключа «1» триває хоч трохи довше, ніж із бітом «0», достатньо статистики за багатьма операціями, щоб відновити ключ біт за бітом. Захист відомий: константний час — код виконує однакову послідовність інструкцій незалежно від значень секрету.
У нативному C/C++ константний час — складна, але розв'язна інженерна задача: розробник контролює інструкції, які виконає процесор. У JavaScript це практично недосяжно, бо між вашим кодом і процесором стоїть рушій, який активно переписує програму на льоту:
- JIT-компілятор оптимізує «гарячі» шляхи коду залежно від даних, які вже бачив: та сама функція в різні моменти виконується різним машинним кодом із різним таймінгом.
- Деоптимізації та збирач сміття вносять паузи, що корелюють з обсягом і формою даних — зокрема з довжиною великих чисел, якими представлено координати точок кривої.
- Арифметика довгих чисел у JS має час, залежний від значень: числа з «коротшим» представленням обробляються швидше, і ця різниця — готовий канал витоку.
Тобто навіть якщо автор чисто-JS бібліотеки сумлінно писав «константний» код, рушій не гарантує, що виконуватиметься саме він. Для звичайної бізнес-логіки це неважливо; для операцій із приватним ключем — критично.
Одноразове число k: найнебезпечніше місце ДСТУ 4145
У схемах підпису на еліптичних кривих родини ECDSA — до якої належить і ДСТУ 4145 — кожен підпис потребує свіжого випадкового числа k (nonce). Підпис — це пара чисел (r, s), де r обчислюється з точки k·P, а s зв'язує в одному рівнянні три величини: геш документа h, приватний ключ d і те саме k — схематично s = k⁻¹(h + d·r).
Тепер механіка катастрофи на пальцях. В одному рівнянні — два невідомих (k і d), тому з одного підпису ключ не дістати. Але якщо k повторився у двох підписах різних документів, невідомих усе ще два — а рівнянь уже два. Віднімаємо одне від одного — d скорочується, і k обчислюється з різниці гешів і різниці s. Підставляємо знайдене k назад — і отримуємо приватний ключ d. Уся «атака» — кілька рядків арифметики над двома публічними підписами: зловмиснику не потрібен ні доступ до пристрою, ні перехоплення ключа. Достатньо двох підписаних файлів.
Гірше того: k не обов'язково має повторитися повністю. Якщо він просто передбачуваний або зміщений — кілька старших бітів невипадкові, генератор має вузький діапазон — існують методи, які відновлюють ключ із десятків-сотень підписів, зібраних пасивно. Тому вимога до k потрійна: унікальний для кожного підпису, рівномірно випадковий, назавжди секретний. Порушення будь-якого пункту в реалізації означає, що кожен виданий підпис — це фрагмент вашого приватного ключа, викладений публічно.
Слабка ентропія: Math.random проти crypto.getRandomValues
Звідки чисто-JS реалізація бере випадковість для k? Правильна відповідь одна: crypto.getRandomValues() — криптографічний генератор (CSPRNG), яким браузер обгортає системне джерело ентропії. Але в JS-екосистемі історично поруч лежить Math.random() — швидкий некриптографічний генератор для ігор і анімацій. Його внутрішній стан невеликий і відновлюється спостерігачем за кількома вихідними значеннями, після чого всі наступні «випадкові» числа передбачувані.
| crypto.getRandomValues() | Math.random() | |
|---|---|---|
| Призначення | криптографія (CSPRNG) | ігри, анімації, вибірки |
| Джерело | системна ентропія ОС | детермінований алгоритм із малим станом |
| Передбачуваність | обчислювально непередбачуваний | стан відновлюється за кількома значеннями |
| Придатність для k | так — єдиний варіант | ні: передбачуваний k = відновлюваний ключ |
Небезпека в тому, що зовні різниці не видно: підпис із k від Math.random() валідний, проходить усі перевірки і працює роками — доки хтось не збере достатньо підписів і не обчислить ключ. Це помилка, яку неможливо помітити тестуванням, — лише аудитом реалізації.
Окрема пастка: перевірка підпису в браузері
Дзеркальна проблема — не підпис, а перевірка. Припустімо, ви робите вхід за КЕП і перевіряєте підпис користувача JS-бібліотекою на клієнті: бібліотека повернула true — пускаємо в кабінет. Це діра в авторизації: браузер повністю контролює користувач. Відкрити консоль і замінити результат перевірки на true — або просто надіслати вашому бекенду «підпис перевірено» — може будь-хто, взагалі без ключа.
Правило просте: рішення про допуск ухвалює лише бекенд на основі серверної перевірки. Саме так побудований вхід за КЕП у DSTUcrypt: ваш бекенд отримує одноразовий challenge, користувач підписує його у віджеті, а підпис ваш сервер віддає на POST /api/auth/verify — DSTUcrypt криптографічно перевіряє його нативним ядром на сервері, куди фронтенд не має доступу, і повертає підтверджену особу (ПІБ, РНОКПП, ЄДРПОУ). Одноразовість challenge унеможливлює повторне використання підпису. Те саме стосується будь-яких критичних рішень: приймаєте документ чи платіж — перевіряйте через серверний POST /api/verify, а не вірте вердикту з браузера.
Чому WASM-збірка нативної бібліотеки — принципово інша річ
Може здатися, що WebAssembly — це «той самий браузер, ті самі проблеми». Ні. Принципова різниця — не в місці виконання, а в походженні коду. WASM-модуль DSTUcrypt — це не переписування алгоритмів на новій мові, а компіляція тієї самої C/C++-кодової бази, що роками працює в українській PKI нативно — у ЦСК, серверних і десктопних системах. Константний час операцій із секретами, коректна генерація одноразового k, робота з пам'яттю — усе це закладено й перевірено в реалізації задовго до того, як вона потрапила у браузер.
До того ж WASM виконує низькорівневі інструкції з передбачуваною арифметикою фіксованої розрядності — без динамічних типів, прихованих алокацій і залежних від значень «зрізань» довгих чисел, які роблять таймінг JS-коду функцією секретних даних. А ентропію ядро бере з crypto.getRandomValues() — CSPRNG браузера. Бонус — швидкість, близька до нативної: гешування і підпис великих файлів працюють швидко навіть на слабких пристроях.
Що це дає в DSTUcrypt. Криптоарифметика — перевірена нативна C/C++-бібліотека, скомпільована у WebAssembly: константний час операцій (захист від timing-атак), коректний одноразовий k при кожному підписі (захист від відновлення ключа з підписів — класичної слабкості чисто-JS реалізацій ДСТУ), ентропія — з CSPRNG браузера. Це production-ядро з роками експлуатації в українській PKI, а не community-експеримент.
Для інтегратора це виглядає як звичайний виклик SDK — уся арифметика відбувається у WASM-ядрі всередині iframe, ключ і пароль не покидають браузер користувача:
підпис у браузері — арифметика у WASM-ядрі
import { embed } from 'https://dstucrypt.io/embed/dstucrypt-embed.mjs';
// віджет працює в iframe на origin dstucrypt.io;
// уся криптоарифметика — у WASM-збірці нативного ядра
const signer = await embed('sign', { mount: 'modal' });
const { signature, accredited } = await signer.sign(fileBytes, {
format: 'CAdES-T',
digest: 'gost-34311', // або 'kupyna-256' — «Купина» ДСТУ 7564
});
signature.download('document.p7s');
Чекліст для розробника
- Не пишіть і не форкайте криптографію самі. Коректність за тестовими векторами ≠ безпека реалізації.
- Перевіряйте походження криптоядра. Компіляція перевіреної нативної бібліотеки (C/C++ → WASM) — добре; алгоритм, переписаний на JS з нуля, — червоний прапорець для операцій із приватним ключем.
- Питайте про константний час. Якщо в документації реалізації немає ні слова про side-channel — вважайте, що захисту немає.
- k і ентропія — лише з CSPRNG. Будь-який слід
Math.random()поруч із підписом — привід зупинити інтеграцію. - Рішення про допуск — лише після серверної перевірки. Вердикт JS-бібліотеки в браузері підробляється; використовуйте серверні
POST /api/verifyтаPOST /api/auth/verify. - Приватний ключ не має покидати браузер. Ізольований iframe на чужому origin захищає ключ навіть при XSS на вашому сайті.
Поширені запитання
Чи можна взагалі безпечно підписувати документи КЕП у браузері?
Так, якщо криптоарифметику виконує не саморобний JavaScript, а перевірена нативна бібліотека, скомпільована у WebAssembly. У DSTUcrypt це криптоядро, що роками працює в українській PKI: константний час операцій, коректне одноразове k і ентропія з CSPRNG браузера. Ключ і пароль при цьому не покидають браузер користувача.
Що станеться, якщо одноразове число k повториться у двох підписах?
Приватний ключ можна буде обчислити арифметично із самих підписів. Два підписи з однаковим k дають систему з двох рівнянь із двома невідомими — k і приватним ключем, — яка розв'язується на папері. Зловмиснику не потрібен доступ до пристрою: достатньо самих підписаних документів.
Чому не можна перевіряти підпис для входу за КЕП JS-бібліотекою в браузері?
Браузер контролює користувач, тому результат перевірки на клієнті можна підробити — наприклад, змінити відповідь скриптом у консолі. Рішення про допуск має ухвалювати бекенд на основі серверної перевірки: DSTUcrypt перевіряє підпис нативним криптоядром на сервері, а одноразовий challenge унеможливлює повторне використання.
Чим WebAssembly-збірка безпечніша за чисто-JS реалізацію?
WASM виконує ту саму кодову базу нативної C/C++-бібліотеки, що й у десктопних та серверних системах: константний час і коректна робота з одноразовим k закладені в реалізацію, перевірену роками промислової експлуатації. Чисто-JS реалізація — це окреме переписування алгоритму, в якому ці властивості треба забезпечувати з нуля, а рушій JavaScript цьому активно заважає.
Читайте також
- Чому приватний ключ ніколи не повинен покидати браузер
- «Купина» (ДСТУ 7564:2014): що це за геш-функція
- Вхід на сайт за КЕП: авторизація користувачів електронним підписом
КЕП на перевіреному нативному ядрі — за 10 хвилин
Готові віджети підпису, перевірки та шифрування за ДСТУ. Ключ і пароль не покидають браузер користувача. На кожному новому домені — 7 днів безкоштовно.
Живе демоЯк підключити