The Signature Nonce That Never Changed — Python Bug Hunt
Modelled on the PlayStation 3 signing-key disclosure presented by the fail0verflow group in December 2010.
- Language: Python
- Layer: Backend
- Difficulty: Hard
- Concepts: Security, Randomness
- Modelled on: Sony PS3 · 2010
- Visible tests: a signature verifies; two signatures never share a nonce
- Reward: 50 XP for a complete fix
Briefing
Modelled on the PlayStation 3 signing-key disclosure presented by the fail0verflow group in December 2010. ECDSA requires a fresh, secret random number (the nonce k) for every signature. Sony's signing used the same k for every signature, and two signatures made with the same nonce are enough to compute the private key with simple algebra — which is what happened to the key Sony used to sign PS3 software.
signer.py signs with a DSA-style scheme over a toy group (group.py; the numbers are tiny so tests run instantly, the structure is the real one). It draws its nonce once, when the signer is created.
Fix Signer so every signature uses a fresh nonce from the injected RNG.
Bug report
BUG-SIGN-K · Priority: Critical (key compromise) · Reported by: crypto review
Signer(private_key, rng).sign(message) returns (r, s):
- each call draws a NEW nonce k = rng.below(group.Q); nothing is drawn when the signer is created
- k == 0 is not a valid nonce: draw again
- r = pow(G, k, P) % Q and s = k^-1 (digest(message) + private_key r) % Q; if r == 0 or s == 0, draw again
- every signature must verify with group.verify(public_key, message, sig)
Consequences the tests check: n signatures consume n nonces (when none are rejected), and two signatures never share an r.
Observed: every signature from the release signer carries the same r.
Logs
[signer] sign pkg=update-3.41 r=0x2c1 s=0x0f3
[signer] sign pkg=update-3.50 r=0x2c1 s=0x1a7
[audit] 2 signatures share r — private key recoverableThe code as shipped
src/signing/signer.py (editable)
group = bug_require("./group.py")
class Signer:
def __init__(self, private_key, rng):
self.private_key = private_key
self.rng = rng
self.nonce = self._draw_nonce()
def _draw_nonce(self):
k = self.rng.below(group.Q)
while k == 0:
k = self.rng.below(group.Q)
return k
def sign(self, message):
k = self.nonce
r = pow(group.G, k, group.P) % group.Q
s = (pow(k, -1, group.Q) * (group.digest(message) + self.private_key * r)) % group.Q
return (r, s)
Read-only context: src/signing/group.py, src/signing/rng.py.
Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More Python bug hunts.