The Signature Made of Zeros — Java Bug Hunt
Modelled on "Psychic Signatures" (CVE-2022-21449), disclosed in April 2022 by Neil Madden.
- Language: Java
- Layer: Backend
- Difficulty: Hard
- Concepts: Security, Crypto
- Modelled on: Java · CVE-2022-21449
- Visible tests: a genuine signature verifies; a blank r = s = 0 signature is rejected; a signature over a different hash is rejected
- Reward: 50 XP for a complete fix
Briefing
Modelled on "Psychic Signatures" (CVE-2022-21449), disclosed in April 2022 by Neil Madden. Java 15 through 18 had a rewritten ECDSA implementation that skipped a check the algorithm requires: that the signature values r and s both lie in [1, n-1]. A signature with r = s = 0 passed verification for any message and any public key, so anything relying on Java's ECDSA — signed JWTs, SAML assertions, TLS handshakes — could be fooled by a blank signature. Oracle fixed it in the April 2022 Critical Patch Update.
This reconstruction uses a deliberately insecure toy group (ToyCurve) that keeps ECDSA's verification algebra: inverting 0 comes back as 0, and the identity "point" has x = 0. EcdsaVerifier.verify does the maths but never checks the ranges.
Fix verify so out-of-range signatures are rejected before any arithmetic.
Bug report
BUG-PSYCHIC · Priority: Critical (auth bypass) · Reported by: security
EcdsaVerifier.verify(z, r, s, publicKey) must return true only for a genuine signature of hash z under publicKey:
- r and s must each satisfy 1 <= value <= N - 1 (N = ToyCurve.N); anything else (0, N, negative, larger than N, null) is rejected immediately
- for in-range values: w = s^-1, u1 = z*w, u2 = r*w (mod N) and the signature is valid when (u1*G + u2*Q) mod N equals r
Observed: verify(anyHash, 0, 0, anyKey) returns true.
Logs
[auth] ES256 token accepted: r=0 s=0 sub=admin
[auth] ES256 token accepted: r=0 s=0 sub=rootThe code as shipped
src/crypto/EcdsaVerifier.java (editable)
import java.math.BigInteger;
class EcdsaVerifier {
static boolean verify(BigInteger z, BigInteger r, BigInteger s, BigInteger publicKey) {
BigInteger w = ToyCurve.inverse(s);
BigInteger u1 = z.multiply(w).mod(ToyCurve.N);
BigInteger u2 = r.multiply(w).mod(ToyCurve.N);
BigInteger point = ToyCurve.add(ToyCurve.mul(u1), ToyCurve.mul(u2, publicKey));
return point.mod(ToyCurve.N).equals(r);
}
}Read-only context: src/crypto/Signer.java, src/crypto/ToyCurve.java.
Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More Java bug hunts.