Verification gets the public key Q, the message hash z and the pair (r, s), and nothing else. It has to decide whether whoever produced those numbers knew d. It does so by rebuilding the signer’s nonce point out of the pieces and checking that it lands where r says it should.
reject unless 1 <= r <= n-1 and 1 <= s <= n-1
u1 = z * inv(s) mod n
u2 = r * inv(s) mod n
R = u1*G + u2*Q
reject if R is the point at infinity
valid if R.x mod n == rThe reason it works is a single substitution. The signer set s = inv(k)·(z + r·d), so inv(s) = k·inv(z + r·d). Feed that into u1·G + u2·Q, remember that Q = d·G, and the whole expression collapses to k·G — the exact point the signer generated and then discarded. The verifier holds it without ever having learned k or d, and compares its x-coordinate with r.
What a valid signature does and does not tell you
- It tells you the signer held the private key matching this public key, and that the message hash was this z. Both, together, or the check fails.
- It tells you nothing about whether that public key belongs to anyone you trust. Binding a key to an identity is what certificates, JWKS endpoints and trust stores are for; the signature check is one link in that chain and never the whole of it.
- It does not make the signature bytes unique. Given a valid (r, s), the pair (r, n − s) is also valid — no private key required. Systems that use signature bytes as an identifier have to normalise to one of the two forms, which is what the “low-s” rule is about.
The range checks in the first line matter too. A verifier that accepts r = 0 or s = 0, or that forgets to confirm the public key is a point on the curve at all, opens the door to forgeries that have nothing to do with breaking the discrete logarithm. Every serious library performs these checks; several historical CVEs exist because one of them did not.