概要
Python でSSL証明書の情報を確認したい場合、以下のようなコードが紹介されていることが多い。
import ssl
import OpenSSL # required pyopenssl
cert = ssl.get_server_certificate(('www.google.com', 443))
x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
試してみると、多くの場合はこの方法で正常に情報が取得できたが、正常に情報が取得できないケースがあったため、調査を実施した。
実際のところ invoke (Fabric 2) でローカルの openssl コマンド叩けばうまくいったけれど、何故 Python でやるとうまくいかなかったのかが疑問だったので。
原因
Server Name Indication(SNI) の指定がないため。
openssl s_client
コマンドで言えば -servername
が未指定のため。
IPアドレス直打ちの場合などにアクセスさせないためや、1つのサーバー上で複数のドメイン管理をしている場合などにSNI設定なしでアクセスすると別設定の証明書を返してきたり、そもそも返さなかったりする。
わざと openssl
コマンドで -servername example.com
など全く別のサーバーネームを与えた場合に同様の事象が再現した。
対処
原因がSNIと分かれば同じような質問があった。 結果としては、ssl.get_server_certificate
は使わず、その中身と同じようなことを自分で書いてやる必要がある。
import socket
import ssl
import OpenSSL
def get_server_certificate(hostname):
context = ssl.create_default_context()
with socket.create_connection((hostname, 443)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as sslsock:
der_cert = sslsock.getpeercert(True)
return ssl.DER_cert_to_PEM_cert(der_cert)
cert = get_server_certificate('www.google.com')
x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)