Lattices are not only a foundation for post-quantum cryptography. They are also one of the most effective tools for breaking classical signatures that are implemented carelessly. If an ECDSA implementation leaks even a few bits of each random nonce, through timing, a biased random number generator or a cache side channel, a lattice reduction algorithm such as LLL can combine those scraps into the full private key. The underlying mathematical problem is the hidden number problem (HNP), introduced by Boneh and Venkatesan in 1996. This guide builds the attack lattice step by step, runs it on a toy curve, and ends with the defenses that close the hole.

Why the ECDSA nonce is as secret as the key

Let GG be a point of prime order nn, let dd be the private key and let hh be the hash of the message. To sign, the signer picks a fresh nonce k∈[1,n−1]k \in [1, n-1], sets rr to the x-coordinate of kGkG reduced mod nn, and computes

s=k−1(h+r d) mod n.s = k^{-1}(h + r\,d) \bmod n.

The pair (r,s)(r,s) is public. Rearranging gives d=r−1(s k−h) mod nd = r^{-1}(s\,k - h) \bmod n, so anyone who learns a single nonce completely learns the key. The degenerate case is reusing a nonce: two signatures with the same kk give two linear equations in two unknowns, and simple algebra recovers kk and then dd. That is how the fail0verflow team recovered Sony’s PlayStation 3 signing key in 2010, because the same nonce had been used for every signature.

The lattice attack handles the much subtler case where no nonce is ever revealed in full, but each one is slightly predictable: for example, its top few bits are always zero, or a timing measurement tells you that they probably are.

The hidden number problem

Divide the signing equation by ss. For every signature ii define the public values ti=risi−1 mod nt_i = r_i s_i^{-1} \bmod n and ui=hisi−1 mod nu_i = h_i s_i^{-1} \bmod n. Then

ki≡ti d+ui(modn).k_i \equiv t_i\, d + u_i \pmod n.

Without extra information this says nothing: kik_i is uniform, so every dd is consistent. Now suppose the attacker knows that every nonce is small, 0≤ki<B0 \le k_i < B with B=n/2ℓB = n / 2^{\ell}, because its top ℓ\ell bits are zero. (Known nonzero top bits reduce to this case by subtracting them off, and known low bits by a similar shift.) The hidden number problem asks: given many pairs (ti,ui)(t_i, u_i) such that tid+ui mod nt_i d + u_i \bmod n is smaller than BB, find the hidden number dd.

Each signature contributes about ℓ\ell bits of information about dd. Since dd has about log⁡2n\log_2 n bits, you need at least m≳log⁡2n/ℓm \gtrsim \log_2 n / \ell signatures, and a lattice turns that information-theoretic count into an algorithm.

Building the attack lattice

The congruences say that the vector of nonces is close to a lattice point: each kik_i equals tid+uit_i d + u_i minus some multiple cinc_i n. The standard way to exploit this is an (m+2)(m+2)-dimensional lattice whose basis rows are

(n00n00⋱⋮⋮n00t1t2⋯tmB/n0u1u2⋯um0B)\begin{pmatrix} n & & & & 0 & 0 \\ & n & & & 0 & 0 \\ & & \ddots & & \vdots & \vdots \\ & & & n & 0 & 0 \\ t_1 & t_2 & \cdots & t_m & B/n & 0 \\ u_1 & u_2 & \cdots & u_m & 0 & B \end{pmatrix}

Take dd copies of the tt-row, one copy of the uu-row, and subtract cic_i copies of each nn-row. The result is the lattice vector

v=(k1,…,km, dB/n, B).v = \bigl(k_1, \dots, k_m,\ dB/n,\ B\bigr).

Every coordinate of vv is at most BB, so its length is at most Bm+2B\sqrt{m+2}. This is the whole trick: an unusually short vector that carries the key in its second-to-last coordinate.

Why the scaling factors B/n and B?

The two extra coordinates are weights chosen so that every entry of the target vector has the same size as a nonce. The key dd can be as large as nn, so the factor B/nB/n shrinks dB/nd B/n to below BB. The last coordinate is an embedding constant: it turns the close-vector problem “find the lattice point nearest to (u1,…,um)(u_1,\dots,u_m)” into a short-vector problem (Kannan’s embedding), and setting it to BB keeps it on the same scale. If one coordinate were much larger than the others, the target would stop being the shortest vector and reduction would find something else. In code, multiply every entry by nn to keep the basis integral.

Is vv really the shortest vector? The lattice has determinant nm−1B2n^{m-1}B^2, so by the Gaussian heuristic a random vector in it has length about (m+2)/(2πe) (nm−1B2)1/(m+2)\sqrt{(m+2)/(2\pi e)}\,(n^{m-1}B^2)^{1/(m+2)}. Requiring ∥v∥\|v\| to be smaller and substituting B=n/2ℓB = n/2^{\ell} gives roughly

m ℓ  >  log⁡2n+m+22log⁡2(2πe).m\,\ell \;>\; \log_2 n + \tfrac{m+2}{2}\log_2(2\pi e).

The first term is the information count from above. The second is the price of using a lattice, about two extra bits per signature. It explains why lattice attacks are comfortable with four or more leaked bits per nonce and struggle as the leak approaches one bit.

Solving it: LLL, BKZ, or CVP

When the gap between ∥v∥\|v\| and the Gaussian heuristic is large, plain LLL finds vv. With less leakage per signature you need more signatures, the lattice dimension grows, and you move up to BKZ or sieving to solve the underlying shortest vector problem. Equivalently, you can drop the embedding row and run a closest vector algorithm such as Babai’s nearest-plane method with the target (−u1,…,−um,0)(-u_1,\dots,-u_m, 0).

Two practical details. First, the reduced basis always contains the harmless vector (0,…,0,B,0)(0,\dots,0,B,0), which is nn times the tt-row minus multiples of the nn-rows. It is as short as vv and carries no information, so scan the reduced rows for one whose last coordinate is ±B\pm B. Second, every candidate is checked for free against the public key: dGdG must equal it.

Worked example: a 16-bit toy curve

Take the curve y2=x3+x+7y^2 = x^3 + x + 7 over F65357\mathbb{F}_{65357}. It has prime order n=65293n = 65293, and we use the generator G=(1,3)G = (1, 3). The private key is d=8806d = 8806, giving the public key Q=dG=(2507,785)Q = dG = (2507, 785). A faulty signer draws every nonce below B=28B = 2^8, so the top 8 of 16 bits are always zero. Four signatures:

ihrstit_iuiu_isecret kik_i
13730314851425792491913276217
25257736535443876508230058196
341354692928026508621927466
477274068286673154015309127

The attacker sees only the first three columns. The six-dimensional lattice has determinant n3B2≈1.8×1019n^{3}B^2 \approx 1.8\times 10^{19}, so the Gaussian heuristic predicts vectors of length about 962. The target v=(217,196,66,127, 34.5, 256)v = (217, 196, 66, 127,\ 34.5,\ 256) has length about 416, less than half of that, and LLL finds it. With the basis scaled by nn to integers, the second row of the reduced basis is

(14168581, 12797428, 4309338, 8292211, 2254336, 16715008).(14168581,\ 12797428,\ 4309338,\ 8292211,\ 2254336,\ 16715008).

Dividing the first four entries by nn gives the nonces 217, 196, 66 and 127. The fifth entry is dB=2254336dB = 2254336, so d=2254336/256=8806d = 2254336/256 = 8806, and 8806 G8806\,G matches QQ. The first reduced row is the harmless (0,0,0,0,−nB,0)(0,0,0,0,-nB,0). With only three signatures the same attack fails on some seeds, in line with the bound above: 3⋅8=243 \cdot 8 = 24 bits is barely more than the 16 bits of the key plus the lattice overhead. The whole lattice step fits in a few lines of fpylll:

from fpylll import IntegerMatrix, LLL

n, B = 65293, 2**8          # group order, bound on every nonce
sigs = [(37303, 14851, 42579), (52577, 36535, 44387),
        (4135, 46929, 28026), (7727, 40682, 8667)]   # (h, r, s)

t = [r * pow(s, -1, n) % n for h, r, s in sigs]
u = [h * pow(s, -1, n) % n for h, r, s in sigs]
m = len(sigs)

# Basis rows, multiplied by n so every entry is an integer.
rows = [[n * n if j == i else 0 for j in range(m)] + [0, 0] for i in range(m)]
rows.append([n * ti for ti in t] + [B, 0])
rows.append([n * ui for ui in u] + [0, B * n])

A = IntegerMatrix.from_matrix(rows)
LLL.reduction(A)
for row in A:
    if abs(row[-1]) == B * n:                  # the embedding coordinate
        sign = 1 if row[-1] > 0 else -1
        print("candidate d =", sign * row[-2] // B % n)
# Prints 8806 and 52739. Keep the one where d*G equals the public key.

On a real 256-bit curve the same code works unchanged in principle. What changes is the number of signatures and the strength of the reduction you need, which is where the research below comes in.

Real-world attacks

On the lattice side, Albrecht and Heninger (Eurocrypt 2021) pushed the boundary further by adding a predicate, a cheap check that recognises the correct key, to enumeration and sieving. That solved HNP instances with fewer leaked bits and fewer signatures than were previously thought reachable with lattices.

Defenses

Common mistakes

Further reading

Boneh and Venkatesan introduced the problem in “Hardness of computing the most significant bits of secret keys in Diffie–Hellman and related schemes” (Crypto 1996). Howgrave-Graham and Smart first applied it to DSA with partially known nonces, and Nguyen and Shparlinski analysed DSA and ECDSA in detail. For the recent attacks, see TPM-FAIL, Minerva, LadderLeak and Albrecht–Heninger’s On Bounded Distance Decoding with Predicate. The reduction algorithms themselves are covered in LLL and BKZ.