mrkeyoor.com_
Sat 19 Sept 15:48 UTC
PyPISecurityupdated 19 Sept 2026

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.

Verdict

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

Lab card: what happened when we installed cryptographyScreenshot of cryptography documentation
Install✓ · 0.4s3 packages on disk · 16 MB
Importimport cryptography in 0.02s · compiled extensions · py.typed · requires Python >=3.9, !=3.9.0, !=3.9.1
Known vulns0(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.

API stability4/5Version 50 puts X.509 verification under the project's documented API stability policy, while Fernet, AEAD classes, serialization loaders, and certificate objects retain familiar shapes. A 4 fits because deprecation can end in removal: older aliases left in version 49, DER loaders now reject additional malformed forms, and all finite-field Diffie-Hellman interfaces are deprecated.
Docs5/5The official manual draws a visible line between recipes and hazardous-material primitives, then documents installation, exceptions, migrations, and private security reporting. Its 50.0 changelog identifies CVE-2026-69247, lists the DER encodings that became errors, explains the Diffie-Hellman deprecation, and links the Cobblestone contract. That is enough detail to plan and test an upgrade.
Maintenance5/5GitHub showed an unarchived repository pushed on August 26, 2026, with 37 open issues and pull requests. The maintainers followed 50.0.0 from July 31 with 50.0.1 on August 25 to move published wheels to OpenSSL 4.0.2. The README also sends vulnerability reports through a private security channel rather than asking reporters to open a public issue.
Ecosystem5/5The stored registry snapshot records 348,550,681 weekly downloads, and GitHub showed 7,724 stars during this rewrite. PyPI links directly to source, issues, documentation, and the changelog. The same key, certificate, and serialization objects appear throughout Python networking and security integrations, while PyNaCl and pyOpenSSL remain available for narrower libsodium or OpenSSL-wrapper needs.

Discussed on

  1. hnThe State of OpenSSL for pyca/cryptography220 points
  2. hnDependency on rust removes support for a number of platforms49 points
  3. hnPython/cryptography – Dependency on rust removes support for number of platforms18 points

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 if

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-ready

The 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 mismatch

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

PackageRegistryPick it when
pycryptodomePyPIPick it when compatibility with a legacy cipher or an existing Crypto namespace matters more than pyca object compatibility.
pynaclPyPIPick it for libsodium boxes, signing, and secret streams when certificates and general ASN.1 work are outside the project.
pyOpenSSLPyPIKeep 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.