Hypothetical adversaries that can trigger the addition bug through Verify
can already recover the private key from the public key alone
The following claim is intended as a security reduction argument. Security of ECDSA over secp256r1 as a cryptographic scheme relies on the assumption that, given only a public key, it is computationally intractable to compute the private key from it. We assume that this is true. Then the below claim argues that if an attacker that only has the public key could use this to efficiently produce (r, s, e)
that triggers the bug described in Finding ref↗, then this attacker would also be able to efficiently recover the private key from the public key alone. But this would contradict the assumption that this is computationally intractable. Hence, it must be in practice impossible for an attacker to produce such (r, s, e)
if they only knew the public key.
Claim: An adversary, who knows arguments (passKey, r, s, e)
that passed to Verify
will cause a call to _jAdd
in which the two summands are equal, can use them to efficiently compute the private key corresponding to the public key passKey
.
Proof: The following diagram depicts the call graph between Verify
and _jAdd
.
We argue that along both branches, triggering the bug implies an efficiently computable, nontrivial linear relation in the elliptic curve between the generator and the public key, which we shall call . This means there are elements and of , where is the (prime) order of the elliptic curve, such that and . Should we have , then the corresponding private key is , so we assume from now on. Then we must have so we can define , which will then satisfy , so is the private key corresponding to .
Let us now consider the left part of the call graph. The function Verify
calls _preComputeJacobianPoints
, passing the public key passKey
as the only argument. The function _preComputeJacobianPoints
in turn calculates an array points
of points on the elliptic curve represented in Jacobian coordinates, with , for .
points[0] = JPoint(0, 0, 0);
points[1] = JPoint(passKey.pubKeyX, passKey.pubKeyY, 1); // u2
points[2] = _jPointDouble(points[1]);
points[3] = _jPointAdd(points[1], points[2]);
points[4] = JPoint(gx, gy, 1); // u1Points[1]
points[5] = _jPointAdd(points[4], points[1]);
points[6] = _jPointAdd(points[4], points[2]);
points[7] = _jPointAdd(points[4], points[3]);
points[8] = _jPointDouble(points[4]); // u1Points[2]
points[9] = _jPointAdd(points[8], points[1]);
points[10] = _jPointAdd(points[8], points[2]);
points[11] = _jPointAdd(points[8], points[3]);
points[12] = _jPointAdd(points[4], points[8]); // u1Points[3]
points[13] = _jPointAdd(points[12], points[1]);
points[14] = _jPointAdd(points[12], points[2]);
points[15] = _jPointAdd(points[12], points[3]);
There are three types of _jAdd
calls being made through the wrapper _jPointAdd
when computing this table:
One summand is ; the other is (for
points[3]
).One summand is ; the other is (for
points[12]
).One summand is ; the other is for (for
points[i]
with ).
In each case, equality of the two summands implies a nontrivial linear relation between and .
Finally, let us consider the right part of the call graph, where we need to look into a call to ShamirMultJacobian(JPoint[16] memory points, uint u1, uint u2)
, where points
is precisely the precomputed list of points discussed and where the concrete values of u1
and u2
are irrelevant for the following argument. This function uses Shamir's trick to efficiently compute using the precomputed points. There is only one call to _jAdd
in this function, namely
(x, y, z) = _jAdd(
x,
y,
z,
points[index].x,
points[index].y,
points[index].z
);
The arguments (x,y,z)
to _jAdd
will represent the point where and . The call to _jAdd
will only be carried out if index
is nonzero, and in that case, we know that we will have with , not both zero. As the values of u1
and u2
are the result of a computation modulo in VerifyWithPrecompute
, they must be smaller than ; from which, it follows that we must have . This implies that is a nontrivial linear relation between and .
This ends the proof of the claim.
As an additional check on this argument, we produced a script that recovers the secret key from the public key and a test vector that triggers this bug.