mrkeyoor.com_
Wed 05 Aug 05:02 UTC
PyPISecurityupdated 05 Aug 2026

cryptography

cryptography is the de facto cryptographic standard library for Python, maintained by the Python Cryptographic Authority (pyca). It has two layers: high-level recipes with safe defaults (Fernet for symmetric encryption) and a low-level layer literally named hazmat with primitives such as AES-GCM, RSA, Ed25519, key derivation functions, HMAC, and a full X.509 certificate toolkit. The heavy lifting happens in Rust and OpenSSL bindings, so it is fast and memory-safe where it matters. It supports Python 3.9+ and PyPy, and almost every Python package that touches TLS or certificates depends on it.

Verdict

If Python code needs real cryptography, this is the default and it earns it: audited primitives, honest documentation, and relentless maintenance. Stay on the recipes layer unless you know exactly why you are in hazmat.

API stability4/5A documented deprecation cycle and very stable core APIs, but the aggressive major cadence drops Python/OpenSSL versions often, and property renames like not_valid_after to not_valid_after_utc require periodic code touches.
Docs5/5cryptography.io documents every primitive with working examples, explicit danger warnings on the hazmat layer, and a changelog that spells out each deprecation and removal.
Maintenance5/5Maintained by the pyca team with commits daily (pushed today), only 31 open issues and PRs, a published security policy, and memory-unsafe paths steadily rewritten in Rust.
Ecosystem5/5351M weekly downloads and it underpins much of Python's TLS and SSH stack: pyOpenSSL, paramiko, certbot, and countless AWS and HTTP libraries depend on it.

Use it if

  • You need symmetric encryption with sane defaults and zero decisions: Fernet gives you authenticated encryption, key generation, and TTL-based expiry in four lines
  • You work with X.509: loading, inspecting, building, or signing certificates and CSRs is a first-class, well-documented API here
  • You need standard primitives (AES-GCM, ChaCha20-Poly1305, RSA, Ed25519, HKDF, PBKDF2) implemented over OpenSSL with a Python API that names its footguns
  • You want the library the rest of the ecosystem already trusts: paramiko, pyOpenSSL, certbot, and most TLS tooling build on it
Skip it if

Setup reality

pip install cryptography is painless on Linux/macOS/Windows because binary wheels cover the common platforms. The pain starts off the paved road: musl-based images with outdated pip, FreeBSD, or unusual CPU architectures compile from source, which needs Rust, a C compiler, and OpenSSL development headers, and the error messages are long. The version number moves fast and each major prunes support for old OpenSSL and Python releases, so a requirements file pinned two years ago often will not install on a new box. Imports are verbose by design: expect five-line import blocks from cryptography.hazmat.primitives before you encrypt anything.

Patterns

Encrypt and decrypt with Fernet (the safe default)symmetric-encrypt

from cryptography.fernet import Fernet

key = Fernet.generate_key()  # store this somewhere safe
f = Fernet(key)
token = f.encrypt(b"A really secret message.")
plaintext = f.decrypt(token)

Fernet is authenticated encryption with all choices made for you; decrypt raises InvalidToken on tampering or a wrong key.

Time-limited tokens with Fernet TTLtoken-expiry

import time
from cryptography.fernet import Fernet

f = Fernet(Fernet.generate_key())
token = f.encrypt(b"session-payload")

# rejects tokens older than 60 seconds
f.decrypt(token, ttl=60)

The timestamp is embedded at encrypt time and checked against the current clock, so clock skew between machines breaks TTL validation.

AES-GCM authenticated encryption (hazmat)aes-gcm

import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

key = AESGCM.generate_key(bit_length=256)
aesgcm = AESGCM(key)
nonce = os.urandom(12)  # 96-bit nonce, NEVER reuse with the same key
ct = aesgcm.encrypt(nonce, b"secret data", b"associated data")
pt = aesgcm.decrypt(nonce, ct, b"associated data")

Nonce reuse with the same key destroys both confidentiality and integrity; store the nonce alongside the ciphertext, it is not secret.

Derive an encryption key from a password (PBKDF2)derive-key-password

import base64, os
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

salt = os.urandom(16)  # store with the ciphertext
kdf = PBKDF2HMAC(
    algorithm=hashes.SHA256(),
    length=32,
    salt=salt,
    iterations=1_200_000,
)
key = base64.urlsafe_b64encode(kdf.derive(b"my password"))  # Fernet-ready

KDF instances are single-use; calling derive twice raises AlreadyFinalized. Scale iterations to your latency budget per current guidance.

Compute a SHA-256 digesthash-digest

from cryptography.hazmat.primitives import hashes

digest = hashes.Hash(hashes.SHA256())
digest.update(b"abc")
digest.update(b"123")  # incremental updates allowed
result = digest.finalize()

For plain hashing, stdlib hashlib is equally fine and dependency-free; use this API when the rest of your crypto already lives here.

Sign and verify a message with HMAChmac-verify

from cryptography.hazmat.primitives import hashes, hmac

h = hmac.HMAC(key, hashes.SHA256())
h.update(b"message to authenticate")
signature = h.finalize()

# verification (constant-time):
h2 = hmac.HMAC(key, hashes.SHA256())
h2.update(b"message to authenticate")
h2.verify(signature)  # raises InvalidSignature on mismatch

verify() is constant-time; never compare MACs with ==, and remember each HMAC object is consumed by finalize/verify.

Generate an RSA key and export encrypted PEMrsa-generate-serialize

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa

key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
pem = key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.BestAvailableEncryption(b"passphrase"),
)

Use NoEncryption() only for keys that live in a real secret store; loading back is serialization.load_pem_private_key(pem, password=b"passphrase").

Sign and verify with RSA-PSSrsa-sign-verify

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding

message = b"important payload"
signature = private_key.sign(
    message,
    padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),
    hashes.SHA256(),
)

public_key = private_key.public_key()
public_key.verify(  # raises InvalidSignature on failure
    signature,
    message,
    padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),
    hashes.SHA256(),
)

verify() returns None on success and raises on failure; do not write if verify(...) checks. Prefer PSS over PKCS1v15 for new signatures.

Modern signatures with Ed25519ed25519-sign

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

private_key = Ed25519PrivateKey.generate()
signature = private_key.sign(b"my authenticated message")

public_key = private_key.public_key()
public_key.verify(signature, b"my authenticated message")

No padding or hash choices to get wrong, which is exactly why Ed25519 is the right default for new signature designs.

Load and inspect an X.509 certificatex509-inspect-cert

from cryptography import x509

cert = x509.load_pem_x509_certificate(pem_bytes)
print(cert.subject.rfc4514_string())
print(cert.not_valid_after_utc)  # timezone-aware datetime
print(cert.serial_number)

Use the _utc properties (not_valid_after_utc); the naive-datetime originals are deprecated and warn on modern versions.

Create a self-signed certificatex509-self-signed

import datetime
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec

key = ec.generate_private_key(ec.SECP256R1())
name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "example.com")])
now = datetime.datetime.now(datetime.timezone.utc)

cert = (
    x509.CertificateBuilder()
    .subject_name(name)
    .issuer_name(name)
    .public_key(key.public_key())
    .serial_number(x509.random_serial_number())
    .not_valid_before(now)
    .not_valid_after(now + datetime.timedelta(days=365))
    .add_extension(x509.SubjectAlternativeName([x509.DNSName("example.com")]), critical=False)
    .sign(key, hashes.SHA256())
)

Browsers validate the SubjectAlternativeName extension, not the common name; a cert without SAN fails in every modern client.

Alternatives

PackageRegistryPick it when
pycryptodomePyPIYou need algorithms pyca/cryptography refuses to ship or a self-contained build with no OpenSSL dependency
pynaclPyPIYou want libsodium's tiny, hard-to-misuse API (secretbox, box, sign) and do not need X.509 or OpenSSL compatibility
bcryptPyPIThe only thing you are doing is hashing passwords; same pyca maintainers, far smaller surface