首页 文章

使用Python验证SSL证书

提问于
浏览
78

我需要编写一个脚本,通过HTTPS连接到公司内部网上的一堆站点,并验证他们的SSL证书是否有效;他们没有过期,他们是为正确的地址等发出的 . 我们为这些网站使用我们自己的内部公司证书颁发机构,因此我们有CA的公钥来验证证书 .

默认情况下,Python在使用HTTPS时接受并使用SSL证书,因此即使证书无效,诸如urllib2和Twisted之类的Python库也会很乐意使用证书 .

是否有一个好的库可以让我通过HTTPS连接到一个站点并以这种方式验证它的证书?

如何在Python中验证证书?

10 回答

  • 26

    Jython默认情况下会执行证书验证,因此使用标准库模块,例如使用jython的httplib.HTTPSConnection等将验证证书并为失败提供例外,即不匹配的身份,过期的证书等 .

    事实上,你必须做一些额外的工作才能使jython表现得像cpython,即让jython不验证证书 .

    我写过一篇关于如何在jython上禁用证书检查的博客文章,因为它在测试阶段等方面很有用 .

    在java和jython上安装一个完全信任的安全提供程序 .
    http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-java-and-jython/

  • 25

    pyOpenSSL是OpenSSL库的接口 . 它应该提供您需要的一切 .

  • 29

    我遇到了同样的问题但想要最小化第三方依赖(因为这个一次性脚本是由许多用户执行的) . 我的解决方案是包装 curl 调用并确保退出代码是 0 . 工作就像一个魅力 .

  • 8

    从发布版本2.7.9 / 3.4.3开始,Python by default 尝试执行证书验证 .

    这已在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参数 .

    请注意,新的内置验证基于系统提供的证书数据库 . 与之相反,requests包附带了自己的证书包 . Trust database section of PEP 476讨论了这两种方法的优缺点 .

  • 12

    我已经在Python Package Index中添加了一个发行版,它使Python 3.2 ssl 包中的 match_hostname() 函数可用于以前版本的Python .

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

    您可以使用Twisted来验证证书 . 主API是CertificateOptions,可以作为 contextFactory 参数提供给各种函数,例如listenSSLstartTLS .

    不幸的是,Python和Twisted都没有提供实际进行HTTPS验证所需的大量CA证书,也没有HTTPS验证逻辑 . 由于a limitation in PyOpenSSL,你还不能完全正确地完成它,但是由于几乎所有证书都包含一个主题commonName,你可以得到足够接近 .

    以下是验证Twisted HTTPS客户端的简单示例实现,它忽略通配符和subjectAltName扩展,并使用大多数Ubuntu发行版中“ca-certificates”包中存在的证书颁发机构证书 . 尝试使用您最喜欢的有效和无效的证书网站:) .

    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()
    
  • 14

    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 .

  • 4

    这是一个演示证书验证的示例脚本:

    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()
    
  • -1

    或者通过使用requests库简化您的生活:

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

    A few more words about its usage.

  • -1

    M2Crypto可以do the validation . 如果您愿意,也可以使用M2Crypto with Twisted . Chandler桌面客户端uses Twisted for networking and M2Crypto for SSL,包括证书验证 .

    基于Glyphs注释,似乎M2Crypto默认情况下比默认使用pyOpenSSL做更好的证书验证,因为M2Crypto也会检查subjectAltName字段 .

    我还发表了关于如何使用get the certificates Mozilla Firefox在Python中附带并使用Python SSL解决方案的博客 .

相关问题