pyOpenSSL vs cryptography Python比較 2026

最近、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をラップするだけの薄い層に過ぎず、間にレイヤーを挟む意味が薄い

比較すると、以下のようになります。

項目pyOpenSSLcryptography
最新バージョン(2026年6月)26.3.049.0.0
月間PyPI DL数約2.6億約12.6億
依存関係cryptography + typing-extensionscffi + 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 GitHub警告メッセージ

まだ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」という名称通り、使い方を誤ると脆弱な実装になるリスクがある
cryptography library architecture diagram

新規実装では、まず高レベル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 移行ガイド

関数マッピング表

pyOpenSSLcryptography相当備考
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_utcdatetimeで返るため変換不要
cert.get_subject().CNcert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].valueOID指定が明確になる
X509Req()(CSR作成)x509.CertificateSigningRequestBuilder()同様のビルダーパターン
Python migration guide code refactoring

移行時のハマりポイント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実装