Skillversitas

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 cryptography API lets you pass a pre-digested value via prehashed when 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.