PythonでSSL証明書を検証する


85

HTTPS経由で企業イントラネット上の多数のサイトに接続し、それらのSSL証明書が有効であることを確認するスクリプトを作成する必要があります。有効期限が切れていないこと、正しいアドレスに対して発行されていることなど。これらのサイトには独自の社内認証局を使用しているため、CAの公開鍵を使用して証明書を検証します。

Pythonは、デフォルトではHTTPSを使用するときにSSL証明書を受け入れて使用するだけなので、証明書が無効な場合でも、urllib2やTwistedなどのPythonライブラリは証明書を問題なく使用します。

HTTPS経由でサイトに接続し、この方法で証明書を検証できる優れたライブラリはどこかにありますか?

Pythonで証明書を確認するにはどうすればよいですか?


10
Twistedに関するあなたのコメントは正しくありません。TwistedはPythonの組み込みSSLサポートではなく、pyopensslを使用しています。HTTPクライアントではデフォルトでHTTPS証明書を検証しませんが、「contextFactory」引数を使用してgetPageおよびdownloadPageを使用し、検証コンテキストファクトリを構築できます。対照的に、私の知る限り、組み込みの「ssl」モジュールが証明書の検証を行うように説得する方法はありません。
グリフ

4
Python 2.6以降のSSLモジュールを使用すると、独自の証明書バリデーターを作成できます。最適ではありませんが、実行可能です。
Heikki Toivonen

3
状況が変わり、Pythonはデフォルトで証明書を検証するようになりました。以下に新しい回答を追加しました。
Jan-Philip Gehrcke博士2015

Twistedの状況も変わりました(実際、Pythonの場合よりも少し前に)。バージョン14.0を使用しているtreq場合twisted.web.client.Agent、またはそれ以降、Twistedはデフォルトで証明書を検証します。
グリフ2015

回答:


19

リリースバージョン2.7.9 / 3.4.3以降、Pythonはデフォルトで証明書の検証を実行しようとします。

これはPEP467で提案されており、読む価値があります: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パラメーターに渡すことができます。

新しい組み込みの検証は、システムが提供する証明書データベースに基づいていることに注意してください。それとは反対に、リクエストパッケージには独自の証明書バンドルが付属しています。両方のアプローチの長所と短所は、PEP476の信頼データベースセクションで説明されています


以前のバージョンのPythonの証明書の検証を確実にするための解決策はありますか?Pythonのバージョンを常にアップグレードできるとは限りません。
vaab 2015

取り消された証明書は検証されません。例:revoked.badssl.com
Raz

HTTPSConnectionクラスの使用は必須ですか?使っていましたSSLSocket。どうすれば検証を行うことができSSLSocketますか?ここでpyopenssl説明さているように、を使用して明示的に検証する必要がありますか?
anir

31

以前のバージョンのPythonでmatch_hostname()Python3.2sslパッケージの関数を使用できるようにするディストリビューションをPythonPackageIndexに追加しました。

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:
    ...

1
何かが足りません...上の空欄に記入するか、完全な例を提供してください(Googleのようなサイトの場合)。
smholloway 2013年

例は、Googleへのアクセスに使用しているライブラリによって異なります。これは、ライブラリによってSSLソケットが異なる場所に配置さgetpeercert()れ、出力をに渡すためにメソッドを呼び出す必要があるのはSSLソケットであるためmatch_hostname()です。
Brandon Rhodes

12
Pythonに代わって、誰もがこれを使用しなければならないのは恥ずかしいです。箱から出してすぐに証明書を検証しないPythonの組み込みSSLHTTPSライブラリは完全に狂気であり、その結果、安全でないシステムがいくつあるかを想像するのは辛いことです。
グレンメイナード


26

Twistedを使用して証明書を検証できます。メインAPIはCertificateOptionsでありcontextFactorylistenSSLstartTLSなどのさまざまな関数への引数として提供できます。

残念ながら、PythonもTwistedも、実際にHTTPS検証を行うために必要なCA証明書の山も、HTTPS検証ロジックも付属していません。PyOpenSSLの制限により、まだ完全に正しく行うことはできませんが、ほとんどすべての証明書にサブジェクトcommonNameが含まれているため、十分に近づくことができます。

これは、ワイルドカードとsubjectAltName拡張子を無視し、ほとんどのUbuntuディストリビューションの「ca-certificates」パッケージにある認証局証明書を使用する、検証用のツイスト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()

ノンブロッキングにすることはできますか?
ショーンライリー

ありがとう; これを読んで理解したことに注意してください。エラーがない場合はコールバックがTrueを返し、エラーがある場合はFalseを返す必要があることを確認してください。commonNameがlocalhostでない場合、コードは基本的にエラーを返します。それがあなたの意図したものかどうかはわかりませんが、場合によってはこれを行うのが理にかなっています。この回答の将来の読者のために、これについてコメントを残すと思いました。
Eli Courtwright 2009

その場合の「self.hostname」は「localhost」ではありません。URLPath(url).netloc:は、secureGetに渡されるURLのホスト部分を意味することに注意してください。つまり、サブジェクトのcommonNameが、呼び出し元によって要求されているものと同じであることを確認しています。
グリフ

私はこのテストコードのバージョンを実行しており、Firefox、wget、およびChromeを使用してテストHTTPSサーバーをヒットしました。ただし、テストの実行では、コールバックverifyHostnameが接続ごとに3〜4回呼び出されていることがわかります。なぜ一度だけ実行されないのですか?
themaestro 2010

2
URLPath(blah).netloc常にlocalhostです。URLPath.__ init__は個々のURLコンポーネントを取り、URL全体を「スキーム」として渡し、デフォルトのnetlocである「localhost」を取得します。おそらくURLPath.fromString(url).netlocを使用するつもりでした。残念ながら、verifyHostNameのチェックインが逆方向になっていることがわかります。https://www.google.com/サブジェクトの1つが「www.google.com」であるために拒否が開始され、関数がFalseを返します。名前が一致する場合はTrue(受け入れられる)を返し、一致しない場合はFalseを返すことを意味している可能性がありますか?
mzz 2010

25

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です。


15

または、リクエストライブラリを使用して、生活を楽にしてください。

import requests
requests.get('https://somesite.com', cert='/path/server.crt', verify=True)

その使用法についてもう少し。


10
cert引数には、クライアント側の証明書ではなく、チェック対象のサーバ証明書です。verify引数を使用します。
–PaŭloEbermann 2014

2
リクエストはデフォルトで検証されますverifyより明示的であるか検証を無効にすることを除いて、引数を使用する必要はありません。
Jan-Philip Gehrcke博士2015

1
内部モジュールではありません。pipインストールリクエストを実行する必要があります
Robert Townley 2018年

14

証明書の検証を示すスクリプトの例を次に示します。

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 __name__ == "__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()

@tonfa:良いキャッチ。ホスト名チェックも追加することになり、使用したコードを含めるように回答を編集しました。
Eli Courtwright 2010年

元のリンク(つまり、「このページ」)にアクセスできません。動いた?
マットボール

@Matt:そうだと思いますが、私のテストプログラムは完全な自己完結型の実用的な例であるため、元のリンクは必要ありません。アトリビューションを提供するのはまともなことのように思えたので、そのコードを書くのに役立つページにリンクしました。しかし、それはもう存在しないので、これを指摘してくれてありがとう、リンクを削除するために私の投稿を編集します。
Eli Courtwright 2011

の手動ソケット接続のため、これはプロキシハンドラのような追加のハンドラでは機能しませんCertValidatingHTTPSConnection.connect。参照してください。このプル要求の詳細について(および修正を)。
schlamar 2012年

2
これは、でクリーンアップされて機能するソリューションbackports.ssl_match_hostnameです。
schlamar 2012年

8

M2Cryptoは検証行うことができます。必要に応じて、TwistedでM2Cryptoを使用することもできます。Chandlerデスクトップクライアントは、ネットワークにTwistedを使用し、SSLM2Cryptoを使用します(証明書の検証を含む)。

Glyphsのコメントに基づくと、M2CryptoはsubjectAltNameフィールドもチェックするため、現在pyOpenSSLで実行できるよりもデフォルトでM2Cryptoの方が証明書の検証が優れているようです。

また Mozilla Firefoxに同梱されている証明書をPythonで取得し、 PythonSSLソリューションで使用する方法についてもブログに書いています。


4

Jythonはデフォルトで証明書の検証を実行するため、httplib.HTTPSConnectionなどの標準ライブラリモジュールをjythonで使用すると、証明書が検証され、IDの不一致、証明書の期限切れなどの失敗に対して例外が発生します。

実際、jythonをcpythonのように動作させるには、つまりjythonに証明書を検証しないようにするには、追加の作業を行う必要があります。

jythonで証明書チェックを無効にする方法についてのブログ投稿を書きました。これは、テストフェーズなどで役立つ可能性があるためです。

Javaとjythonにすべてを信頼するセキュリティプロバイダーをインストールします。
http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-java-and-jython/


2

次のコードを使用すると、プラグイン可能な検証ステップ(ホスト名の検証やその他の追加の証明書検証ステップなど)を除いて、すべての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 __name__ == '__main__':
    # try expired.badssl.com or self-signed.badssl.com !
    conn = create_custom_HTTPSConnection('badssl.com')
    conn.request('GET', '/')
    conn.getresponse().read()

-1

pyOpenSSLは、OpenSSLライブラリへのインターフェイスです。それはあなたが必要とするすべてを提供するはずです。


OpenSSLはホスト名の照合を実行しません。OpenSSL1.1.0で計画されています。
jww 2014年

-1

私は同じ問題を抱えていましたが、サードパーティの依存関係を最小限に抑えたいと思っていました(この1回限りのスクリプトは多くのユーザーによって実行されるため)。私の解決策は、curl呼び出しをラップして、終了コードがであることを確認することでした0。チャームのように働いた。


pycurlを使用したstackoverflow.com/a/1921551/1228491の方がはるかに優れたソリューションだと思います。
マリアン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.