python ssl 自己署名証明書 自作 ローカル環境の構築、OpenSSLコマンドを使わずPythonだけで完結させたいと思ったことはありませんか?実は cryptography ライブラリ一本で、証明書生成からHTTPSサーバー起動まで全部Pythonで賄えます!
そこで今回は、Python 3.10以上の環境で「cryptography ライブラリ(42.x系)」を使ったローカルHTTPS環境の完全セットアップを実際に手を動かしながら解説します!コードはそのままコピペで動く形式にまとめていますので、ぜひ皆さんも記事を読んで試してみてください!
この記事で分かること
-
>cryptography ライブラリ(42.x)でRSA秘密鍵と自己署名証明書をPythonだけで生成する方法
>SAN(Subject Alternative Name)付き証明書でlocalhost・127.0.0.1両対応にする手順
>FlaskでHTTPSサーバーを起動し、requestsやcurlで接続確認する具体的なコード
なぜローカル開発にHTTPSが必要なのか
「開発環境はhttpで十分でしょ」と思いがちですが、実はHTTPSが必須な機能が年々増えています。本番デプロイ前日に初めてhttpsが必要と気づくと詰みます。
python ssl 自己署名証明書 自作 ローカル環境が必要になる代表的なケースはこちらです:
-
>Service Worker:PWAのオフラインキャッシュやプッシュ通知はhttpsコンテキスト必須
>Cookie の SameSite=None:クロスサイトCookieはhttps上でないと発行されない
>WebAuthn / Passkeys:生体認証APIはSecure Contextでのみ動作
>Geolocation API・Camera API:ブラウザがhttp環境では許可ダイアログを出さない
>HTTP/2・HSTS:本番と同じプロトコル環境で開発しないと挙動差が出る
特にService WorkerはChrome・Firefoxともにlocalhostを「安全なオリジン」として例外扱いしますが、localhost以外のローカルIPやカスタムドメイン(例:de>dev.myapp.local)を使い始めた瞬間に例外が外れます。「開発段階でhttpsを使わないと本番で詰む」という状況は、フロントエンドとバックエンドを別ポートで開発しているモノレポ構成では特に頻繁に発生します。
OpenSSLコマンドに頼らない理由
WindowsではOpenSSLのパスが通っていないことも多く、チームメンバーの環境差異が生まれます。Pythonスクリプト一本にまとめれば、de>pip install cryptography だけで誰でも同じ証明書生成環境を再現できます。
cryptographyライブラリが最適な理由
Pythonで証明書生成を行うライブラリは複数ありますが、現在のデファクトスタンダードは de>cryptography です。後述のforgeとの比較表も参照ください。
SSL証明書の基本構造をPythonで理解する
コードを書く前に証明書の正体を理解しておくと、後の実装でつまずきません。
証明書の構造(テキスト図解)
de class="language-text">┌──────────────────────────────────────────────┐
│ X.509 Certificate │
├──────────────────────────────────────────────┤
│ Subject : CN=localhost, O=MyOrg, C=JP │ ← 「誰のものか」
│ Issuer : CN=localhost, O=MyOrg, C=JP │ ← 「誰が発行したか」(自己署名は同じ)
│ 公開鍵 : RSA 2048-bit Public Key │ ← 暗号化に使う鍵(公開)
│ 有効期限 : 2025-01-01 〜 2026-01-01 │ ← Validity Period
│ SAN : DNS:localhost, IP:127.0.0.1 │ ← Subject Alternative Name
├──────────────────────────────────────────────┤
│ 署名 : SHA-256(上記全体) by 秘密鍵 │ ← 改ざん検知のデジタル署名
└──────────────────────────────────────────────┘
↑
秘密鍵(サーバーだけが持つ)と対になっている
証明書とは「公開鍵+メタ情報+署名」の塊です。自己署名証明書(オレオレ証明書)は、発行者と所有者が同一であるため、第三者CA(認証局)による信頼チェーンがありません。ローカル開発限定で使う分には問題ありません。
cryptographyライブラリのインストール
cryptography ライブラリはPyCA(Python Cryptographic Authority)が管理するRustバックエンドの暗号ライブラリです。42.x以降でAPIの安定化が進んでいます。
de class="language-bash"># cryptographyライブラリをインストール(42.x系を指定) pip install "cryptography>=42.0.0"
forgeとcryptographyの比較
Node.js由来の de>python-forge(de>forge)とPyCAの de>cryptography はどちらもPythonで証明書を扱えますが、用途が異なります。
| 項目 | cryptography(PyCA) | python-forge |
|---|---|---|
| メンテナンス状況 | ✅ 活発(42.x〜50.x継続更新) | ⚠️ 更新が少ない |
| バックエンド | Rust (OpenSSL / BoringSSL) | Pure Python |
| パフォーマンス | 高速(Rustネイティブ) | 遅い(純Pythonのため) |
| X.509 SAN対応 | ✅ ネイティブAPI | △ 手動ASN.1操作が必要 |
| インストール容易さ | pip一発(バイナリ配布あり) | pip一発だが依存が重い |
| ドキュメント | 充実(公式チュートリアルあり) | 少ない |
| 本番利用実績 | PyOpenSSL・Let's Encrypt等 | 限定的 |
| Python 3.10+対応 | ✅ 完全対応 | △ 一部API非推奨 |
結論として、新規プロジェクトでは cryptography 一択です。forgeは既存コードとの互換性維持が必要なケースにのみ使います。
秘密鍵と自己署名証明書を生成する(cryptographyライブラリ)
ここからが本題です。順番通りに実行すれば de>server.key.pem と de>server.cert.pem の2ファイルが生成されます。
手順1:RSA-2048秘密鍵の生成
秘密鍵とは「サーバーだけが持つ、絶対に外に出してはいけない乱数ベースのデータ」です。RSA-2048は現時点で十分な強度を持ち、ほぼすべてのクライアントライブラリが対応しています。
de class="language-python"># ファイル名: generate_cert.py
import datetime
import ipaddress
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
# ─────────────────────────────────────────────
# Step 1: RSA-2048 秘密鍵を生成する
# public_exponent=65537 は RFC 推奨値(速度と安全性のバランスが良い)
# ─────────────────────────────────────────────
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
# 秘密鍵を PEM 形式でファイルに保存(パスフレーズなし)
with open("server.key.pem", "wb") as f:
f.write(
private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(), # ローカル開発なので暗号化なし
)
)
print("[OK] 秘密鍵を server.key.pem に保存しました")
手順2:Subject・Issuer・有効期限の設定
証明書のメタ情報(Subject)は「この証明書が誰のものか」を示すラベルです。自己署名なのでIssuerとSubjectは同じ値になります。
de class="language-python"># ─────────────────────────────────────────────
# Step 2: Subject / Issuer の設定(generate_cert.py の続き)
# NameOID.COMMON_NAME = 証明書の「通称」。ブラウザのアドレスバーに表示されることがある
# ─────────────────────────────────────────────
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "JP"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Aichi"),
x509.NameAttribute(NameOID.LOCALITY_NAME, "Nagoya"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "LocalDev"),
x509.NameAttribute(NameOID.COMMON_NAME, "localhost"),
])
# 有効期限: 現在時刻から365日後(datetime.timezone.utc を使うことで deprecation 警告を回避)
now = datetime.datetime.now(datetime.timezone.utc)
valid_from = now
valid_to = now + datetime.timedelta(days=365)
手順3:SAN付き自己署名証明書の生成と保存
SAN(Subject Alternative Name)とは、証明書が有効なホスト名やIPアドレスを明示するフィールドです。Chrome 58以降、SANがない証明書は無条件に拒否されます。de>localhost と de>127.0.0.1 両方を登録しておくのがポイントです。
de class="language-python"># ─────────────────────────────────────────────
# Step 3: SAN付き自己署名証明書を生成(generate_cert.py の続き)
# CertificateBuilder はメソッドチェーンで設定を積み上げる設計
# ─────────────────────────────────────────────
cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(private_key.public_key()) # 対応する公開鍵を埋め込む
.serial_number(x509.random_serial_number()) # シリアル番号はランダム生成推奨
.not_valid_before(valid_from)
.not_valid_after(valid_to)
# SAN: DNS名とIPアドレスの両方を登録する
.add_extension(
x509.SubjectAlternativeName([
x509.DNSName("localhost"),
x509.DNSName("*.localhost"), # ワイルドカード(任意)
x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")),
x509.IPAddress(ipaddress.IPv6Address("::1")),
]),
critical=False, # SAN は non-critical が標準
)
# BasicConstraints: CA=False で「末端証明書」であることを示す
.add_extension(
x509.BasicConstraints(ca=False, path_length=None),
critical=True,
)
# KeyUsage: デジタル署名と鍵合意を許可
.add_extension(
x509.KeyUsage(
digital_signature=True,
key_encipherment=True,
content_commitment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=False,
crl_sign=False,
encipher_only=False,
decipher_only=False,
),
critical=True,
)
# ExtendedKeyUsage: TLS サーバーとして使うことを明示
.add_extension(
x509.ExtendedKeyUsage([x509.oid.ExtendedKeyUsageOID.SERVER_AUTH]),
critical=False,
)
.sign(private_key, hashes.SHA256()) # 秘密鍵でSHA-256署名
)
# 証明書を PEM 形式でファイルに保存
with open("server.cert.pem", "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
print("[OK] 証明書を server.cert.pem に保存しました")
print(f" 有効期限: {valid_from.date()} 〜 {valid_to.date()}")
Pythonのrequestsから自己署名証明書を信頼させる
証明書が生成できたら、Pythonクライアント側の設定です。
手順1:verify=Falseの何が問題か
ネット上のサンプルに de>verify=False が多いですが、これは証明書の検証を完全スキップするため、中間者攻撃(MITM)に対して無防備になります。開発環境のみでも、誤ってこのコードを本番にコピーするリスクがあります。必ず de>InsecureRequestWarning が出る理由を理解した上で使いましょう。
de class="language-python"># ファイル名: client_bad_example.py(やってはいけない例)
import requests
import urllib3
# NG: 証明書検証を完全無効化 → 中間者攻撃を検知できない
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
try:
response = requests.get("https://localhost:5443/", verify=False)
print(response.text)
except requests.exceptions.RequestException as e:
print(f"[ERROR] {e}")
手順2:生成した証明書をverify=に渡す正しい方法
de>verify= に証明書ファイルのパスを渡すと、requestsはそのCAバンドルを信頼します。自己署名証明書の場合、その証明書自体がルートCAになるため、de>server.cert.pem を直接指定します。
de class="language-python"># ファイル名: client_correct.py(正しい書き方)
import requests
CERT_PATH = "server.cert.pem" # 生成した自己署名証明書のパス
try:
# verify= に証明書パスを渡すことで、その証明書を「信頼するCA」として扱う
response = requests.get(
"https://localhost:5443/",
verify=CERT_PATH,
timeout=5,
)
print(f"[OK] ステータスコード: {response.status_code}")
print(f"[OK] レスポンス: {response.text}")
except requests.exceptions.SSLError as e:
print(f"[SSL ERROR] {e}")
except requests.exceptions.ConnectionError as e:
print(f"[CONNECTION ERROR] サーバーが起動しているか確認してください: {e}")
手順3:cert=(クライアント証明書)との違い
de>verify= はサーバー証明書の検証に使う設定です。de>cert= はまったく別物で、クライアント証明書認証(mTLS)でクライアント自身の証明書と秘密鍵を提示するために使います。
de class="language-python"># cert= はクライアント証明書認証(mTLS)用。verify= とは役割が違う
response = requests.get(
"https://localhost:5443/",
verify="server.cert.pem", # サーバー証明書の検証用CA
cert=("client.cert.pem", "client.key.pem"), # クライアント自身の証明書と秘密鍵
timeout=5,
)
FlaskでHTTPSサーバーを立ててcurlで接続確認する
証明書と秘密鍵が揃ったので、FlaskでHTTPSサーバーを立ち上げます。
手順1:ssl_contextを使ったFlask HTTPSサーバー
https ローカル開発の動作確認には、Flaskの開発サーバーのssl_context引数が最も手軽です。本番はgunicorn+nginxが推奨ですが、開発目的ではこれで十分です。
de class="language-python"># ファイル名: flask_https_server.py
import ssl
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/")
def index():
return jsonify({"status": "ok", "message": "HTTPS ローカルサーバーが動いています!"})
@app.route("/health")
def health():
return jsonify({"healthy": True})
if __name__ == "__main__":
# ssl.SSLContext を使って証明書と秘密鍵を読み込む
ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
try:
ssl_ctx.load_cert_chain(
certfile="server.cert.pem", # 自己署名証明書
keyfile="server.key.pem", # RSA秘密鍵
)
except FileNotFoundError as e:
print(f"[ERROR] 証明書ファイルが見つかりません: {e}")
print(" → generate_cert.py を先に実行してください")
raise SystemExit(1)
print("[INFO] HTTPS サーバー起動: https://localhost:5443/")
app.run(
host="0.0.0.0",
port=5443,
ssl_context=ssl_ctx,
debug=False, # デバッグモードはssl_contextと相性が悪いのでFalse推奨
)
手順2:curlコマンドで接続確認
curlの de>--cacert オプションに自己署名証明書を渡すと、信頼済みCAとして扱われます。
de class="language-bash"># --cacert で自己署名証明書をCAとして信頼させる curl --cacert server.cert.pem https://localhost:5443/ # 詳細なSSLハンドシェイクを確認したい場合 curl -v --cacert server.cert.pem https://localhost:5443/ # 証明書の内容をOpenSSLコマンドで確認(インストール済みの場合) openssl x509 -in server.cert.pem -text -noout | grep -A 5 "Subject Alternative"
手順3:ブラウザに証明書をインポートする手順(OS別)
ブラウザでアクセスするには de>server.cert.pem を「信頼するルートCA」としてOSのキーストアに登録します。
-
>Windows:de>server.cert.pem をダブルクリック → 「証明書のインポート」→ 「ローカルコンピューター」→ 「信頼されたルート証明機関」に格納
>macOS:de>security add-trusted-cert -d -r trustRoot -k ~/Library/Keychains/login.keychain-db server.cert.pem をターミナルで実行
>Linux (Ubuntu/Debian):de>sudo cp server.cert.pem /usr/local/share/ca-certificates/local-dev.crt && sudo update-ca-certificates
>Chrome独自ストア(Linux):de>chrome://settings/certificates → 「認証局」タブ → インポート → 「ウェブサイトの識別にこの認証局を信頼する」にチェック
よくあるエラーと解決策
実際に動かすと必ずどれかに引っかかります。原因と対処をまとめました。
SSLError: CERTIFICATE_VERIFY_FAILED
最も頻繁に遭遇するエラーです。原因はほぼ3パターンに絞られます。
de class="language-python"># ファイル名: debug_ssl.py — SSLエラーの原因を診断するスクリプト
import ssl
import socket
def diagnose_ssl(host: str, port: int, ca_cert: str) -> None:
"""SSL接続を診断して問題を特定する"""
ctx = ssl.create_default_context(cafile=ca_cert)
try:
with socket.create_connection((host, port), timeout=5) as sock:
with ctx.wrap_socket(sock, server_hostname=host) as ssock:
cert = ssock.getpeercert()
print(f"[OK] 接続成功")
print(f" 証明書のSAN: {cert.get('subjectAltName', '未設定!')}")
print(f" 有効期限: {cert.get('notAfter')}")
except ssl.SSLCertVerificationError as e:
print(f"[SSL ERROR] 証明書検証失敗: {e}")
print(" 考えられる原因:")
print(" 1. verify= に正しい証明書パスが渡されていない")
print(" 2. 証明書の SAN に接続先ホスト名が含まれていない")
print(" 3. 証明書の有効期限切れ")
except ConnectionRefusedError:
print(f"[ERROR] ポート {port} に接続できません。サーバーが起動しているか確認")
if __name__ == "__main__":
diagnose_ssl("localhost", 5443, "server.cert.pem")
SANがないと最新ブラウザで弾かれる問題
Chrome 58以降、Common Name(CN)のみの証明書は無効として扱われます。前述の証明書生成コードで de>SubjectAlternativeName 拡張を必ず付与してください。既存の証明書にSANがあるか確認するには以下を実行します。
de class="language-python"># ファイル名: check_san.py — 証明書のSAN有無を確認する
from cryptography import x509
from cryptography.hazmat.primitives import serialization
from cryptography.x509.oid import ExtensionOID
def check_san(cert_path: str) -> None:
with open(cert_path, "rb") as f:
cert = x509.load_pem_x509_certificate(f.read())
try:
san = cert.extensions.get_extension_for_oid(ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
dns_names = san.value.get_values_for_type(x509.DNSName)
ip_addrs = san.value.get_values_for_type(x509.IPAddress)
print(f"[OK] SAN あり")
print(f" DNS: {dns_names}")
print(f" IP : {ip_addrs}")
except x509.ExtensionNotFound:
print("[NG] SAN が設定されていません!ブラウザで弾かれます")
print(" → generate_cert.py を再実行してSAN付き証明書を再生成してください")
if __name__ == "__main__":
check_san("server.cert.pem")
有効期限切れ証明書の自動更新スクリプト
証明書の有効期限を定期チェックして、残り日数が少なくなったら自動再生成するスクリプトです。cronやタスクスケジューラに登録しておくと便利です。
de class="language-python"># ファイル名: auto_renew_cert.py — 有効期限切れを検知して自動再生成
import datetime
import ipaddress
import subprocess
import sys
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import ExtensionOID, NameOID
CERT_PATH = "server.cert.pem"
KEY_PATH = "server.key.pem"
RENEW_DAYS_BEFORE = 30 # 残り30日以下になったら再生成
def is_renewal_needed(cert_path: str) -> bool:
"""証明書の有効期限をチェックして更新が必要か判定する"""
try:
with open(cert_path, "rb") as f:
cert = x509.load_pem_x509_certificate(f.read())
now = datetime.datetime.now(datetime.timezone.utc)
expires = cert.not_valid_after_utc # cryptography 42.x 以降の新API
remaining = (expires - now).days
print(f"[INFO] 証明書有効期限: {expires.date()} (残り {remaining} 日)")
if remaining <= RENEW_DAYS_BEFORE:
print(f"[WARN] 残り {remaining} 日のため再生成します")
return True
return False
except FileNotFoundError:
print(f"[INFO] {cert_path} が見つかりません。新規生成します")
return True
def generate_cert() -> None:
"""RSA秘密鍵と自己署名証明書を再生成する"""
# 秘密鍵生成
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
with open(KEY_PATH, "wb") as f:
f.write(key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
))
# 証明書生成
now = datetime.datetime.now(datetime.timezone.utc)
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "JP"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "LocalDev"),
x509.NameAttribute(NameOID.COMMON_NAME, "localhost"),
])
cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.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("localhost"),
x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")),
]),
critical=False,
)
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
.sign(key, hashes.SHA256())
)
with open(CERT_PATH, "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
print(f"[OK] 証明書を再生成しました: {CERT_PATH}")
if __name__ == "__main__":
if is_renewal_needed(CERT_PATH):
generate_cert()
else:
print("[OK] 証明書は有効です。更新不要")
まとめ・FAQ・次の記事への導線
まとめ
-
>python ssl 自己署名証明書 自作 ローカルの構築は de>cryptography ライブラリ一本で完結でき、OpenSSLコマンド不要
>証明書生成時は必ず SAN(SubjectAlternativeName) に de>localhost と de>127.0.0.1 を追加する(Chrome 58以降の必須要件)
>de>requests から使う場合は de>verify=False を避け、de>verify="server.cert.pem" と証明書パスを明示する
>Flaskの de>ssl_context 引数に de>ssl.SSLContext を渡すだけで https ローカル開発環境が立ち上がる
>自動更新スクリプトをcronに登録しておけば有効期限切れによる突然の障害を防げる
よくある質問(FAQ)
Q. forgeライブラリとcryptographyの違いは何ですか?
A. de>python-forge はNode.jsの de>node-forge からインスパイアされたPureなPythonライブラリで、暗号処理すべてをPythonで実装しています。対して de>cryptography(PyCA)はRust/OpenSSLバックエンドを持ち、パフォーマンスとセキュリティの更新頻度で圧倒的に優れています。SAN付き証明書生成のAPIも de>cryptography の方がシンプルです。新規開発では de>cryptography を選んでください。
Q. 本番環境でも自己署名証明書を使えますか?
A. 推奨しません。自己署名証明書はブラウザのルートCAストアに登録されていないため、一般ユーザーには「安全ではない」警告が表示されます。本番環境では Let's Encrypt(無料)か商用CAの証明書を使ってください。de>certbot を使えばLet's Encrypt証明書の自動取得・更新が可能です。
Q. cryptography 42.x で deprecated になった API は何ですか?
A. 最も影響が大きいのは de>datetime.datetime.utcnow() の非推奨化です。42.x以降では de>datetime.datetime.now(datetime.timezone.utc) を使ってください。また証明書の有効期限取得は de>not_valid_after(廃止予定)の代わりに de>not_valid_after_utc を使います。本記事のコードはすべてこの新APIに対応済みです。
次に読むべき記事
HTTPS環境が整ったら、次はリアルタイム通信のデバッグに挑戦してみましょう。
-
>➡️ 「PythonでWebSocketプロキシを自作してリアルタイム通信を全ログする方法」
【完全版】1ファイルで動くオールインワンスクリプト
上記コードを1ファイルにまとめたバージョンです。これ1本で「証明書生成→Flaskサーバー起動→requestsで接続確認」まで全部実行できます。
de class="language-python"># ファイル名: local_https_all_in_one.py
# 使い方: pip install "cryptography>=42.0.0" flask requests
# python local_https_all_in_one.py
import datetime
import ipaddress
import ssl
import threading
import time
import requests
import urllib3
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
from flask import Flask, jsonify
CERT_PATH = "server.cert.pem"
KEY_PATH = "server.key.pem"
PORT = 5443
# ── Step 1: 証明書と秘密鍵を生成 ────────────────────────────────
def generate_self_signed_cert() -> None:
"""RSA-2048の秘密鍵とSAN付き自己署名証明書を生成してPEMファイルに保存する"""
# 秘密鍵生成
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
with open(KEY_PATH, "wb") as f:
f.write(private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
))
# Subject / Issuer(自己署名なので同一)
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "JP"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Aichi"),
x509.NameAttribute(NameOID.LOCALITY_NAME, "Nagoya"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "LocalDev"),
x509.NameAttribute(NameOID.COMMON_NAME, "localhost"),
])
# 有効期限設定(aware datetimeで deprecation 警告を回避)
now = datetime.datetime.now(datetime.timezone.utc)
cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(private_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now)
.not_valid_after(now + datetime.timedelta(days=365))
# SAN: localhost と 127.0.0.1 の両方を登録(Chrome 58以降の必須要件)
.add_extension(
x509.SubjectAlternativeName([
x509.DNSName("localhost"),
x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")),
x509.IPAddress(ipaddress.IPv6Address("::1")),
]),
critical=False,
)
.add_extension(
x509.BasicConstraints(ca=False, path_length=None),
critical=True,
)
.add_extension(
x509.KeyUsage(
digital_signature=True, key_encipherment=True,
content_commitment=False, data_encipherment=False,
key_agreement=False, key_cert_sign=False,
crl_sign=False, encipher_only=False, decipher_only=False,
),
critical=True,
)
.add_extension(
x509.ExtendedKeyUsage([x509.oid.ExtendedKeyUsageOID.SERVER_AUTH]),
critical=False,
)
.sign(private_key, hashes.SHA256())
)
with open(CERT_PATH, "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
print(f"[OK] 証明書生成完了: {CERT_PATH} (有効期限: {now.date()} 〜 {(now + datetime.timedelta(days=365)).date()})")
# ── Step 2: Flaskサーバーをスレッドで起動 ────────────────────────
def run_flask_server() -> None:
"""SSL証明書を読み込んでFlask HTTPSサーバーを起動する"""
app = Flask(__name__)
@app.route("/")
def index():
return jsonify({"status": "ok", "message": "HTTPS ローカルサーバーが動いています!"})
@app.route("/health")
def health():
return jsonify({"healthy": True})
ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_ctx.load_cert_chain(certfile=CERT_PATH, keyfile=KEY_PATH)
# use_reloader=False でスレッド実行時のWerkzeugリロードを無効化
app.run(host="0.0.0.0", port=PORT, ssl_context=ssl_ctx,
debug=False, use_reloader=False)
# ── Step 3: requestsで接続確認 ───────────────────────────────────
def verify_connection() -> None:
"""生成した証明書を verify= に渡してHTTPS接続を確認する"""
# サーバー起動待ち
time.sleep(2)
try:
# verify= に証明書パスを指定することで、verify=False を使わずに接続できる
resp = requests.get(
f"https://localhost:{PORT}/health",
verify=CERT_PATH,
timeout=5,
)
print(f"[OK] HTTPS接続成功!ステータス: {resp.status_code}")
print(f"[OK] レスポンス: {resp.json()}")
except requests.exceptions.SSLError as e:
print(f"[SSL ERROR] 証明書検証に失敗しました: {e}")
except requests.exceptions.ConnectionError as e:
print(f"[CONNECTION ERROR] {e}")
# ── メイン処理 ───────────────────────────────────────────────────
if __name__ == "__main__":
# 証明書を生成
generate_self_signed_cert()
# Flaskをバックグラウンドスレッドで起動
server_thread = threading.Thread(target=run_flask_server, daemon=True)
server_thread.start()
print(f"[INFO] Flaskサーバーを起動しました: https://localhost:{PORT}/")
# requestsで接続確認
verify_connection()
print("[INFO] サーバーは継続起動中です。Ctrl+C で終了してください")
try:
server_thread.join()
except KeyboardInterrupt:
print("\n[INFO] サーバーを停止しました")