Learning Goals
3 minBy the end of this lesson you can:
- Sign data with a private key and verify with the matching public key.
- Explain what a signature proves: authenticity, integrity, non-repudiation.
- Distinguish signing from encryption (opposite key directions).
- Relate signatures to TLS certificates, software updates, and git commits.
Warm-Up · A Seal Anyone Can Check
5 minENCRYPT: encrypt with PUBLIC key → only PRIVATE decrypts (for privacy) SIGN: sign with PRIVATE key → anyone verifies w/ PUBLIC (for proof) A signature is like a wax seal only YOU can make (private key) but ANYONE can recognise as yours (public key) — and it breaks if the message is altered.
Signing uses the key pair in the reverse direction from encryption. Only your private key can produce a signature for a message; anyone with your public key can verify it. That proves three things at once: the message is from you (authenticity), it wasn't altered (integrity), and you can't later deny sending it (non-repudiation). Signatures are the trust layer of the internet — certificates, updates, packages, and commits all rely on them.
New Concept · Sign, Verify & What It Proves
14 minHow signing actually works
SIGN: hash the message → encrypt the hash with the PRIVATE key = signature
VERIFY: hash the message yourself → decrypt the signature with the PUBLIC key
→ do the two hashes match? yes = valid; no = forged or alteredYou sign the hash of the message (small, fast), not the whole thing. Verification recomputes the hash and checks it against the signature using the public key. If even one byte of the message changed, the recomputed hash differs and verification fails.
Sign with the private key
from cryptography.hazmat.primitives.asymmetric import rsa, padding from cryptography.hazmat.primitives import hashes private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) public_key = private_key.public_key() message = b"I authorise payment of RM500 to Aisha." signature = private_key.sign( message, padding.PSS( # PSS = the modern signature padding mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), hashes.SHA256())
Verify with the public key
from cryptography.exceptions import InvalidSignature def verify(public_key, message, signature) -> bool: try: public_key.verify( signature, message, padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), hashes.SHA256()) return True except InvalidSignature: return False print(verify(public_key, message, signature)) # True print(verify(public_key, b"...RM5000 to Mallory.", signature)) # False — altered!
Use PSS padding for signatures (the modern, randomised scheme). Change the message and verification fails — that's integrity. Use the wrong public key and it fails — that's authenticity.
The three guarantees
- Authenticity — only the holder of the private key could have produced a signature that verifies with the public key.
- Integrity — any change to the message breaks the signature (the hash won't match).
- Non-repudiation — the signer can't credibly deny it, since only they have the private key. (This is why "protect your private key" is paramount.)
Signing ≠ encryption
A signature does not hide the message — the message travels in the clear alongside its signature. Signing proves who and unaltered; encryption provides secrecy. You can do both (sign and encrypt) when you need authenticity and confidentiality. They use the key pair in opposite directions and solve different problems.
Where you already rely on signatures
TLS certificates a CA SIGNS a site's cert; your browser verifies it (L8-09) software updates the vendor signs the package; your OS verifies before install package managers pip/apt verify signatures to detect tampered packages (L8-35) git signed commits/tags prove who authored them JWTs signed tokens prove the server issued them (L8-38)
Worked Example · A Signed Software Release
12 minGoal: a publisher signs a release file; a downloader verifies it's authentic and untampered before trusting it — the exact mechanism behind secure software updates and the Lesson 35 supply-chain defence.
from cryptography.hazmat.primitives.asymmetric import rsa, padding from cryptography.hazmat.primitives import hashes, serialization from cryptography.exceptions import InvalidSignature from pathlib import Path # --- publisher side (has the PRIVATE key) --- def sign_release(file_path: str, private_key) -> bytes: data = Path(file_path).read_bytes() return private_key.sign( data, padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), hashes.SHA256()) # --- downloader side (has only the publisher's PUBLIC key) --- def verify_release(file_path: str, signature: bytes, public_key) -> bool: data = Path(file_path).read_bytes() try: public_key.verify( signature, data, padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), hashes.SHA256()) return True except InvalidSignature: return False # demo priv = rsa.generate_private_key(public_exponent=65537, key_size=2048) pub = priv.public_key() Path("release.bin").write_bytes(b"v2.0 application binary contents") sig = sign_release("release.bin", priv) # publisher signs, ships sig + file Path("release.bin.sig").write_bytes(sig) # downloader verifies BEFORE running the file: if verify_release("release.bin", sig, pub): print("✓ signature valid — safe to install") else: print("✗ INVALID — do NOT install (tampered or fake)") # attacker tampers with the file after signing: Path("release.bin").write_bytes(b"v2.0 application + hidden backdoor") print("after tampering:", "valid" if verify_release("release.bin", sig, pub) else "REJECTED")
✓ signature valid — safe to install after tampering: REJECTED
Read the code
This is how your computer protects you from malicious updates. The publisher signs the release with their private key (which never leaves them); you verify with their public key (widely distributed, e.g. baked into your OS). If an attacker swaps in a backdoored binary after signing, the signature no longer matches the file's hash — verification fails, and you refuse to install. No secret had to be shared, yet authenticity and integrity are guaranteed. The whole trust chain of software distribution runs on exactly this.
Try It Yourself
13 minGenerate a key pair, sign a message, and verify it. Then change one character of the message and confirm verification fails. Then verify the original with a different public key and confirm that fails too.
Demonstrate both failure modes separately: (a) same key, altered message → integrity fail; (b) unaltered message, wrong key → authenticity fail. Explain which guarantee each protects.
Combine lessons: encrypt a message to Bob (his public key, Lesson 16) AND sign it with your private key, so Bob gets secrecy and proof it's from you. Have Bob decrypt then verify. State the order you chose (sign-then-encrypt vs encrypt-then-sign) and why.
Hint
# common pattern: sign the plaintext, then encrypt {message + signature}. # Bob decrypts to get message+signature, then verifies the signature # with Aisha's public key. He learns it's secret AND genuinely from her.
Mini-Challenge · A Signed-Document Verifier CLI
8 minBuild a small CLI with three commands: keygen (save a PEM key pair), sign <file> (write a .sig using the private key), and verify <file> <sig> <pubkey> (report VALID/INVALID). This is a usable tool for distributing files with proof of origin.
Show a sample solution
import argparse from pathlib import Path from cryptography.hazmat.primitives.asymmetric import rsa, padding from cryptography.hazmat.primitives import hashes, serialization from cryptography.exceptions import InvalidSignature PSS = padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH) def keygen(a): priv = rsa.generate_private_key(public_exponent=65537, key_size=2048) Path("priv.pem").write_bytes(priv.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.BestAvailableEncryption(a.passphrase.encode()))) Path("pub.pem").write_bytes(priv.public_key().public_bytes( serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo)) print("wrote priv.pem (encrypted) + pub.pem") def sign(a): priv = serialization.load_pem_private_key( Path("priv.pem").read_bytes(), password=a.passphrase.encode()) sig = priv.sign(Path(a.file).read_bytes(), PSS, hashes.SHA256()) Path(a.file + ".sig").write_bytes(sig) print("wrote", a.file + ".sig") def verify(a): pub = serialization.load_pem_public_key(Path(a.pubkey).read_bytes()) try: pub.verify(Path(a.sig).read_bytes(), Path(a.file).read_bytes(), PSS, hashes.SHA256()) print("VALID ✓") except InvalidSignature: print("INVALID ✗ — tampered or wrong key") p = argparse.ArgumentParser(); sub = p.add_subparsers(required=True) g = sub.add_parser("keygen"); g.add_argument("--passphrase", required=True); g.set_defaults(func=keygen) s = sub.add_parser("sign"); s.add_argument("file"); s.add_argument("--passphrase", required=True); s.set_defaults(func=sign) v = sub.add_parser("verify"); v.add_argument("file"); v.add_argument("sig"); v.add_argument("pubkey"); v.set_defaults(func=verify) args = p.parse_args(); args.func(args)
Non-negotiables: keygen/sign/verify, PSS padding, passphrase-protected private key, clear VALID/INVALID output.
Recap
3 minA digital signature uses the key pair in reverse: you sign a message's hash with your private key, and anyone verifies with your public key (use PSS padding). It proves three things — authenticity (only you could sign), integrity (any change breaks it), and non-repudiation (you can't deny it). Signing is not encryption: it proves origin and integrity but doesn't hide the message; combine both when you need secrecy too. Signatures are the trust layer behind TLS certificates, software updates, package managers, git, and JWTs — guard your private key accordingly.
Vocabulary Card
- digital signature
- A private-key proof of authorship and integrity, verifiable with the public key.
- PSS
- The modern, randomised padding scheme for RSA signatures.
- non-repudiation
- The signer can't credibly deny signing (only they hold the private key).
- sign vs encrypt
- Sign = prove origin/integrity; encrypt = secrecy. Opposite key directions.
Homework
4 minBuild the signed-document verifier CLI and use it to sign and verify a real file, demonstrating that tampering after signing is detected. Write a short explainer: how a signature proves authenticity + integrity + non-repudiation, and a concrete example of where you rely on signatures daily (software updates, HTTPS, app stores).
Sample · signatures explainer
A signature is made by hashing the message and "locking" that hash with my PRIVATE key. Anyone with my PUBLIC key can "unlock" it and compare to a fresh hash of the message: - Authenticity: only MY private key could produce a signature that unlocks with MY public key → it's provably from me. - Integrity: change one byte and the fresh hash won't match → caught. - Non-repudiation: since only I hold the private key, I can't claim "someone else signed it." Daily reliance: when my phone installs an app update, the OS verifies the developer's SIGNATURE on the package before running it. A tampered or fake update fails verification and is refused — exactly the demo: edit the file after signing and "verify" returns INVALID.
Non-negotiables: working sign/verify CLI showing tamper-detection, and an explanation covering all three guarantees with a real example.