使用Python验证SSL证书


85

我需要编写一个脚本,该脚本通过HTTPS连接到公司内部网络上的许多站点,并验证其SSL证书是否有效;它们尚未过期,是否为正确的地址颁发等等。我们对这些站点使用我们自己的内部公司证书颁发机构,因此我们拥有CA的公钥来验证证书。

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

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

如何在Python中验证证书?


10
您对Twisted的评论不正确:Twisted使用pyopenssl,而不是Python的内置SSL支持。虽然默认情况下它不会在其HTTP客户端中验证HTTPS证书,但是您可以使用“ contextFactory”参数将getPage和downloadPage构造为一个验证上下文工厂。相比之下,据我所知,无法说服内置的“ ssl”模块进行证书验证。
雕文

4
使用Python 2.6和更高版本中的SSL模块,您可以编写自己的证书验证器。不是最佳,但可行。
Heikki Toivonen

3
情况发生了变化,Python现在默认情况下会验证证书。我在下面添加了新答案。
Jan-Philip Gehrcke博士2015年

Twisted的情况也发生了变化(实际上,它在Python之前有所变化);如果使用版本14.0treqtwisted.web.client.Agent更高版本,则Twisted默认情况下会验证证书。
雕文

Answers:


19

从版本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参数。

请注意,新的内置验证基于系统提供的证书数据库。与此相反,请求包附带了自己的证书捆绑包。PEP 476的“信任数据库”部分讨论了这两种方法的优缺点。


有什么解决方案可以确保对python先前版本的证书进行验证?不能总是升级python版本。
vaab 2015年

它不验证已撤销的证书。例如revoked.badssl.com
拉兹

HTTPSConnection上课是强制性的吗?我正在使用SSLSocket。如何进行验证SSLSocket?我是否必须pyopenssl按照此处的说明进行显式验证?
Anir

31

我在Python包索引中添加了一个发行版,该发行版使match_hostname()Python 3.2ssl包中的功能可在以前的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:
    ...

1
我遗漏了一些东西……您能不能填写上面的空白或提供完整的示例(对于Google之类的网站)?
smholloway

该示例根据您用来访问Google的库的不同而有所不同,因为不同的库将SSL套接字放置在不同的位置,并且SSL套接字需要getpeercert()调用其方法,以便可以将输出传递给match_hostname()
布兰登·罗兹

12
我为任何人都必须使用它而感到尴尬。Python的内置SSL HTTPS库默认情况下不对开箱即用的证书进行验证是完全疯狂的,并且很难想象现在有多少不安全的系统存在。
Glenn Maynard 2014年


26

您可以使用Twisted来验证证书。主要API是CertificateOptions,可以将它作为contextFactory各种函数(如listenSSLstartTLS)的参数提供。

不幸的是,Python和Twisted都没有实际执行HTTPS验证所需的一堆CA证书,也没有HTTPS验证逻辑。由于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()

您可以使其不受阻碍吗?
肖恩·莱利,2009年

谢谢; 现在,我已经阅读并理解了以下说明:验证回调在没有错误时应返回True,在没有错误时应返回False。当commonName不是localhost时,您的代码基本上返回错误。我不确定这是否是您想要的,尽管在某些情况下这样做很有意义。我只是想为以后的读者受益而对此发表评论。
Eli Courtwright 09年

在这种情况下,“ self.hostname”不是“ localhost”;注意URLPath(url).netloc:,这意味着传递到secureGet的URL的主机部分。换句话说,它正在检查主题的commonName是否与调用方请求的相同。
雕文

我一直在运行此测试代码的版本,并使用Firefox,wget和Chrome浏览器测试了HTTPS Server。不过在测试运行中,我看到每个连接都调用回调verifyHostname 3-4次。为什么它不只运行一次?
themaestro'7

2
URLPath(blah).netloc始终localhost:URLPath .__ init__带有单独的url组件,您将整个URL传递为“ scheme”,并获得默认的netloc“ localhost”。您可能打算使用URLPath.fromString(url).netloc。不幸的是,这使checkHostName中的检查向后暴露:https://www.google.com/由于主题之一是“ 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参数。
圣保罗Ebermann

2
请求默认验证。verify除了更明确或禁用验证之外,无需使用该参数。
Jan-Philip Gehrcke博士,2015年

1
它不是内部模块。您需要运行pip安装请求
Robert Townley,

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:好收获;我最终还添加了主机名检查,并且我编辑了答案以包括我使用的代码。
伊莱·考特赖特

我无法到达原始链接(即“此页面”)。它移动了吗?
Matt Ball

@Matt:我想是的,但是FWIW原始链接不是必需的,因为我的测试程序是一个完整的,自包含的工作示例。我链接到该页面,该页面可以帮助我编写该代码,因为提供属性似乎是一件不错的事情。但由于它不再存在,因此我将编辑我的帖子以删除该链接,谢谢您指出这一点。
伊莱·考特赖特

由于中的手动套接字连接,因此无法与其他处理程序(如代理处理程序)一起使用CertValidatingHTTPSConnection.connect。有关详细信息(和修复),请参阅此请求
schlamar 2012年

2
是的清理工作方案backports.ssl_match_hostname
schlamar 2012年

8

M2Crypto可以进行验证。如果愿意,还可以将M2Crypto与Twisted一起使用。Chandler桌面客户端将Twisted用于网络,将M2Crypto用于SSL,包括证书验证。

根据字形注释,默认情况下,M2Crypto似乎比当前使用pyOpenSSL的证书验证更好,因为M2Crypto也会检查subjectAltName字段。

我还写了博客,介绍如何获取Mozilla Firefox在Python中附带的证书以及可与Python SSL解决方案一起使用的证书


4

Jython不会在默认情况下执行证书验证,因此将带有jython的标准库模块(例如httplib.HTTPSConnection等)与jython一起使用,将验证证书并给出失败的例外,例如身份不匹配,证书过期等。

实际上,您必须做一些额外的工作才能使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

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


我会说使用pycurl的stackoverflow.com/a/1921551/1228491是一个更好的解决方案。
玛丽安
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.