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

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

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

如何在 Python 中验证证书?

有帮助吗?

解决方案

从发行版上,Python的按默认2.7.9 / 3.4.3 试图执行证书验证。

此已经提出了PEP 467,这是值得一读: HTTPS://www.python。组织的/ dev / PEPS / PEP-0476 /

的变化影响的所有相关STDLIB模块(的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()可以被传递到上下文参数。

请注意的是,新的内置验证是基于系统提供的证书数据库。反对的是,href="http://docs.python-requests.org">请求包船舶它自己的证书捆绑的 信任数据库讨论的节PEP 476 。

其他提示

我添加了一个分配到Python包索引,这使得从Python 3.2 match_hostname()包上Python之前的版本中提供的功能ssl

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

可以使用扭曲来验证证书。主API是 CertificateOptions 时,其可以是提供作为contextFactory参数各种功能,诸如 listenSSL STARTTLS

不幸的是,Python中也不扭曲带有一个CA证书所需的桩真正做到HTTPS验证,也不是HTTPS验证逻辑。由于在PyOpenSSL 的限制,你不能这样做完全正确的,只是还没有,但由于这样的事实:几乎所有的证书包括主题COMMONNAME,你可以得到足够接近。

下面是一种扭曲的验证HTTPS客户端而忽略通配符和的SubjectAltName扩展的幼稚示例实现,并且使用存在于“CA证书”包在大多数Ubuntu的分布中的证书的授权证书。用你喜欢的有效和无效证书的网站试试吧:)

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

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。

下面是其证明证书验证的示例脚本:

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

或者干脆让您的生活更轻松使用请求库:

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

一些关于其使用多个字。

M2加密进行验证. 。您还可以使用 M2Crypto 与 Twisted 如果你喜欢。钱德勒桌面客户端 使用 Twisted 进行网络连接,使用 M2Crypto 进行 SSL, ,包括证书验证。

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

我还在博客上介绍了如何 获得证书 Mozilla Firefox 附带 Python,可与 Python SSL 解决方案一起使用。

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

事实上,你必须做一些额外的工作来获得的Jython表现得像CPython的,即获得的Jython到无法验证证书。

我已经写就如何禁用证书检查对Jython的,因为它可以在测试阶段中有用的博客文章,等等。

安装于Java和Jython全信任安全提供。结果 HTTP://jython.xhaus。 COM /安装-AN-全信任安全提供商上的Java和 - 的jython /

下面的代码允许从所有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()

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

我有同样的问题,但希望尽量减少第三方的依赖关系(因为这种一次性脚本是由许多用户执行)。我的解决办法是包装一个curl电话,确保退出代码被0。工作就像一个魅力。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top