Syllabus Lesson 84 of 156 · Encryption you can explain
Encryption you can explain

Verify a signature

Why it matters Every software update your phone installs, every certificate behind a padlock, and every passkey login rests on one check: "was this really signed by the right key?" Doing that check yourself, once, turns signatures from magic into something you can explain.

A signature works like a wax seal stamped with a ring only the owner has. Anyone who knows what the ring's pattern looks like can check the seal, but nobody else can make it. And if the letter is changed after sealing, the seal no longer matches.

Digital signature: a number made from a message and a private key. Anyone with the matching public key can check that the owner made it, and that the message has not changed since.

Step 1: fingerprint the message with a hash

Hash: a function that turns any data into a short fixed-size fingerprint. The same input always gives the same fingerprint, a tiny change gives a completely different one, and you cannot run it backwards. SHA-256 is a common one.

import hashlib
print(hashlib.sha256(b"Lunch at 1pm").hexdigest()[:16])
print(hashlib.sha256(b"Lunch at 2pm").hexdigest()[:16])
# two completely different fingerprints

Signing the fingerprint instead of the whole message keeps things small and fast. To do maths on it, turn the 32 bytes into one big whole number with int.from_bytes(digest, "big"), then take the remainder after dividing by n with % n so it fits the key.

Step 2: undo the signature with the public key

In lesson 81's toy RSA, the owner signs by computing pow(fingerprint, d, n) with the private d. To check, you compute pow(signature, e, n) with the public e. If the result equals the fingerprint you computed yourself, the signature is genuine.

In Python a key is a pair of numbers in a tuple, such as public_key = (n, e). The line n, e = public_key unpacks it: it puts the first number in n and the second in e.

signer:   signature = pow(fingerprint, d, n)      # needs the private key
checker:  pow(signature, e, n) == fingerprint ?   # needs only the public key

If someone changes the message, its fingerprint changes and no longer matches. If they change the signature, undoing it gives a different number. And if someone signs with their own private key while pretending to be Ana (the sender in this exercise), your check with Ana's public key fails. All three are caught by one comparison.

Important: this is toy RSA with tiny keys and no padding, for learning only. Real signatures use vetted libraries and schemes such as Ed25519 or RSA with PSS padding. Never use homemade cryptography to protect real data.

A common misconception: "a signature hides the message". It does not; the message travels in plain view. A signature proves who made it and that it is unchanged. Hiding it is encryption's job.

In other languages

Real code verifies with a library. Each call returns true or false (or raises an error) instead of comparing numbers by hand.

# Python, with the cryptography package (Ed25519)
from cryptography.exceptions import InvalidSignature
try:
    public_key.verify(signature, message)   # raises if it does not match
    ok = True
except InvalidSignature:
    ok = False
// JavaScript (browser Web Crypto, ECDSA)
const ok = await crypto.subtle.verify(
  { name: "ECDSA", hash: "SHA-256" }, publicKey, signature, messageBytes);
// Go (crypto/ed25519)
ok := ed25519.Verify(publicKey, message, signature)
// PHP (libsodium, built in)
$ok = sodium_crypto_sign_verify_detached($signature, $message, $publicKey);
// C# (.NET)
bool ok = ecdsa.VerifyData(message, signature, HashAlgorithmName.SHA256);

Recap

  • A signature proves who made a message and that it has not changed. It does not hide it.
  • Hash the message first, then check the signature against that fingerprint.
  • Checking needs only the public key; signing needs the private key.
  • Toy RSA is for understanding; real code uses vetted libraries.
Your turn

Ana signs her messages with toy RSA. Her public key is PUBLIC_KEY = (n, e), and INBOX holds (message, signature) pairs, some tampered with. Write three functions:

  1. message_number(message, n): return the SHA-256 digest of the message (UTF-8 bytes), as a number from int.from_bytes(digest, "big"), modulo n.
  2. verify(message, signature, public_key): unpack n, e, and return True only if pow(signature, e, n) equals message_number(message, n), otherwise False.
  3. genuine_messages(inbox, public_key): return a list of the message texts whose signatures verify, in inbox order.
The last lines print the genuine messages. Tests use new messages and keys, so the answer cannot be hard-coded.

Spotted a problem in this lesson? Report it

Code · runs in your browser
Output