Digital Signatures
A digital signature is RSA's other half: the private key "signs" a message, and anyone with the public key can verify it. Signatures provide authenticity (only the key holder could have produced this) and integrity (any change breaks verification), and - unlike MACs - they provide non-repudiation, because the signer's private key cannot be shared with the verifier.
Signing with PSS
PKCS#1 v1.5 signatures work but have subtle malleability; PSS (Probabilistic Signature Scheme) is the modern default, with a random salt on every signing:
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
message = b"invoice #42 - total 900 USD"
signature = private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
),
hashes.SHA256(),
)
# verification - with the PUBLIC key only
public_key.verify(signature, message, padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
), hashes.SHA256()) # raises InvalidSignature if anything is off
Notice the asymmetry: private_key.sign(), public_key.verify(). The verifier needs no secret, which is what makes signatures scalable - one signature, any number of checkers.
What You Actually Sign
Signatures are computed over a digest, not over the message bytes directly - the hash inside sign(msg, ..., hashes.SHA256()) is the RSA input. Consequences:
- You can sign data larger than the key's modulus limit (the hash is fixed-size).
- The signature binds the exact bytes hashed; sign the canonical form (the exact JSON you transmit, the exact file bytes) or verification of "the same content" will fail.
- To sign a file, hash it in chunks (as in the hashing chapter) and feed the digest - the
cryptographyAPI lets you pass a pre-digested value viaprehashedwhen needed, or just sign the file bytes directly for moderate sizes.
A Practical Verification Flow
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
from cryptography.exceptions import InvalidSignature
def verify_release(public_pem: bytes, tarball: bytes, sig: bytes) -> bool:
pub = rsa.RSAPublicNumbers.from_public_key(
serialization.load_pem_public_key(public_pem))
try:
pub.verify(sig, tarball, padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
), hashes.SHA256())
return True
except InvalidSignature:
return False
This is the shape of software-update checks: the release tarball, its signature, and the project's public key (bundled or fetched over HTTPS) - three inputs, one boolean.
MAC vs Signature, Again
Use HMAC when both sides share a secret and only they need to verify. Use signatures when the verifier is a third party, when the public key can be distributed without risk, or when the sender must be able to point at a proof they produced. They are not interchangeable in protocols, even though both "check that the data is from X."
Summary
Sign with private_key.sign using PSS-SHA256, verify with public_key.verify, and sign the exact canonical bytes you want bound. Signatures are the tool for updates, certificates, and any flow where verification is public and the signer must be provable.