cryptography review
Cryptography 50.0.0 loaded in 0.02 seconds in our Python 3.12 sandbox and brought compiled extensions plus typed Python APIs. It covers Fernet tokens, authenticated encryption, signatures, key serialization, certificate work, and lower-level primitives. The 50.0 line stabilizes X.509 verification, introduces the Cobblestone streaming recipe, retires finite-field Diffie-Hellman, rejects more malformed DER, and fixes the PKCS7 oracle described in CVE-2026-69247. Release 50.0.1 changes the bundled OpenSSL in published wheels to 4.0.2.
Cryptography 50.0.0 installed in 0.4 seconds, occupied 16 MB across 3 packages, and produced 0 pip-audit findings in our sandbox. Install it for certificates, standard key formats, or its named recipes; use the standard library for plain hashes and seek protocol review before reaching for hazmat primitives.
We installed it
| Install | ✓ · 0.4s | 3 packages on disk · 16 MB |
| Import | ✓ | import cryptography in 0.02s · compiled extensions · py.typed · requires Python >=3.9, !=3.9.0, !=3.9.1 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does cryptography install cleanly?
Yes. In a fresh container with an empty cache, pip install cryptography finished in 0.4s, leaving 3 packages and 16 MB on disk. pip-audit reported no known vulnerabilities.
What does cryptography need to run?
Python >=3.9, !=3.9.0, !=3.9.1, and a platform wheel with compiled extensions. In our run import cryptography succeeded in 0.02s, and the package ships py.typed for type checkers.
cryptography or pycryptodome: which should you use?
Pick pycryptodome when compatibility with a legacy cipher or an existing Crypto namespace matters more than pyca object compatibility. Cryptography 50.0.0 installed in 0.4 seconds, occupied 16 MB across 3 packages, and produced 0 pip-audit findings in our sandbox.
When should you not use cryptography?
Skip it for password storage alone. argon2-cffi and bcrypt present password-focused interfaces, while this package makes your code choose KDF parameters and storage policy.
Discussed on
Use it if
- Install it when Python code must issue or inspect certificates, CSRs, CRLs, or OCSP messages through one object model.
- Use it for documented Fernet or Cobblestone message formats instead of designing encryption framing in application code.
- Choose it when an interoperability requirement names AES-GCM, Ed25519, RSA-PSS, HKDF, PEM, or DER.
- Keep it when another package already exposes pyca key and certificate objects, since converting those objects adds little value.
- Skip it for password storage alone. argon2-cffi and bcrypt present password-focused interfaces, while this package makes your code choose KDF parameters and storage policy.
- Do not adopt the hazmat layer if the team cannot review nonce reuse, padding, key rotation, and algorithm selection. The API permits combinations that are syntactically valid and unsafe for a protocol.
- Current releases are a dead end for Intel Macs and 32-bit Windows because wheel support for both targets ended in version 49.
- Avoid the 50.x upgrade until finite-field Diffie-Hellman callers can migrate. The full DH type and key-loading surface is deprecated.
- Use Python's hashlib or hmac when byte hashing is the entire requirement and no caller needs cryptography's algorithm objects.
Setup reality
We installed cryptography 50.0.0 in a clean Python 3.12 Bookworm container in 0.4 seconds. The environment ended with 3 packages using 16 MB, and import cryptography completed in 0.02 seconds. pip-audit reported 0 known vulnerabilities. The wheel has 3 direct dependencies, compiled .so files, and py.typed; it requires Python 3.9 while excluding 3.9.0 and 3.9.1. No license value appeared in the measured package metadata.
Published wheels hide most build work. A source install needs the Rust and C toolchains plus Python, libffi, and OpenSSL headers, according to the installation guide. Check that pip can select a wheel before debugging a compiler log. Version 49 ended Intel macOS wheels and 32-bit Windows support. Release 50.0.1 rebuilds Windows, macOS, and Linux wheels with OpenSSL 4.0.2.
Keys remain an application responsibility. Store them away from code, retain the salt and cost with password-derived material, and define rotation before encrypting durable records. Fernet tokens reveal their creation timestamp. An AEAD nonce may travel beside ciphertext, but repeating one under the same key breaks the security contract. Verification and decryption signal bad input with exceptions, so a catch block must fail closed.
Version 50 deprecates every finite-field Diffie-Hellman path and parses several DER structures more strictly. Test old certificates, OCSP data, and serialized keys during the upgrade. Cobblestone supplies chunked authenticated encryption for large messages, but its framing belongs to that recipe and cannot be mixed with an arbitrary stream format. Stable X.509 verification APIs still require the caller to provide trust roots, time, and hostname policy.
Patterns
Encrypt a compact value with Fernet 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.decrypt raises InvalidToken when authentication fails, the token is too old, or the supplied key differs. Keep that key outside the token store.
Apply an age limit to a Fernet token token-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 visible inside every Fernet token. A 60-second TTL also assumes the producer and verifier clocks agree closely enough.
Seal bytes with AES-GCM 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")The 12-byte nonce can be stored with the ciphertext, but one key must never see that nonce twice. Decryption also requires identical associated data.
Turn a password into Fernet key material 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-readyThe record needs its 16-byte salt and 1,200,000-iteration setting for later derivation. A PBKDF2HMAC object is single use after derive or verify.
Feed a SHA-256 digest in chunks hash-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()Hashlib can calculate the same SHA-256 digest without this dependency. This object form fits code that already passes cryptography algorithm instances.
Create and verify an HMAC tag hmac-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 mismatchHMAC.verify performs the constant-time comparison and raises InvalidSignature on a mismatch. Either verify or finalize consumes that HMAC instance.
Write an RSA private key as encrypted PEM rsa-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"),
)BestAvailableEncryption encrypts the PKCS8 payload with the provided passphrase. Storing that passphrase beside the PEM defeats the protection.
Sign and verify with RSA-PSS rsa-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(),
)RSA verify returns None on success and raises InvalidSignature otherwise. The verifier must use the same PSS and SHA-256 choices as the signer.
Use Ed25519 without padding options ed25519-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")Ed25519 does not ask the caller to select a digest or padding scheme. Any changed message or signature makes verify raise InvalidSignature.
Inspect a certificate subject and expiry x509-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)not_valid_after_utc returns a timezone-aware datetime. The older property without the utc suffix is deprecated because it returns a naive value.
Build a self-signed certificate for tests x509-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 check the SubjectAlternativeName extension for example.com. Setting only the common name is insufficient for current hostname validation.
Open an encrypted PEM private key load-private-key
from cryptography.hazmat.primitives.serialization import load_pem_private_key
private_key = load_pem_private_key(
pem_bytes,
password=key_password,
)Use None only when the PEM has no encryption. A malformed key or incorrect password reaches the caller as ValueError.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pycryptodome | PyPI | Pick it when compatibility with a legacy cipher or an existing Crypto namespace matters more than pyca object compatibility. |
| pynacl | PyPI | Pick it for libsodium boxes, signing, and secret streams when certificates and general ASN.1 work are outside the project. |
| pyOpenSSL | PyPI | Keep it for an older integration that explicitly consumes OpenSSL connection or context wrappers. |
More security guides
pyjwt · jose · dompurify · requests-oauthlib · jsonwebtoken · oauthlib · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

