87

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

默认情况下,Python 在使用 HTTPS 时只接受并使用 SSL 证书,因此即使证书无效,urllib2 和 Twisted 等 Python 库也会愉快地使用该证书。

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

如何在 Python 中验证证书?

4

11 回答 11

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:
    ...
于 2010-10-15T23:06:57.427 回答
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-07-06T17:29:48.750 回答
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。

于 2009-12-17T12:48:51.283 回答
20

从发布版本 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 的信任数据库部分讨论了这两种方法的优缺点。

于 2015-02-04T15:57:34.883 回答
15

或者只是使用requests库让您的生活更轻松:

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

再说几句关于它的用法。

于 2013-09-12T14:32:57.417 回答
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()
于 2010-08-23T21:05:59.077 回答
8

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

根据 Glyphs 的评论,默认情况下 M2Crypto 的证书验证似乎比您目前使用 pyOpenSSL 所做的更好,因为 M2Crypto 也会检查 subjectAltName 字段。

我还写了一篇关于如何获取Mozilla Firefox 附带的 Python证书以及如何与 Python SSL 解决方案一起使用的博客。

于 2009-09-17T23:09:19.047 回答
4

Jython 默认执行证书验证,因此使用标准库模块,例如 httplib.HTTPSConnection 等,与 jython 将验证证书并给出失败异常,即身份不匹配、证书过期等。

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

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

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

于 2011-05-17T09:21:43.870 回答
3

以下代码允许您从所有 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()
于 2019-04-02T20:27:06.407 回答
-1

我遇到了同样的问题,但想尽量减少第 3 方的依赖(因为这个一次性脚本将由许多用户执行)。我的解决方案是包装一个curl调用并确保退出代码是0. 像魅力一样工作。

于 2013-12-11T11:18:58.590 回答
-1

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

于 2009-07-06T14:52:32.240 回答