概要
JavaScript のコードをブラウザーで試すとき Python の HTTP サーバーは手軽な手段として重宝します。2016年から JavaScript のコミュニティで注目されるようになった Progressive Web App (PWA) では HTTPS が必須ですが、Python の HTTPS サーバーおよび Python 3 系での ssl モジュールの変更についての記事が以外となかったので、調べたことをまとめることにしました。
前提
Python 3.6 および OpenSSL 1.0.2 とそれ以降のバージョンを前提としてます。Python 3 系の ssl モジュールにさまざまなメソッドや定数が追加されており、Python 3.6 以前のバージョンではサンプルコードが動かない可能性があります。
OpenSSL のバージョンは ssl の定数から調べることができます。
>>> import ssl
>>> ssl.OPENSSL_VERSION
HTTP サーバーの復習
HTML ファイルを提供するサーバーをコマンドラインから起動させる場合、次のコマンドを実行します。
python3 -m http.server 8000
CGI を利用したい場合、--cgi
オプションを指定します。
python3 -m http.server --cgi 8000
実行したいスクリプトは /cgi-bin
もしくは /htbin
に設置する必要があります。
#!/usr/bin/env python3
print("Content-Type: text/plain; charset=utf-8;\r\n")
print("hello world")
自己署名証明書と秘密鍵の生成
次のワンライナーで自己署名証明書および秘密鍵を生成することができます。
# https://stackoverflow.com/a/41366949/531320
openssl req -x509 -newkey rsa:4096 -sha256 \
-nodes -keyout server.key -out server.crt \
-subj "/CN=example.com" -days 3650
HTTPS サーバーを立てる
http.server
モジュールに加えて、ssl
モジュールを読み込む必要があります。
SSLContext
オブジェクトを生成し、いろいろな設定を指定した上で、SSLContext.wrap_socket
を使って SSLSocket
オブジェクトを生成します。
from http.server import HTTPServer, SimpleHTTPRequestHandler
import ssl
def run(host, port, ctx, handler):
server = HTTPServer((host, port), handler)
server.socket = ctx.wrap_socket(server.socket)
print('Server Starts - %s:%s' % (host, port))
try:
server.serve_forever()
except KeyboardInterrupt:
pass
server.server_close()
print('Server Stops - %s:%s' % (host, port))
if __name__ == '__main__':
host = 'localhost'
port = 8000
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ctx.load_cert_chain('server.crt', keyfile='server.key')
ctx.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
handler = SimpleHTTPRequestHandler
run(host, port, ctx, handler)
ssl.OP_NO_TLSv1
と ssl.OP_NO_TLSv1_1
はそれぞれ TLS 1.0、TLS 1.1 を無効にすることを意味します。
CGI に対応する
CGI の場合、SimpleHTTPRequestHandler
の代わりに CGIHTTPRequestHandler
を使います。
from http.server import HTTPServer, CGIHTTPRequestHandler
import ssl
def run(host, port, ctx, handler):
server = HTTPServer((host, port), handler)
server.socket = ctx.wrap_socket(server.socket)
print('Server Starts - %s:%s' % (host, port))
try:
server.serve_forever()
except KeyboardInterrupt:
pass
server.server_close()
print('Server Stops - %s:%s' % (host, port))
if __name__ == '__main__':
host = 'localhost'
port = 8000
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ctx.load_cert_chain('server.crt', keyfile='server.key')
ctx.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
handler = CGIHTTPRequestHandler
handler.cgi_directories = ['/cgi-bin', '/htbin']
# https://stackoverflow.com/a/27303995/531320
handler.have_fork=False
run(host, port, ctx, handler)
macOS および Unix の場合、handler.have_fork=False
を追加しないとサーバーが動きません。
CGI のメリットは Python 以外の言語のプログラムを動かすことができることです。Node.js のコードは次のようになります。
#!/usr/bin/env node
console.log("Content-type: text/plain; charset=utf-8\n");
console.log("Hello World");
WSGI に対応する
wsgiref
モジュールを使えば WSGI に対応したサーバーを立てることができます。
from wsgiref.simple_server import make_server
from pathlib import Path
import ssl
def simple_app(env, start_response):
info = env['PATH_INFO'][1:]
if info == '':
info = 'index.html'
root = Path.cwd()
path = root.joinpath(info).resolve()
if root in path.parents and path.is_file():
status = '200 OK'
body = path.read_bytes()
suffix = path.suffix
content_type = {
'.html': 'text/html',
'.txt': 'text/plain',
'.json': 'application/json'
}.get(suffix, 'text/plain')
else :
body = b'404 Not Found'
status = '404 Not Found'
content_type = 'text/plain'
headers = [
('Content-Type', content_type),
('Content-Length', str(len(body)))
]
start_response(status, headers)
return [body]
def run(host, port, ctx, app):
server = make_server(host, port, app)
server.socket = ctx.wrap_socket(server.socket)
print('Server Starts - %s:%s' % (host, port))
try:
server.serve_forever()
except KeyboardInterrupt:
pass
server.server_close()
print('Server Stops - %s:%s' % (host, port))
if __name__ == '__main__':
host = 'localhost'
port = 8000
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ctx.load_cert_chain('server.crt', keyfile='server.key')
ctx.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
run(host, port, ctx, simple_app)
定義されているすべての環境変数 (env
) が知りたいのであれば、PEP-333 をご参照ください。wsgiref.simple_server.demo_app
を実行アプリにしたときに表示されるページから確認することもできます。
SSLContext
生成
一般的な使い方では ssl.create_default_context
が推奨されています。
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
指定できる定数は Purpose.SERVER_AUTH
(クライアントサイドのソケットを作るのに使う)と Purpose.CLIENT_AUTH
(サーバサイドのソケットを作るのに使う) です。
SSLContext
のコンストラクタを直接使うこともできます。
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
指定できる定数は PROTOCOL_TLS
(デフォルト)、PROTOCOL_TLS_CLIENT
、PROTOCOL_TLS_SERVER
です。Python 3.6 以前で使えたさまざまな定数は撤廃になりました。
オプション
options を通して、設定の確認や追加ができます。デフォルトのオプションは次のように確認できます。
>>> import ssl
>>> ssl.create_default_context().options
<Options.OP_ALL|OP_NO_SSLv3|OP_NO_SSLv2|
OP_CIPHER_SERVER_PREFERENCE|
OP_SINGLE_DH_USE|OP_SINGLE_ECDH_USE|
OP_NO_COMPRESSION: 2203714559>
次のコードでも同じ結果になります。
>>> ssl.SSLContext().options
Python 3.6 のデフォルトオプションは次のとおりです。
- OP_NO_COMPRESSION SSL チャネルでの圧縮を無効にする
- OP_CIPHER_SERVER_PREFERENCE クライアントよりもサーバーの暗号リストを優先する
- OP_SINGLE_DH_USE SL セッションを区別するのに同じ DH 鍵を再利用しない
- OP_SINGLE_ECDH_USE SL セッションを区別するのに同じ ECDH 鍵を再利用しない
- OP_NO_SSLv2 SSLv2 接続を禁止
- OP_NO_SSLv3 SSLv3 接続を禁止
wrap_socket
でソケットオブジェクトをラップした後でコンテキストのオプションにアクセスしたり、変更することができます。
server = HTTPServer((host, port), handler)
server.socket = ctx.wrap_socket(server.socket)
print(server.socket.context.options)
2つの wrap_socket の違い
ssl.wrap_socket
と SSLContext.wrap_socket
は指定できる引数が異なります。Python 3.6 での引数は次のとおりです。
ssl.wrap_socket(
sock,
keyfile=None,
certfile=None,
server_side=False,
cert_reqs=CERT_NONE,
ssl_version={see docs},
ca_certs=None,
do_handshake_on_connect=True,
suppress_ragged_eofs=True,
ciphers=None
)
SSLContext.wrap_socket(
sock,
server_side=False,
do_handshake_on_connect=True,
suppress_ragged_eofs=True,
server_hostname=None,
session=None
)
2つの違いは SSLContext.wrap_socket
のほうが指定できるオプションが少ないことです。Python 3.6 で SSLContext.wrap_socket
に追加された session
は ssl.wrap_socket
では指定できません。
ssl.wrap_socket
と SSLContext.wrap_socket
の使いわけの基準としては設定の数が多いか少ないかでしょう。
ssl.wrap.socket
を使う場合、設定は server.socket.context
を通して変更することになるので、設定の数が多いとコードが読みづらくなります。
SSLContext
のオブジェクトを直接つくる別のメリットはサードパーティの HTTP ライブラリに流用できることです。
暗号スイート
暗号スイート (Ciphersuites) は get_ciphers
で確認できます。暗号スイートを変更するには set_ciphers
を使います。
>>> import ssl
>>> ctx = ssl.create_default_context().get_ciphers()
>>> ctx.set_ciphers('ECDHE+AESGCM:!ECDSA')
>>> ctx.get_ciphers()
Mozilla のサイトでおすすめの暗号スイートが公開されています。TLS 1.3 に関して Cloudflare が nginx の設定ファイルを公開しています。OpenSSL 1.1.0 が TLS 1.3 の一部に対応しており、OpenSSL 1.1.1 で完全に対応する予定です。
その他
TLS の最小バージョン
クレジットカードの決済サービスに採用される PCI DSS v3.2 は2018年6月末日まで SSL および TSL 1.0 を無効にすることを求めています。PyPI に採用されている CDN サービスの fastly は TLS 1.1 も無効にします。
システムに導入されている Python 3 がサポートする TLS のバージョンは次のワンライナーで調べることができます。
# https://news.ycombinator.com/item?id=13539034
python3 -c "import json, urllib.request; print(json.loads(urllib.request.urlopen('https://www.howsmyssl.com/a/check').read().decode('UTF-8'))['tls_version'])"
TLS のための新しい API の提案
PEP 543 は TLS のための新しい API の導入を提案しています。開発の動機として、ssl モジュールは OpenSSL 依存しているために新しい OpenSSL を導入するには Python を再コンパイルしなければならないことや OpenSSL とは異なるさまざまな TLS ライブラリに切り換えることができないことが挙げられています。
主な SSL/TLS ライブラリは次のとおりです。
- Schannel SSP - Windows
- Secure Transport - macOS
- BoringSSL - Android
- LibreSSL - OpenBSD
- TLS in Linux kernel (4.1.3)
- picotls - HTTP/2 サーバーの h2o プロジェクトで開発
- s2n - Amazon