Blog · Security

The dangers of pure-JS cryptography: timing attacks, the one-time nonce k, and key recovery from signatures

Published

The DSTU 4145 algorithm is mathematically sound — but that won't save you if the implementation leaks. A developer-focused look at why cryptography rewritten "in JavaScript" is vulnerable where native code is not, and what WebAssembly does about it.

Published August 7, 2026


Short answer. The danger of pure-JS cryptography lies not in the math of the algorithms but in the implementation. JavaScript execution time depends on the data (JIT, garbage collector, engine optimizations), and that dependency leaks outward as a timing side-channel. In elliptic-curve schemes — such as Ukraine's DSTU 4145 — the most critical spot is even more insidious: the one-time nonce k. If it repeats or is predictable, an attacker can arithmetically recover the private key from the signatures themselves, with no access to the device whatsoever. That is why DSTUcrypt does not rewrite cryptography in JS: all crypto arithmetic runs in a battle-tested native crypto core compiled to WebAssembly, with constant-time operations, a correct k, and entropy from the browser's CSPRNG.

Cryptography is not "just math in code"

A common developer misconception: "the algorithm is published in a standard, so any implementation that passes the test vectors is secure". In reality, test vectors only check correctness — that input X produces output Y. Security is a different property: the implementation must not leak secrets through side channels, must not repeat random values, must not rely on weak entropy.

Modern attacks on signatures almost never "break the math". DSTU 4145 on elliptic curves and hash functions like Kupyna (DSTU 7564:2014) are sound on paper. What gets attacked is the implementation: exactly how the code performs scalar multiplication, where it gets its randomness, how long an operation takes depending on the key bits. And this is precisely where pure-JS implementations — rewritten from scratch by enthusiasts, without years of industrial deployment — lose to native libraries where it matters most.

Timing side-channel: when execution time gives away key bits

A timing attack recovers a secret from how long operations take. If a computation with a key bit of "1" takes even slightly longer than with a bit of "0", statistics over many operations is enough to recover the key bit by bit. The defense is well known: constant time — the code executes the same instruction sequence regardless of the secret values.

In native C/C++, constant time is a hard but solvable engineering problem: the developer controls the instructions the CPU will execute. In JavaScript it is practically unattainable, because between your code and the CPU sits an engine that actively rewrites the program on the fly:

  • The JIT compiler optimizes "hot" code paths based on the data it has already seen: the same function executes as different machine code at different moments, with different timing.
  • Deoptimizations and the garbage collector introduce pauses that correlate with the size and shape of the data — including the length of the big integers that represent curve point coordinates.
  • Big-integer arithmetic in JS takes value-dependent time: numbers with a "shorter" representation are processed faster, and that difference is a ready-made leakage channel.

So even if the author of a pure-JS library diligently wrote "constant-time" code, the engine does not guarantee that this is the code that actually runs. For ordinary business logic this is irrelevant; for private-key operations it is critical.

The one-time nonce k: the most dangerous spot in DSTU 4145

In elliptic-curve signature schemes of the ECDSA family — which DSTU 4145 belongs to — every signature needs a fresh random number k (the nonce). A signature is a pair of numbers (r, s), where r is computed from the point k·P, and s ties three quantities together in one equation: the document hash h, the private key d, and that same k — schematically, s = k⁻¹(h + d·r).

Now the mechanics of the catastrophe, on your fingers. One equation holds two unknowns (k and d), so a single signature does not expose the key. But if k repeats across two signatures of different documents, there are still two unknowns — and now two equations. Subtract one from the other — d cancels out, and k is computed from the difference of the hashes and the difference of the s values. Substitute the recovered k back — and out comes the private key d. The entire "attack" is a few lines of arithmetic over two public signatures: the attacker needs neither access to the device nor interception of the key. Two signed files are enough.

Worse still: k does not even have to repeat in full. If it is merely predictable or biased — a few high-order bits non-random, a generator with a narrow range — there are methods that recover the key from tens to hundreds of signatures collected passively. So the requirement on k is threefold: unique for every signature, uniformly random, secret forever. Violating any one of these in an implementation means every signature you issue is a fragment of your private key, published for all to see.

Weak entropy: Math.random vs crypto.getRandomValues

Where does a pure-JS implementation get its randomness for k? There is exactly one right answer: crypto.getRandomValues() — the cryptographic generator (CSPRNG) with which the browser wraps the OS entropy source. But the JS ecosystem has historically kept Math.random() right next to it — a fast non-cryptographic generator for games and animations. Its internal state is small and can be reconstructed by an observer from a handful of outputs, after which all subsequent "random" numbers are predictable.

crypto.getRandomValues() Math.random()
Purpose cryptography (CSPRNG) games, animations, sampling
Source OS system entropy deterministic algorithm with small state
Predictability computationally unpredictable state recoverable from a few outputs
Suitability for k yes — the only option no: predictable k = recoverable key

The danger is that the difference is invisible from the outside: a signature with a k from Math.random() is valid, passes every check, and works for years — until someone collects enough signatures and computes the key. It is a mistake that testing cannot catch — only an implementation audit can.

A separate trap: verifying signatures in the browser

The mirror-image problem is not signing but verification. Suppose you build sign-in with a QES and verify the user's signature with a JS library on the client: the library returned true — let them into the account. That is a hole in your authorization: the browser is fully controlled by the user. Anyone can open the console and flip the verification result to true — or simply tell your backend "signature verified" — without any key at all.

The rule is simple: only the backend decides on access, based on server-side verification. That is exactly how QES sign-in is built in DSTUcrypt: your backend receives a one-time challenge, the user signs it in the widget, and your server submits the signature to POST /api/auth/verify — DSTUcrypt cryptographically verifies it with the native core on the server, out of the frontend's reach, and returns the confirmed identity (full name, RNOKPP, EDRPOU). The one-time challenge makes signature replay impossible. The same goes for any critical decision: when accepting a document or a payment, verify through the server-side POST /api/verify rather than trusting a verdict from the browser.

Why a WASM build of a native library is a fundamentally different thing

It may seem that WebAssembly is "the same browser, the same problems". It is not. The fundamental difference lies not in where the code runs but in where the code comes from. The DSTUcrypt WASM module is not a rewrite of the algorithms in a new language — it is a compilation of the very same C/C++ codebase that has run natively in Ukrainian PKI for years — in CAs, server and desktop systems. Constant-time operations on secrets, correct generation of the one-time nonce k, memory handling — all of that was designed and vetted in the implementation long before it ever reached the browser.

On top of that, WASM executes low-level instructions with predictable fixed-width arithmetic — no dynamic types, hidden allocations, or value-dependent big-integer "shortcuts" that make JS code timing a function of secret data. And the core draws its entropy from crypto.getRandomValues() — the browser's CSPRNG. As a bonus, you get near-native speed: hashing and signing large files stays fast even on weak devices.

What this gives you in DSTUcrypt. The crypto arithmetic is a battle-tested native C/C++ library compiled to WebAssembly: constant-time operations (protection against timing attacks), a correct one-time k for every signature (protection against key recovery from signatures — the classic weakness of pure-JS DSTU implementations), and entropy from the browser's CSPRNG. It is a production core with years of service in Ukrainian PKI, not a community experiment.

To an integrator it looks like an ordinary SDK call — all the arithmetic happens in the WASM core inside an iframe, and the key and password never leave the user's browser:

signing in the browser — arithmetic in the WASM core

import { embed } from 'https://dstucrypt.io/embed/dstucrypt-embed.mjs';

// the widget runs in an iframe on the dstucrypt.io origin;
// all crypto arithmetic runs in the WASM build of the native core
const signer = await embed('sign', { mount: 'modal' });
const { signature, accredited } = await signer.sign(fileBytes, {
  format: 'CAdES-T',
  digest: 'gost-34311', // or 'kupyna-256' — Kupyna DSTU 7564
});
signature.download('document.p7s');

A developer's checklist

  • Do not write or fork cryptography yourself. Correctness against test vectors ≠ implementation security.
  • Check the provenance of the crypto core. A compilation of a proven native library (C/C++ → WASM) is good; an algorithm rewritten in JS from scratch is a red flag for private-key operations.
  • Ask about constant time. If the implementation's documentation says nothing about side channels — assume there is no protection.
  • k and entropy — only from a CSPRNG. Any trace of Math.random() near signing code is a reason to halt the integration.
  • Access decisions — only after server-side verification. A JS library's verdict in the browser can be forged; use the server-side POST /api/verify and POST /api/auth/verify.
  • The private key must never leave the browser. An isolated iframe on a foreign origin protects the key even under XSS on your site.

Frequently asked questions

Can documents be signed with a QES in the browser safely at all?

Yes — provided the crypto arithmetic is done not by home-grown JavaScript but by a proven native library compiled to WebAssembly. In DSTUcrypt this is a crypto core with years of service in Ukrainian PKI: constant-time operations, a correct one-time nonce k, and entropy from the browser's CSPRNG. The key and password never leave the user's browser.

What happens if the one-time nonce k repeats in two signatures?

The private key can then be computed arithmetically from the signatures themselves. Two signatures with the same k give a system of two equations in two unknowns — k and the private key — which can be solved on paper. The attacker needs no access to the device: the signed documents alone are enough.

Why can't a JS library in the browser be used to verify a signature for QES sign-in?

The user controls the browser, so a client-side verification result can be forged — for example, by overriding the response from the console. The access decision must be made by the backend based on server-side verification: DSTUcrypt verifies the signature with the native crypto core on the server, and the one-time challenge makes replay impossible.

How is a WebAssembly build safer than a pure-JS implementation?

WASM runs the very same codebase of the native C/C++ library used in desktop and server systems: constant time and correct handling of the one-time nonce k are built into an implementation proven by years of industrial use. A pure-JS implementation is a separate rewrite of the algorithm in which these properties must be achieved from scratch — while the JavaScript engine actively works against you.

Read next

QES on a proven native core — in 10 minutes

Ready-made widgets for signing, verification and encryption under DSTU standards. The key and password never leave the user's browser. Every new domain gets 7 days free.

Live demoHow to integrate

Try QES on your site

Ready-made widgets for signing, verification and sign-in under DSTU standards. Every new domain gets 7 days free.