
最近、Pythonセキュリティ界隈で「cryptography」への移行が加速して、話題になっていますね!どうやら、「cryptography」を使うと、pyOpenSSLと比べてモダンなAPIで安全な暗号処理を実装できるようです!
そこで今回は、バックエンドエンジニアの視点で「pyOpenSSL vs cryptography」の機能・保守状況・コード比較を行ってみました!どちらを選べば迷っている方でも判断できる内容ですので、記事を読んで実装の参考にしてみてください!
この記事で分かること
- 2026年時点でpyOpenSSLとcryptographyどちらを選ぶべきか
- RSA鍵生成・自己署名証明書・PEM読み込みの両ライブラリ実装コード
- pyOpenSSL → cryptography移行時のハマりポイントと関数マッピング
結論:2026年の推奨はcryptography一択
新規プロジェクトでPython暗号処理ライブラリを選ぶなら、cryptography一択。理由は明確だ。
- pyOpenSSLの公式が移行を推奨:pyCA(Python Cryptographic Authority)自身が「可能な場合はpyca/cryptographyを使え」とREADMEで明記している
- ダウンロード数が5倍近い差:月間DL数はcryptographyが約12.6億回に対し、pyOpenSSLは約2.6億回(2026年6月時点)
- pyOpenSSLはcryptographyに依存している:現在のpyOpenSSLはcryptographyをラップするだけの薄い層に過ぎず、間にレイヤーを挟む意味が薄い
比較すると、以下のようになります。
| 項目 | pyOpenSSL | cryptography |
|---|---|---|
| 最新バージョン(2026年6月) | 26.3.0 | 49.0.0 |
| 月間PyPI DL数 | 約2.6億 | 約12.6億 |
| 依存関係 | cryptography + typing-extensions | cffi + typing-extensions |
| 高レベルAPI | なし(低レベルのみ) | あり(Fernet等) |
| 低レベルAPI | あり(OpenSSL直接) | あり(hazmat) |
| 公式の推奨スタンス | 「cryptographyを使え」と自ら案内 | 積極的にメンテ継続中 |
| 推奨用途 | レガシー維持・OCSP等特殊用途のみ | 新規開発すべて |
pyOpenSSLの現状(2026年)
メンテ状況と非推奨の経緯
pyOpenSSLは2000年代初頭から存在するOpenSSLのPythonラッパーだ。長らく唯一の選択肢だったが、2013年にpyCA(Python Cryptographic Authority)がcryptographyライブラリを公開し、状況が変わった。
決定的だったのは、pyOpenSSL自身がcryptographyに依存する形に設計変更された点だ。現在のpyOpenSSL 26.x系は内部でcryptographyを呼び出すラッパーに過ぎず、pyCAのGitHubでは長年「Note: The Python Cryptographic Authority strongly suggests the use of pyca/cryptography where possible」という警告を掲示している。2026年3月にはバッファオーバーフロー脆弱性(v22.0.0〜v26.0.0未満)が報告・修正されており、セキュリティ対応はかろうじて続いている状態だ。
まだpyOpenSSLを使うべき場面
とはいえ、以下のケースではpyOpenSSLが現役だ。
- レガシーコードの維持:既存プロジェクトがpyOpenSSLに深く依存しており、リファクタコストが高い場合
- OCSPステープリングの操作:標準ssl/cryptographyでは直接扱えないOpenSSL固有の低レベル機能を使う場合
- Scrapyなど依存ライブラリが要求する場合:Scrapy・mitmproxy・azure-cli-coreなど一部ライブラリが間接依存として引き込む
cryptographyライブラリの概要
cryptographyライブラリは高レベルAPI(Recipes layer)と低レベルAPI(Hazardous Materials / hazmat layer)の2層構造を持つ。
- 高レベルAPI(Fernet等):安全なデフォルト値が組み込まれており、暗号の知識が浅くても誤用しにくい設計。対称暗号化にはFernetを使えばAES-128-CBC + HMAC-SHA256が自動で適用される
- 低レベルAPI(hazmat):
from cryptography.hazmat.primitives import ...でアクセスする。RSA・ECDSA・AES-GCM等を細かく制御できるが、「Hazardous Materials」という名称通り、使い方を誤ると脆弱な実装になるリスクがある
新規実装では、まず高レベルAPIで実現できるか検討し、どうしても必要な場合のみhazmatに降りるのが原則だ。
同じ処理を両ライブラリで書く
事前インストール
まず両ライブラリをインストールする。
pip install cryptography pyOpenSSL
RSA鍵ペアの生成
2048ビットのRSA秘密鍵と公開鍵を生成する処理。出力はPEM形式のバイト列になる。
pyOpenSSL版
# ファイル名: rsa_keygen_pyopenssl.py
from OpenSSL.crypto import PKey, TYPE_RSA, dump_privatekey, dump_publickey, FILETYPE_PEM
# 2048ビットRSA鍵ペアを生成
key = PKey()
key.generate_key(TYPE_RSA, 2048)
# PEM形式でエクスポート
private_pem = dump_privatekey(FILETYPE_PEM, key)
public_pem = dump_publickey(FILETYPE_PEM, key)
print(private_pem.decode())
# 出力: -----BEGIN PRIVATE KEY-----\nMIIEvg...(PEMブロック)
cryptography版
# ファイル名: rsa_keygen_cryptography.py
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
# 2048ビットRSA鍵ペアを生成
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
# PEM形式でエクスポート
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
public_pem = private_key.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
print(private_pem.decode())
# 出力: -----BEGIN RSA PRIVATE KEY-----\nMIIEow...(PEMブロック)
cryptography版はpublic_exponent=65537を明示的に指定するため、安全でない指数を誤って使うリスクがない。pyOpenSSL版はデフォルトが65537だが、コードから読み取れない。
自己署名証明書の作成
開発環境・mTLS・内部CA用途で頻繁に使う自己署名証明書の生成。両者でAPIの思想の違いが顕著に出る。
pyOpenSSL版
# ファイル名: selfsigned_pyopenssl.py
from OpenSSL.crypto import (
PKey, TYPE_RSA, X509, X509Req,
dump_certificate, FILETYPE_PEM
)
import datetime
key = PKey()
key.generate_key(TYPE_RSA, 2048)
cert = X509()
cert.get_subject().C = "JP"
cert.get_subject().ST = "Aichi"
cert.get_subject().O = "Example Corp"
cert.get_subject().CN = "example.local"
cert.set_serial_number(1)
# 有効期間: now ~ +365日
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(365 * 24 * 60 * 60)
cert.set_issuer(cert.get_subject()) # 自己署名 = issuer==subject
cert.set_pubkey(key)
cert.sign(key, "sha256")
pem = dump_certificate(FILETYPE_PEM, cert)
print(pem.decode())
# 出力: -----BEGIN CERTIFICATE-----\nMIICpD...(PEMブロック)
cryptography版
# ファイル名: selfsigned_cryptography.py
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
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "JP"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Aichi"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Example Corp"),
x509.NameAttribute(NameOID.COMMON_NAME, "example.local"),
])
cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(private_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.datetime.now(datetime.timezone.utc))
.not_valid_after(datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=365))
.sign(private_key, hashes.SHA256())
)
pem = cert.public_bytes(serialization.Encoding.PEM)
print(pem.decode())
# 出力: -----BEGIN CERTIFICATE-----\nMIICpD...(PEMブロック)
cryptography版はメソッドチェーンのビルダーパターンで可読性が高い。pyOpenSSL版はgmtime_adj_notBeforeのような非直感的な関数名が多く、コードレビューで意図が伝わりにくい。
既存PEM証明書の読み込みと情報表示
外部から取得したPEM証明書を解析し、有効期限・Subject・シリアル番号を表示する処理。
pyOpenSSL版
# ファイル名: parse_cert_pyopenssl.py
from OpenSSL.crypto import load_certificate, FILETYPE_PEM
with open("server.crt", "rb") as f:
pem_data = f.read()
cert = load_certificate(FILETYPE_PEM, pem_data)
subject = cert.get_subject()
print(f"CN: {subject.CN}")
print(f"O : {subject.O}")
print(f"Serial: {cert.get_serial_number()}")
# 日付はASN1形式のバイト列として返ってくるため文字列変換が必要
not_after_raw = cert.get_notAfter()
print(f"NotAfter (raw): {not_after_raw.decode()}")
# 出力例:
# CN: example.local
# O : Example Corp
# Serial: 1234567890
# NotAfter (raw): 20270628000000Z
cryptography版
# ファイル名: parse_cert_cryptography.py
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.backends import default_backend
with open("server.crt", "rb") as f:
pem_data = f.read()
cert = x509.load_pem_x509_certificate(pem_data, default_backend())
cn = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
org = cert.subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)[0].value
print(f"CN: {cn}")
print(f"O : {org}")
print(f"Serial: {cert.serial_number}")
print(f"NotAfter: {cert.not_valid_after_utc}") # datetimeオブジェクトで返る
# 出力例:
# CN: example.local
# O : Example Corp
# Serial: 1234567890
# NotAfter: 2027-06-28 00:00:00+00:00
cryptography版のnot_valid_after_utcはPythonのdatetimeオブジェクトで返るため、日付計算がそのまま行える。pyOpenSSL版はASN.1形式のバイト列(20270628000000Z)で返るため、datetime.strptimeでの変換が別途必要になる。
pyOpenSSL → cryptography 移行ガイド
関数マッピング表
| pyOpenSSL | cryptography相当 | 備考 |
|---|---|---|
PKey().generate_key(TYPE_RSA, 2048) | rsa.generate_private_key(65537, 2048) | public_exponentの明示が必要 |
X509() + .sign() | x509.CertificateBuilder().sign() | ビルダーパターンに変わる |
dump_privatekey(FILETYPE_PEM, key) | key.private_bytes(Encoding.PEM, ...) | フォーマット指定が明示的 |
dump_certificate(FILETYPE_PEM, cert) | cert.public_bytes(Encoding.PEM) | シンプル化 |
load_certificate(FILETYPE_PEM, data) | x509.load_pem_x509_certificate(data) | backend引数は3.x以降省略可 |
cert.get_notAfter() | cert.not_valid_after_utc | datetimeで返るため変換不要 |
cert.get_subject().CN | cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value | OID指定が明確になる |
X509Req()(CSR作成) | x509.CertificateSigningRequestBuilder() | 同様のビルダーパターン |
移行時のハマりポイント3つ
ハマりポイント1:default_backend()の扱い
cryptography 3.x以降、default_backend()の引数は不要になったが、古いドキュメントやStack Overflowのサンプルには残っている。渡しても動作はするが、DeprecationWarningが出ることがある。
# 旧(3.x以前の書き方)
from cryptography.hazmat.backends import default_backend
cert = x509.load_pem_x509_certificate(data, default_backend())
# 新(3.x以降)
cert = x509.load_pem_x509_certificate(data)
ハマりポイント2:not_valid_afterのtimezone問題
cryptography 42.0以降、cert.not_valid_after(naive datetime)はdeprecatedになり、cert.not_valid_after_utc(timezone-aware datetime)が正式になった。pyOpenSSLのget_notAfter()から移行する際にtzinfo処理を追加し忘れると、比較演算でエラーになる。
import datetime
# 有効期限チェック(正しい書き方)
now = datetime.datetime.now(datetime.timezone.utc)
if cert.not_valid_after_utc < now:
print("証明書は期限切れです")
ハマりポイント3:シリアル番号の自動生成
pyOpenSSLではcert.set_serial_number(1)と固定値を設定するサンプルが多いが、cryptographyのx509.random_serial_number()はRFC 5280準拠のランダム値を生成する。本番CAを実装する場合は必ずランダム生成を使うこと。固定値のシリアル番号はCAとして複数証明書を発行した際に衝突する。
# NG: 固定値はRFC 5280違反になる可能性
builder.serial_number(1)
# OK: ランダム生成を使う
builder.serial_number(x509.random_serial_number())
関連記事:Python OpenSSL完全ガイド
関連記事:Python mTLS実装