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=root

The 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.