用Python验证SSL证书

90 投票
11 回答
238530 浏览
提问于 2025-04-15 12:42

我需要写一个脚本,连接我们公司内部网络上的一些网站,通过HTTPS来检查它们的SSL证书是否有效;比如说,证书没有过期,证书是为正确的地址发放的等等。我们使用自己内部的公司证书颁发机构(CA)来管理这些网站,所以我们有CA的公钥来验证这些证书。

在Python中,默认情况下,当使用HTTPS时,它会直接接受并使用SSL证书,所以即使证书无效,像urllib2和Twisted这样的Python库也会毫无顾忌地使用这些证书。

那么,我该如何在Python中验证一个证书呢?

11 个回答

26

你可以使用Twisted来验证证书。主要的接口是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()
31

我在Python包索引上添加了一个分发包,这样就可以在早期版本的Python中使用Python 3.2的ssl包里的match_hostname()函数了。

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

从版本 2.7.9 和 3.4.3 开始,Python 默认 会尝试进行证书验证。

这个变化在 PEP 467 中有提到,值得一读:https://www.python.org/dev/peps/pep-0476/

这些改动影响了所有相关的标准库模块(比如 urllib/urllib2、http、httplib)。

相关文档:

https://docs.python.org/2/library/httplib.html#httplib.HTTPSConnection

这个类现在默认会进行所有必要的证书和主机名检查。如果想恢复到之前不验证的行为,可以将 ssl._create_unverified_context() 传给上下文参数。

https://docs.python.org/3/library/http.client.html#http.client.HTTPSConnection

在版本 3.4.3 中进行了更改:这个类现在默认会进行所有必要的证书和主机名检查。如果想恢复到之前不验证的行为,可以将 ssl._create_unverified_context() 传给上下文参数。

需要注意的是,新的内置验证是基于 系统提供的 证书数据库。而与此不同的是,requests 包自带自己的证书包。两种方法的优缺点在 信任数据库 部分的 PEP 476 中有讨论。

撰写回答