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

bash
pip install cryptography

Output: (none — exits 0 on success)

bash
uv add cryptography

Output: resolved + added to pyproject.toml

bash
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.x series 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+ on 42.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 cryptography you 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

PackageTrade-off
pynacllibsodium bindings — high-level NaCl primitives (XChaCha20-Poly1305, Curve25519). Safer-by-default API but no X.509.
pycryptodomePure-Python Crypto.* namespace; legacy pycrypto successor. Use only if you cannot install cryptography (rare).
argon2-cffiArgon2 password hashing — purpose-built for the one job cryptography does NOT cover well.
bcryptJust bcrypt password hashing; commonly installed alongside cryptography.
tinkGoogle'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

  1. Fernet keys MUST be Fernet.generate_key() output — base64-encoded 32 random bytes. Passing arbitrary strings fails with binascii.Error or, worse, succeeds with a weak key derived from your input.
  2. InvalidToken raised 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).
  3. Hazmat APIs require explicit backend=default_backend() on older releases. Newer (42+) makes it optional; older calls still need it.
  4. Padding is NOT automatic for raw AES-CBC. Wrap with cryptography.hazmat.primitives.padding.PKCS7(...). Forgetting this is a top-3 bug pattern.
  5. encrypt on RSA is hybrid-only in practice. Direct RSA encryption is limited to (modulus-size – padding). Use OAEP padding and wrap a symmetric key, never plaintext.
  6. load_pem_private_key(password=...) expects bytes, not str. Encode to UTF-8 first.
  7. x509.CertificateBuilder requires subject_name + issuer_name + public_key + serial_number + not_valid_before + not_valid_after BEFORE sign(). Forgetting any one raises ValueError.
  8. 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.

python
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.

python
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).

python
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.

python
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).

python
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 cryptography bump, not a system patch. Subscribe to PyCA's security announcements.
  • Audit allowed primitives. Set up CI lint rules (bandit, custom AST checks) that flag hashes.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 Ed25519 over RSA for new signatures — 100× faster sign, much smaller keys, no padding to misconfigure.
  • Reuse Fernet and Cipher instances 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 AESGCM from cryptography.hazmat.primitives.ciphers.aead for 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 — dropped OpenSSL 1.1.0; bundled OpenSSL 3.0. Some legacy ciphers (RC4) removed.
  • 41.x → 42.xdefault_backend() argument is optional everywhere; the function is still importable but no longer needed.
  • 42.x → 43.x — minor API tightening on x509.CertificateBuilder; SHA-1 signatures emit DeprecationWarning in cert issuance.
python
# 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_digest or cryptography.hazmat.primitives.constant_time.bytes_eq for 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) or bcrypt — not raw PBKDF2 or Scrypt rolled by hand.
  • Random sources. os.urandom and secrets use the OS CSPRNG. Never seed crypto with random.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.crypto and 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.
python
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 the ssl context built from cryptography-issued certs transparently.
  • pyOpenSSL — legacy bridge; new code should use cryptography directly.
  • paramiko — SSH client; uses cryptography for primitives since 3.x.
  • pyjwt — JWT signing/verification; the cryptography backend handles RS256/ES256/Ed25519.
  • josepy / acme — Let's Encrypt client stack built on cryptography.
  • pyca/bcrypt — separate package, same maintainers.
  • certbot — the reference Let's Encrypt client uses cryptography for cert generation.

Compatibility matrix

Pythoncryptography lineNotes
3.741.x and earlierEOL'd; CVE-only patches stopped.
3.842.xLast release for 3.8 in some matrices.
3.942.x+Current floor for 43.x.
3.1042.x+Supported.
3.1142.x+Best perf via faster bigint.
3.1242.x+Supported.
3.1343.x+GIL builds only; free-threaded experimental.

Troubleshooting common errors

Error / SymptomLikely causeFix
InvalidToken from Fernet.decryptWrong key, expired ttl, or tampered ciphertextVerify key version; check ttl= argument; treat as auth failure.
ValueError: Invalid key — must be 32 url-safe base64-encoded bytesHand-built key from password or stringUse Fernet.generate_key(); if password-derived, run through Scrypt first then base64-encode.
cryptography.exceptions.InvalidSignatureWrong public key or message tamperedVerify which key signed; check canonicalization.
OSError: dlopen ... libssl.so on LinuxSource-built without bundled OpenSSLReinstall via a manylinux wheel: pip install --only-binary :all: cryptography.
error: can't find Rust compilerSource build on unsupported platformInstall Rust toolchain or use a prebuilt wheel.
Slow startup on first importBundled OpenSSL self-testOne-time; cache the Python process.
UnsupportedAlgorithmBackend doesn't support the named curve / cipherUpgrade cryptography; some legacy primitives are removed.

When NOT to use this

  • Password storage — use argon2-cffi or bcrypt. Don't roll Scrypt by hand.
  • High-level token formats — JWT? Use pyjwt. PASETO? Use paseto.
  • Pure side-effect-free hashing of non-secret data — stdlib hashlib is sufficient.
  • Embedded / restricted runtimes — the wheel is large (~5-8 MB). For micro-runtimes consider pynacl or tink.
  • 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.

python
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).

python
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.

python
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.

python
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):

python
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.

python
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