cheat sheet
cryptography
Package-level reference for the cryptography library on PyPI — install, version policy, Fernet, asymmetric keys, X.509, and TLS contexts.
cryptography
What it is
cryptography is the de-facto Python interface to modern cryptographic primitives, maintained by the Python Cryptographic Authority (PyCA). It exposes two layers: a small "recipes" API (Fernet, password hashing helpers) that's safe by default, and a much larger "hazmat" layer (cryptography.hazmat.primitives) that gives direct access to AES, RSA, ECDSA, Ed25519, X25519, HKDF, KDFs, and X.509 — the "hazardous materials" naming is deliberate, signalling that misuse is dangerous.
Reach for cryptography when you need: symmetric encryption of tokens or files, asymmetric signing/verification, key derivation from passwords, X.509 certificate issuance, or anything below the TLS handshake that ssl / requests already do for you. For password storage specifically, prefer argon2-cffi or passlib over rolling your own with hazmat KDFs.
Install
pip install cryptography
Output: (none — exits 0 on success)
uv add cryptography
Output: resolved + added to pyproject.toml
poetry add cryptography
Output: updated lockfile + virtualenv install
The wheel ships statically-linked OpenSSL on Linux/macOS/Windows, so you do not need a system libcrypto. Source builds require a Rust toolchain (since the 35.0 release, the backend is partially Rust) and OpenSSL headers.
Versioning & Python support
- Current line is the
42.x/43.xseries in 2025-26. Major-version bumps happen yearly; the project follows a strict deprecation policy with a one-year warning window. - Supports Python 3.7+ on
41.x, Python 3.8+ on42.x, and Python 3.9+ on the latest release as of mid-2026. - Two-Year LTS line for OpenSSL: each major bumps the bundled OpenSSL to the latest stable. Always run the latest
cryptographyyou can — CVE patches land here first. - Pin the major version in apps (
cryptography>=42,<44) and re-evaluate at major bumps.
Package metadata
- Maintainer: Python Cryptographic Authority (PyCA)
- Project home: github.com/pyca/cryptography
- Docs: cryptography.io
- PyPI: pypi.org/project/cryptography
- License: Apache-2.0 OR BSD-3-Clause (dual-licensed)
- Governance: PyCA stewards, corporate sponsorship from PSF and several vendors
- First released: 2013
- Downloads: hundreds of millions per month; near-universal transitive dep across Python TLS / auth tooling
Optional dependencies & extras
cryptography has no PyPI extras. Its hard runtime deps are:
cffi>=1.12— the Python ↔ C bridge for OpenSSL surfaces still implemented in C.- A bundled libssl/libcrypto inside the wheel (no system OpenSSL required on the platforms shipping wheels).
There are NO runtime hooks for Argon2, bcrypt, scrypt-with-yescrypt, or libsodium — install those packages separately (argon2-cffi, bcrypt, pynacl) if you need them.
Alternatives
| Package | Trade-off |
|---|---|
pynacl | libsodium bindings — high-level NaCl primitives (XChaCha20-Poly1305, Curve25519). Safer-by-default API but no X.509. |
pycryptodome | Pure-Python Crypto.* namespace; legacy pycrypto successor. Use only if you cannot install cryptography (rare). |
argon2-cffi | Argon2 password hashing — purpose-built for the one job cryptography does NOT cover well. |
bcrypt | Just bcrypt password hashing; commonly installed alongside cryptography. |
tink | Google's higher-level "use-this-and-don't-think" crypto API; smaller Python footprint. |
ssl (stdlib) | TLS sockets only — no primitives, no key generation, no X.509 issuance. |
Common gotchas
- Fernet keys MUST be
Fernet.generate_key()output — base64-encoded 32 random bytes. Passing arbitrary strings fails withbinascii.Erroror, worse, succeeds with a weak key derived from your input. InvalidTokenraised on decrypt is the only signal of corruption or wrong key. Catch it; don't assume the ciphertext was modified by an attacker (could be a key rotation issue).- Hazmat APIs require explicit
backend=default_backend()on older releases. Newer (42+) makes it optional; older calls still need it. - Padding is NOT automatic for raw AES-CBC. Wrap with
cryptography.hazmat.primitives.padding.PKCS7(...). Forgetting this is a top-3 bug pattern. encrypton RSA is hybrid-only in practice. Direct RSA encryption is limited to (modulus-size – padding). UseOAEPpadding and wrap a symmetric key, never plaintext.load_pem_private_key(password=...)expects bytes, not str. Encode to UTF-8 first.x509.CertificateBuilderrequiressubject_name+issuer_name+public_key+serial_number+not_valid_before+not_valid_afterBEFOREsign(). Forgetting any one raisesValueError.- Free-threaded Python (3.13t) is not fully supported. GIL-disabled builds work but are not audited; pin to GIL builds in production.
Real-world recipes
The recipes below cover the most common production needs — symmetric encryption of secrets, RSA signing, password-based KDF, X.509 issuance, and TLS context building. All snippets are copy-pasteable; substitute your own keys before deploying.
Recipe 1 — Fernet symmetric encrypt + decrypt for short-lived tokens.
from cryptography.fernet import Fernet
key = Fernet.generate_key() # save securely; never hard-code
f = Fernet(key)
token = f.encrypt(b"session=abc123;user=42")
plaintext = f.decrypt(token) # raises InvalidToken on tamper / wrong key
print(plaintext.decode())
Output: session=abc123;user=42 — Fernet wraps AES-128-CBC + HMAC-SHA256 + URL-safe base64.
Recipe 2 — RSA keypair: generate, sign, verify.
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization
priv = rsa.generate_private_key(public_exponent=65537, key_size=3072)
pub = priv.public_key()
sig = priv.sign(b"payload", padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), hashes.SHA256())
pub.verify(sig, b"payload", padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), hashes.SHA256())
Output: (no return value; verify raises InvalidSignature on mismatch). 3072-bit RSA is the modern recommendation; use Ed25519 for new designs.
Recipe 3 — Password-based key derivation with scrypt (use this, not raw PBKDF2 for new code).
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
import os
salt = os.urandom(16)
kdf = Scrypt(salt=salt, length=32, n=2**15, r=8, p=1)
key = kdf.derive(b"correct horse battery staple")
print(len(key), salt.hex())
Output: 32 <hex-salt> — 32-byte symmetric key derived; store the salt alongside the resulting ciphertext.
Recipe 4 — Self-signed X.509 certificate for dev TLS.
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
import datetime as dt
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "myhost.local")])
cert = (x509.CertificateBuilder()
.subject_name(name).issuer_name(name)
.public_key(key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(dt.datetime.utcnow())
.not_valid_after(dt.datetime.utcnow() + dt.timedelta(days=90))
.add_extension(x509.SubjectAlternativeName([x509.DNSName("myhost.local")]), critical=False)
.sign(key, hashes.SHA256()))
open("cert.pem", "wb").write(cert.public_bytes(serialization.Encoding.PEM))
Output: cert.pem written — usable for local HTTPS dev servers (do NOT use self-signed in production).
Recipe 5 — TLS context with mTLS client auth (server-side).
import ssl
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ctx.load_cert_chain("server.pem", "server-key.pem")
ctx.load_verify_locations("client-ca.pem")
ctx.verify_mode = ssl.CERT_REQUIRED
# `cryptography` issued the cert above; `ssl` consumes the PEM normally.
Output: an ssl.SSLContext requiring valid client cert signed by client-ca.pem.
Production deployment notes
- Pin major versions. Roll forward minors aggressively for CVE patches; pin majors so the Rust toolchain requirement doesn't surprise CI.
- Wheels are the happy path. Manylinux/musllinux/macOS-universal2/Windows wheels exist for every supported Python; if you see a source build kick off, your platform is missing or your pip is outdated.
- Bundled OpenSSL means CVEs in OpenSSL require a
cryptographybump, not a system patch. Subscribe to PyCA's security announcements. - Audit allowed primitives. Set up CI lint rules (
bandit, custom AST checks) that flaghashes.MD5,hashes.SHA1,AES.MODE_ECB, and bare-RSA-without-OAEP — all easy mistakes the library lets you make. - Key material lifecycle. Never log Fernet keys, private-key PEMs, or AES keys; use a secrets manager (Vault, AWS KMS, GCP KMS) and only materialize bytes inside the request path.
Performance tuning
- Use
Ed25519over RSA for new signatures — 100× faster sign, much smaller keys, no padding to misconfigure. - Reuse
FernetandCipherinstances per key. Constructor cost is small but amortizes when encrypting many small messages. - Batch HMAC verification. Constructing a new HMAC per call wastes setup; reuse with
.copy()for streaming. - Hazmat AES-GCM beats AES-CBC + HMAC, both in CPU and in code-correctness. Use
AESGCMfromcryptography.hazmat.primitives.ciphers.aeadfor new code. - Rust backend is faster on big payloads — released versions since 41+ moved the AEAD inner loops out of Python's overhead.
Version migration guide
< 35.0— pre-Rust era. Upgrade as soon as practical; older releases lack modern CVE fixes.35.0 → 36.0— Rust toolchain now required for source builds. Wheel users unaffected.39.x → 40.x— droppedOpenSSL 1.1.0; bundledOpenSSL 3.0. Some legacy ciphers (RC4) removed.41.x → 42.x—default_backend()argument is optional everywhere; the function is still importable but no longer needed.42.x → 43.x— minor API tightening onx509.CertificateBuilder; SHA-1 signatures emitDeprecationWarningin cert issuance.
# Old (35.x and earlier)
from cryptography.hazmat.backends import default_backend
priv = rsa.generate_private_key(65537, 3072, backend=default_backend())
# Current (42+)
priv = rsa.generate_private_key(65537, 3072)
Output: identical key; explicit-backend form is legacy.
Security considerations
cryptography is correct if you use it correctly. The library doesn't stop you from picking bad primitives — that's the point of the hazmat split.
- Use Fernet for tokens, AES-GCM for files, Ed25519 for new signatures, OAEP for RSA encryption (rare), PSS for RSA signatures. Avoid: MD5, SHA-1 (in signatures), AES-ECB, RSA-PKCS1v15-encrypt, raw-DH without authentication.
- Constant-time comparison. Use
hmac.compare_digestorcryptography.hazmat.primitives.constant_time.bytes_eqfor any signature/HMAC comparison. - CVE hygiene. Subscribe to PyCA security advisories; this package has high CVE blast radius.
- Don't reinvent password storage.
argon2-cffi(Argon2id) orbcrypt— not rawPBKDF2orScryptrolled by hand. - Random sources.
os.urandomandsecretsuse the OS CSPRNG. Never seed crypto withrandom.random(). - Side-channel surface. The library mitigates timing leaks for HMAC and signature verification, but you can still leak via your own conditional branches on secret data.
Testing & CI integration
- Mark cryptographic tests as
@pytest.mark.cryptoand gate slow KDF tests behind an env var. - Use fixed test vectors from RFC test suites where possible; freezing-output unit tests reveal accidental algorithm changes.
- For end-to-end TLS tests, generate a fresh self-signed cert per test session and clean it up in a fixture.
- Don't write tests that print secrets; pytest captures stdout and CI logs are searchable.
import pytest
from cryptography.fernet import Fernet, InvalidToken
def test_fernet_roundtrip():
f = Fernet(Fernet.generate_key())
assert f.decrypt(f.encrypt(b"x")) == b"x"
def test_fernet_tamper_detected():
f = Fernet(Fernet.generate_key())
tok = bytearray(f.encrypt(b"x"))
tok[-1] ^= 1
with pytest.raises(InvalidToken):
f.decrypt(bytes(tok))
Output: both tests pass; tamper detection verified.
Ecosystem integrations
requests/httpx/urllib3— all consume thesslcontext built fromcryptography-issued certs transparently.pyOpenSSL— legacy bridge; new code should usecryptographydirectly.paramiko— SSH client; usescryptographyfor primitives since 3.x.pyjwt— JWT signing/verification; thecryptographybackend handles RS256/ES256/Ed25519.josepy/acme— Let's Encrypt client stack built oncryptography.pyca/bcrypt— separate package, same maintainers.certbot— the reference Let's Encrypt client usescryptographyfor cert generation.
Compatibility matrix
| Python | cryptography line | Notes |
|---|---|---|
| 3.7 | 41.x and earlier | EOL'd; CVE-only patches stopped. |
| 3.8 | 42.x | Last release for 3.8 in some matrices. |
| 3.9 | 42.x+ | Current floor for 43.x. |
| 3.10 | 42.x+ | Supported. |
| 3.11 | 42.x+ | Best perf via faster bigint. |
| 3.12 | 42.x+ | Supported. |
| 3.13 | 43.x+ | GIL builds only; free-threaded experimental. |
Troubleshooting common errors
| Error / Symptom | Likely cause | Fix |
|---|---|---|
InvalidToken from Fernet.decrypt | Wrong key, expired ttl, or tampered ciphertext | Verify key version; check ttl= argument; treat as auth failure. |
ValueError: Invalid key — must be 32 url-safe base64-encoded bytes | Hand-built key from password or string | Use Fernet.generate_key(); if password-derived, run through Scrypt first then base64-encode. |
cryptography.exceptions.InvalidSignature | Wrong public key or message tampered | Verify which key signed; check canonicalization. |
OSError: dlopen ... libssl.so on Linux | Source-built without bundled OpenSSL | Reinstall via a manylinux wheel: pip install --only-binary :all: cryptography. |
error: can't find Rust compiler | Source build on unsupported platform | Install Rust toolchain or use a prebuilt wheel. |
| Slow startup on first import | Bundled OpenSSL self-test | One-time; cache the Python process. |
UnsupportedAlgorithm | Backend doesn't support the named curve / cipher | Upgrade cryptography; some legacy primitives are removed. |
When NOT to use this
- Password storage — use
argon2-cffiorbcrypt. Don't roll Scrypt by hand. - High-level token formats — JWT? Use
pyjwt. PASETO? Usepaseto. - Pure side-effect-free hashing of non-secret data — stdlib
hashlibis sufficient. - Embedded / restricted runtimes — the wheel is large (~5-8 MB). For micro-runtimes consider
pynaclortink. - You don't know which primitive you need — read a primer first; misuse is worse than a slow library.
Worked example: encrypted secrets file with key rotation
A worked end-to-end pattern most teams need at some point — encrypted secrets on disk, with a documented rotation path that doesn't require re-issuing the underlying keys to consumers.
Step 1 — generate a master key, write it to a sealed file.
from cryptography.fernet import Fernet
import pathlib, os
key = Fernet.generate_key()
p = pathlib.Path("/etc/myapp/key")
p.write_bytes(key)
os.chmod(p, 0o600)
Output: /etc/myapp/key with mode 0600 — readable only by the owning service account.
Step 2 — encrypt a payload (use Fernet on small secrets; AES-GCM for large blobs).
from cryptography.fernet import Fernet, MultiFernet
import pathlib
current = Fernet(pathlib.Path("/etc/myapp/key").read_bytes())
token = current.encrypt(b'{"db_password":"hunter2"}')
pathlib.Path("/etc/myapp/secrets.enc").write_bytes(token)
Output: secrets.enc written; only the holder of /etc/myapp/key can decrypt.
Step 3 — rotate keys with MultiFernet so old ciphertexts still decrypt.
from cryptography.fernet import Fernet, MultiFernet
old = Fernet(b"<previous key>")
new = Fernet.generate_key()
mf = MultiFernet([Fernet(new), old]) # ordered: new first, old second
old_token = b"<existing ciphertext>"
fresh_token = mf.rotate(old_token) # re-encrypts under `new`
plain = mf.decrypt(fresh_token)
Output: mf.rotate re-wraps the ciphertext with the new key while leaving plaintext intact; readers tolerate both keys until full rotation completes.
Step 4 — destroy the old key after every consumer has rotated.
import os, secrets
# Overwrite-then-delete pattern. Filesystem may COW; rely on KMS for hard guarantees.
with open("/etc/myapp/old-key", "ba+") as f:
f.write(secrets.token_bytes(32))
os.unlink("/etc/myapp/old-key")
Output: old key file is gone; ciphertexts encrypted only with it are now permanently unreadable. The takeaway: build rotation in from day one — retrofitting a rotation flow into a system that didn't plan for it is painful.
FAQ
Q: Should I use Fernet or AES-GCM for new code?
A: AES-GCM (cryptography.hazmat.primitives.ciphers.aead.AESGCM) for anything large or performance-sensitive. Fernet is fine for short tokens — its overhead per message is small and the API is harder to misuse.
Q: I need to interoperate with a JS client that uses Web Crypto. Which primitives align? A: AES-GCM, HMAC-SHA-256, ECDSA-P256, Ed25519, X25519, HKDF-SHA-256, PBKDF2 — all line up with Web Crypto. RSA-OAEP also works. Avoid: scrypt (not in Web Crypto), Fernet (Python-specific).
Q: How do I store an RSA private key with a passphrase?
A: serialization.BestAvailableEncryption(passphrase_bytes):
from cryptography.hazmat.primitives import serialization
pem = priv.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.BestAvailableEncryption(b"correct horse"),
)
Output: PEM bytes with PKCS8 + a strong KDF; load back with load_pem_private_key(pem, password=b"correct horse").
Q: My CI hangs on key generation in containers — why?
A: Some minimal containers have a thin /dev/urandom. Confirm with cat /proc/sys/kernel/random/entropy_avail. Mount --device /dev/urandom from the host or upgrade to a base image with proper entropy support.
Q: Is FIPS mode supported?
A: cryptography's bundled OpenSSL is NOT a FIPS-certified build. For FIPS compliance you must build against a FIPS-validated OpenSSL — see the project's FIPS docs and be prepared for a non-trivial environment setup.
AES-GCM end-to-end pattern
The recipe most apps need but don't realize they need until later — authenticated symmetric encryption of files or message blobs.
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = AESGCM.generate_key(bit_length=256)
gcm = AESGCM(key)
nonce = os.urandom(12) # 96-bit nonce — NEVER reuse with same key
aad = b"context=user-42" # associated data (auth'd but not encrypted)
ct = gcm.encrypt(nonce, b"secret body", aad)
pt = gcm.decrypt(nonce, ct, aad) # raises InvalidTag on any mismatch
print(pt, len(ct))
Output: b'secret body' <len> — AEAD = encryption + integrity in one call. Store (nonce, ct) together; the aad is reconstructed by the consumer.
Critical: never reuse a nonce with the same key. The recommended pattern is a 96-bit counter or random 12-byte nonce per message; with random nonces, key rotation is needed after roughly 2^32 messages to keep collision probability low.
See also
- Concept: API — designing APIs that handle secrets safely
- Concept: HTTP — context for TLS and cert issuance