HTTPS経由で企業イントラネット上の多数のサイトに接続し、SSL証明書が有効であることを確認するスクリプトを作成する必要があります。有効期限が切れていないこと、正しいアドレスに対して発行されていることなど。これらのサイトには社内の認証局を使用しているため、CAの公開鍵を使用して証明書を検証します。
PythonはデフォルトでHTTPSの使用時にSSL証明書を受け入れて使用するため、証明書が無効な場合でも、urllib2やTwistedなどのPythonライブラリーは証明書を喜んで使用します。
HTTPS経由でサイトに接続し、この方法で証明書を検証できる優れたライブラリがどこかにありますか?
Pythonで証明書を確認するにはどうすればよいですか?
リリースバージョン2.7.9/3.4.3以降、Pythonデフォルトでは証明書の検証を実行しようとします。
これはPEP 467で提案されており、読む価値があります。 https://www.python.org/dev/peps/pep-0476/
変更は、関連するすべてのstdlibモジュール(urllib/urllib2、http、httplib)に影響します。
関連ドキュメント:
https://docs.python.org/2/library/httplib.html#httplib.HTTPSConnection
このクラスは、デフォルトですべての必要な証明書とホスト名のチェックを実行します。以前の未検証の動作に戻すには、ssl._create_unverified_context()をcontextパラメーターに渡すことができます。
https://docs.python.org/3/library/http.client.html#http.client.HTTPSConnection
バージョン3.4.3で変更:このクラスは、デフォルトですべての必要な証明書とホスト名のチェックを実行するようになりました。以前の未検証の動作に戻すには、ssl._create_unverified_context()をcontextパラメーターに渡すことができます。
新しい組み込み検証はsystem-provided証明書データベースに基づいていることに注意してください。それとは反対に、 requests パッケージは独自の証明書バンドルを出荷します。両方のアプローチの長所と短所については、PEP 476の Trust databaseセクション で説明されています。
以前のバージョンのPythonでPython 3.2 ssl
パッケージからmatch_hostname()
関数を使用可能にするPython Package Indexにディストリビューションを追加しました。
http://pypi.python.org/pypi/backports.ssl_match_hostname/
以下でインストールできます:
pip install backports.ssl_match_hostname
または、プロジェクトのsetup.py
にリストされている依存関係にすることもできます。いずれにしても、次のように使用できます。
from backports.ssl_match_hostname import match_hostname, CertificateError
...
sslsock = ssl.wrap_socket(sock, ssl_version=ssl.PROTOCOL_SSLv3,
cert_reqs=ssl.CERT_REQUIRED, ca_certs=...)
try:
match_hostname(sslsock.getpeercert(), hostname)
except CertificateError, ce:
...
Twistedを使用して証明書を検証できます。メインAPIは CertificateOptions で、これは listenSSL や startTLS などのさまざまな関数へのcontextFactory
引数として提供できます。
残念ながら、PythonもTwistedも、実際にHTTPS検証を行うために必要なCA証明書の山も、HTTPS検証ロジックも付属していません。 PyOpenSSLの制限 のため、まだ完全に正しく行うことはできませんが、ほとんどすべての証明書にサブジェクトcommonNameが含まれているため、十分に近づけることができます。
以下は、ワイルドカードとsubjectAltName拡張を無視し、ほとんどのUbuntuディストリビューションの「ca-certificates」パッケージにある認証局証明書を使用する検証済みのTwisted HTTPSクライアントの単純なサンプル実装です。お気に入りの有効な証明書サイトと無効な証明書サイトで試してみてください:)。
import os
import glob
from OpenSSL.SSL import Context, TLSv1_METHOD, VERIFY_PEER, VERIFY_FAIL_IF_NO_PEER_CERT, OP_NO_SSLv2
from OpenSSL.crypto import load_certificate, FILETYPE_PEM
from twisted.python.urlpath import URLPath
from twisted.internet.ssl import ContextFactory
from twisted.internet import reactor
from twisted.web.client import getPage
certificateAuthorityMap = {}
for certFileName in glob.glob("/etc/ssl/certs/*.pem"):
# There might be some dead symlinks in there, so let's make sure it's real.
if os.path.exists(certFileName):
data = open(certFileName).read()
x509 = load_certificate(FILETYPE_PEM, data)
digest = x509.digest('sha1')
# Now, de-duplicate in case the same cert has multiple names.
certificateAuthorityMap[digest] = x509
class HTTPSVerifyingContextFactory(ContextFactory):
def __init__(self, hostname):
self.hostname = hostname
isClient = True
def getContext(self):
ctx = Context(TLSv1_METHOD)
store = ctx.get_cert_store()
for value in certificateAuthorityMap.values():
store.add_cert(value)
ctx.set_verify(VERIFY_PEER | VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
ctx.set_options(OP_NO_SSLv2)
return ctx
def verifyHostname(self, connection, x509, errno, depth, preverifyOK):
if preverifyOK:
if self.hostname != x509.get_subject().commonName:
return False
return preverifyOK
def secureGet(url):
return getPage(url, HTTPSVerifyingContextFactory(URLPath.fromString(url).netloc))
def done(result):
print 'Done!', len(result)
secureGet("https://google.com/").addCallback(done)
reactor.run()
PycURL はこれを美しく行います。
以下は短い例です。何かが怪しい場合はpycurl.error
をスローします。エラーコードと人間が読めるメッセージを含むタプルを取得します。
import pycurl
curl = pycurl.Curl()
curl.setopt(pycurl.CAINFO, "myFineCA.crt")
curl.setopt(pycurl.SSL_VERIFYPEER, 1)
curl.setopt(pycurl.SSL_VERIFYHOST, 2)
curl.setopt(pycurl.URL, "https://internal.stuff/")
curl.perform()
おそらく、結果を保存する場所など、より多くのオプションを設定する必要があります。しかし、必須ではないサンプルで煩雑になる必要はありません。
発生する可能性のある例外の例:
(60, 'Peer certificate cannot be authenticated with known CA certificates')
(51, "common name 'CN=something.else.stuff,O=Example Corp,C=SE' does not match 'internal.stuff'")
便利だと思ったリンクは、setoptとgetinfoのlibcurl-docsです。
または、単純に requests ライブラリを使用して生活を楽にします:
import requests
requests.get('https://somesite.com', cert='/path/server.crt', verify=True)
証明書の検証を示すスクリプトの例を次に示します。
import httplib
import re
import socket
import sys
import urllib2
import ssl
class InvalidCertificateException(httplib.HTTPException, urllib2.URLError):
def __init__(self, Host, cert, reason):
httplib.HTTPException.__init__(self)
self.Host = Host
self.cert = cert
self.reason = reason
def __str__(self):
return ('Host %s returned an invalid certificate (%s) %s\n' %
(self.Host, self.reason, self.cert))
class CertValidatingHTTPSConnection(httplib.HTTPConnection):
default_port = httplib.HTTPS_PORT
def __init__(self, Host, port=None, key_file=None, cert_file=None,
ca_certs=None, strict=None, **kwargs):
httplib.HTTPConnection.__init__(self, Host, port, strict, **kwargs)
self.key_file = key_file
self.cert_file = cert_file
self.ca_certs = ca_certs
if self.ca_certs:
self.cert_reqs = ssl.CERT_REQUIRED
else:
self.cert_reqs = ssl.CERT_NONE
def _GetValidHostsForCert(self, cert):
if 'subjectAltName' in cert:
return [x[1] for x in cert['subjectAltName']
if x[0].lower() == 'dns']
else:
return [x[0][1] for x in cert['subject']
if x[0][0].lower() == 'commonname']
def _ValidateCertificateHostname(self, cert, hostname):
hosts = self._GetValidHostsForCert(cert)
for Host in hosts:
Host_re = Host.replace('.', '\.').replace('*', '[^.]*')
if re.search('^%s$' % (Host_re,), hostname, re.I):
return True
return False
def connect(self):
sock = socket.create_connection((self.Host, self.port))
self.sock = ssl.wrap_socket(sock, keyfile=self.key_file,
certfile=self.cert_file,
cert_reqs=self.cert_reqs,
ca_certs=self.ca_certs)
if self.cert_reqs & ssl.CERT_REQUIRED:
cert = self.sock.getpeercert()
hostname = self.Host.split(':', 0)[0]
if not self._ValidateCertificateHostname(cert, hostname):
raise InvalidCertificateException(hostname, cert,
'hostname mismatch')
class VerifiedHTTPSHandler(urllib2.HTTPSHandler):
def __init__(self, **kwargs):
urllib2.AbstractHTTPHandler.__init__(self)
self._connection_args = kwargs
def https_open(self, req):
def http_class_wrapper(Host, **kwargs):
full_kwargs = dict(self._connection_args)
full_kwargs.update(kwargs)
return CertValidatingHTTPSConnection(Host, **full_kwargs)
try:
return self.do_open(http_class_wrapper, req)
except urllib2.URLError, e:
if type(e.reason) == ssl.SSLError and e.reason.args[0] == 1:
raise InvalidCertificateException(req.Host, '',
e.reason.args[1])
raise
https_request = urllib2.HTTPSHandler.do_request_
if __== "__main__":
if len(sys.argv) != 3:
print "usage: python %s CA_CERT URL" % sys.argv[0]
exit(2)
handler = VerifiedHTTPSHandler(ca_certs = sys.argv[1])
opener = urllib2.build_opener(handler)
print opener.open(sys.argv[2]).read()
M2Crypto can 検証を行う 。必要に応じて M2Crypto with Twisted を使用することもできます。チャンドラーデスクトップクライアント ネットワークにはTwistedを、SSLにはM2Cryptoを使用 (証明書の検証を含む)。
Glyphsのコメントに基づくと、M2CryptoはsubjectAltNameフィールドもチェックするため、M2Cryptoは現在のpyOpenSSLでできることよりも、デフォルトで証明書の検証が優れているようです。
また、 証明書の取得 Mozilla FirefoxはPythonに同梱され、Python SSLソリューションで使用可能です。
Jythonはデフォルトで証明書の検証を行うため、標準ライブラリモジュールを使用します。 jythonを使用したhttplib.HTTPSConnectionなどは、証明書を検証し、失敗、つまり、一致しないID、期限切れの証明書などの例外を提供します。
実際、jythonがcpythonのように動作するには、つまりjythonが証明書を検証しないようにするには、追加の作業が必要です。
Jythonで証明書チェックを無効にする方法についてのブログ投稿を書いています。これはテストフェーズなどで役立つ可能性があるためです。
Javaおよびjythonにすべて信頼できるセキュリティプロバイダーをインストールします。
http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-Java-and-jython/
次のコードを使用すると、すべてのSSL検証チェック(日付の有効性、CA証明書チェーンなど)の恩恵を受けることができます。ホスト名を検証するか、他の追加の証明書検証手順を実行します。
from httplib import HTTPSConnection
import ssl
def create_custom_HTTPSConnection(Host):
def verify_cert(cert, Host):
# Write your code here
# You can certainly base yourself on ssl.match_hostname
# Raise ssl.CertificateError if verification fails
print 'Host:', Host
print 'Peer cert:', cert
class CustomHTTPSConnection(HTTPSConnection, object):
def connect(self):
super(CustomHTTPSConnection, self).connect()
cert = self.sock.getpeercert()
verify_cert(cert, Host)
context = ssl.create_default_context()
context.check_hostname = False
return CustomHTTPSConnection(Host=host, context=context)
if __== '__main__':
# try expired.badssl.com or self-signed.badssl.com !
conn = create_custom_HTTPSConnection('badssl.com')
conn.request('GET', '/')
conn.getresponse().read()