The cryptography library in Python can do more than just encryption; it can also provide cryptographic integrity and authenticity through digital signatures.
Let’s see how it works with a quick example. Imagine you have a sensitive message and you want to ensure that:
- No one can read it if intercepted (confidentiality).
- You can prove you sent it and that it hasn’t been tampered with (authenticity and integrity).
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key
from cryptography.hazmat.backends import default_backend
# --- Key Generation ---
# Generate a private/public key pair for signing
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
public_key = private_key.public_key()
# Generate a Fernet key for symmetric encryption
fernet_key = Fernet.generate_key()
cipher_suite = Fernet(fernet_key)
# --- Data Preparation ---
original_message = b"This is a secret message that needs to be signed and encrypted."
# --- Encryption (Symmetric) ---
encrypted_message = cipher_suite.encrypt(original_message)
# --- Signing (Asymmetric) ---
# Hash the *encrypted* message to create a digest
hasher = hashes.Hash(hashes.SHA256(), backend=default_backend())
hasher.update(encrypted_message)
digest = hasher.finalize()
# Sign the digest with the private key
signature = private_key.sign(
digest,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print(f"Original Message: {original_message}")
print(f"Encrypted Message: {encrypted_message}")
print(f"Signature: {signature.hex()}") # Displaying as hex for readability
# --- Verification and Decryption (Recipient's Side) ---
# The recipient needs the public key and the fernet key, and the signature.
# 1. Verify the signature
# Hash the received encrypted message
verifier_hasher = hashes.Hash(hashes.SHA256(), backend=default_backend())
verifier_hasher.update(encrypted_message)
verifier_digest = verifier_hasher.finalize()
try:
public_key.verify(
signature,
verifier_digest,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print("\nSignature is valid!")
# 2. Decrypt the message (only if signature is valid)
decrypted_message = cipher_suite.decrypt(encrypted_message)
print(f"Decrypted Message: {decrypted_message}")
except Exception as e: # In a real app, catch specific InvalidSignature exception
print(f"\nSignature verification failed: {e}")
This example highlights the core problem: how do you ensure both secrecy and provenance? You can encrypt data to keep it secret, but how do you prove who sent it and that it hasn’t been altered in transit? Digital signatures provide this.
The system works by combining two distinct cryptographic approaches: symmetric encryption for confidentiality and asymmetric encryption for authenticity.
- Symmetric Encryption (Confidentiality): For the actual message content, we use
cryptography.fernet. Fernet is a high-level API that provides symmetric authenticated encryption. It uses a single secret key (fernet_key) to both encrypt and decrypt data. This is fast and efficient for large amounts of data. Thecipher_suiteobject handles this. - Asymmetric Encryption (Authenticity & Integrity): For proving the message’s origin and integrity, we use public-key cryptography, specifically RSA in this example.
- Key Pair: A
private_keyand a correspondingpublic_keyare generated. The private key is kept secret by the sender, while the public key can be freely distributed. - Hashing: Before signing, the data to be signed is first hashed. A hash function (like SHA256) creates a fixed-size "fingerprint" of the data. If even a single bit of the data changes, the hash will change drastically. This ensures that we’re signing a representation of the data, not the data itself, which is more efficient.
- Signing: The sender uses their
private_keyto "sign" this hash (digest). This signature is a mathematical transformation of the digest using the private key. - Verification: The recipient uses the sender’s
public_keyto verify the signature. They re-hash the received data and then use the public key to check if the signature mathematically corresponds to the re-calculated hash. If it does, the recipient knows two things:- Integrity: The data hasn’t been altered since it was signed (because the hashes match).
- Authenticity: Only the holder of the corresponding private key could have created this signature, proving it came from the claimed sender.
- Key Pair: A
In our example, we sign the encrypted message. This means the signature attests to the integrity and authenticity of the ciphertext. If the ciphertext is altered, the signature verification will fail. If the signature is valid, we can then proceed to decrypt.
The critical part of the signing process is the choice of padding scheme. padding.PSS (Probabilistic Signature Scheme) is the modern, recommended standard for RSA signatures, offering stronger security guarantees against certain types of attacks compared to older schemes like PKCS#1 v1.5 padding. The salt_length=padding.PSS.MAX_LENGTH ensures the salt used in PSS is as large as possible, further enhancing security.
The most surprising thing about this dual approach is that the signature is applied after encryption. This might seem counterintuitive; you might think you’d sign the original message and then encrypt the signed message. However, signing the ciphertext proves the integrity of the encrypted payload. If the encryption itself were compromised or tampered with (e.g., a bit flip in the ciphertext that changes the decrypted message), the signature verification on the ciphertext would fail, preventing the decryption of corrupted data.
If you were to try and decrypt a message where the signature verification failed, you would likely hit an InvalidSignature exception when calling public_key.verify.