Digital Signatures and PKI
On this page
Proving Who Signed It and That It Wasn't Changed#
Encryption keeps data secret. A digital signature keeps data authentic. Where encryption hides content, a signature attaches a mathematical proof of authorship and integrity. It delivers three properties at once:
- Authenticity: the message really came from the holder of a specific private key.
- Integrity: it has not been altered by a single bit since it was signed.
- Non-repudiation: the signer cannot later credibly deny signing it, because only they hold the private key.
That last property is what a plain HMAC cannot give you: an HMAC uses a shared secret, so either party could have produced the tag. A signature uses a key pair, so only one party could have.
How a Signature Works#
Signing uses the same asymmetric key pair as public-key encryption, but in the opposite direction. In encryption you encrypt to someone's public key so only their private key can read it. In signing, the private key produces the signature and the public key verifies it. The logic is: anyone can verify, but only the private-key holder could have produced a signature that verifies.
For speed, you do not sign the whole message. You hash it to a fixed-size digest and sign the digest, exactly the hybrid idea from encryption:
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes
from cryptography.exceptions import InvalidSignature
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()
message = b"transfer 100 to alice"
signature = private_key.sign(message, ec.ECDSA(hashes.SHA256()))
# The verifier has only the public key, the message, and the signature
public_key.verify(signature, message, ec.ECDSA(hashes.SHA256()))
print("signature valid")
# Change one byte and verification fails
try:
public_key.verify(signature, b"transfer 900 to alice", ec.ECDSA(hashes.SHA256()))
except InvalidSignature:
print("tampered message rejected")
The verifier never sees the private key, yet can be certain the message is unmodified and came from whoever holds it. verify raises rather than returning False, so treat any exception as "reject."
Common signature algorithms: RSA (with PSS padding for new use, not the older PKCS#1 v1.5), ECDSA (used above, compact keys), and Ed25519 (a modern, fast, misuse-resistant choice):
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
sk = Ed25519PrivateKey.generate()
sig = sk.sign(b"release v2.1.0")
sk.public_key().verify(sig, b"release v2.1.0") # raises if invalid
Ed25519 is a good default for new systems: short keys and signatures, fast, and hard to misuse (no separate nonce to get wrong, unlike naive ECDSA).
Signing a file follows the same shape: hash the contents, sign the digest, and distribute the file alongside the signature and the public key. A minimal release-signing routine:
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey, Ed25519PublicKey)
from cryptography.exceptions import InvalidSignature
def sign_file(private_key, path: str) -> bytes:
with open(path, "rb") as f:
return private_key.sign(f.read())
def verify_file(public_key, path: str, signature: bytes) -> bool:
with open(path, "rb") as f:
try:
public_key.verify(signature, f.read())
return True
except InvalidSignature:
return False
The publisher runs sign_file with the private key they keep offline; every downloader runs verify_file with the widely published public key. If even one byte of the download was altered in transit or on a compromised mirror, verification returns False. This is the mechanism behind the package-signing examples below.
Signature vs Encryption vs MAC#
| Tool | Key type | Who can verify | Gives non-repudiation? |
|---|---|---|---|
| Encryption | Public/private | N/A (hides data) | No |
| HMAC (MAC) | Shared secret | Anyone with the secret | No |
| Digital signature | Public/private | Anyone with the public key | Yes |
Reach for a signature when a third party must be able to verify authorship without holding a shared secret, for example verifying a software release you downloaded. Reach for an HMAC when both ends already share a secret and non-repudiation does not matter, such as signing your own webhook payloads.
PKI: Turning a Key Into an Identity#
A signature proves a message came from a particular key, but how do you know that key belongs to the person or domain you think it does? That binding is the job of Public Key Infrastructure (PKI). A Certificate Authority (CA) signs a certificate that binds a public key to an identity (a domain name, an organisation, a person). To trust example.com, your browser walks the chain: the site's leaf certificate is signed by an intermediate, which is signed by a root the browser already trusts.
The value is scalable trust: you pre-trust a small set of roots, each CA vouches for the identities it certifies, and the whole web leans on those decisions. This same X.509 machinery powers the TLS handshake. It also explains why a stolen CA key or a fraudulently issued certificate is so serious: it lets an attacker forge a trusted identity to everyone at once.
Where Signatures Are Used Every Day#
The same pattern (artifact + signature + a trusted public key) appears throughout computing:
- Code signing: operating systems verify that an application was signed by a known developer and not tampered with since. An unsigned or modified binary triggers a warning or is blocked.
- Software updates and packages: apt, npm, PyPI, container registries and OS update channels verify signatures so a compromised mirror cannot serve a malicious build. This is a core defence in supply chain security.
- Email authentication (DKIM, S/MIME): the sending mail server signs outgoing mail so receivers can verify the sending domain, cutting down spoofing and phishing.
- Documents and transactions: PDF signatures, signed commits in Git, and blockchain transactions all rely on the same primitive.
The Lifetime of Trust: Revocation and Expiry#
A signature is a statement about a moment: this key was valid and its holder authorised this artifact. When a private key leaks, everything it signed becomes suspect and the certificate must be revoked before its natural expiry. PKI handles this with CRLs (revocation lists) and OCSP (real-time status checks), covered in the TLS lesson. The modern trend is toward short-lived certificates (such as 90-day web certificates renewed automatically), which shrink the window a leaked key is useful and reduce reliance on revocation working perfectly.
For code signing specifically, timestamping matters: a trusted timestamp records when something was signed, so a signature made while the certificate was valid remains verifiable even after the certificate later expires, which is essential for software that stays installed for years.
Verifying a Signature Yourself#
You will most often meet signatures when verifying a download. The workflow with GnuPG:
# Import the project's public key (obtained from a trusted source)
gpg --import project-release-key.asc
# Verify a detached signature against the file
gpg --verify installer.tar.gz.sig installer.tar.gz
# "Good signature from ..." means the file is intact and signed by that key
Two cautions. First, "Good signature" only means the file matches the signature made by that key; you still need confidence the key genuinely belongs to the project (verify its fingerprint through an independent channel). Second, a warning that the key is "not certified with a trusted signature" is about your local trust settings, not the file's integrity. The recurring security question across all of PKI is simply: who can issue for this identity, and how would anyone notice if that issuance were fraudulent?
Check your understanding
4 quick questions on this lesson. Get them all right and the lesson is marked complete.