用PythonvalidationSSL证书

我需要编写一个脚本,通过HTTPS连接到企业内部网上的一堆网站,并validation其SSL证书是否有效; 没有过期,签发的地址是否正确等等。我们使用我们自己的内部公司authentication中心,因此我们有CA的公钥来validation证书。

默认情况下,Python在使用HTTPS时接受和使用SSL证书,所以即使证书无效,Python库(如urllib2和Twisted)也会很高兴地使用证书。

是否有一个好的图书馆让我通过HTTPS连接到一个网站,并以这种方式validation其证书?

如何在Python中validation证书?

从版本2.7.9 / 3.4.3开始,Python 默认尝试执行证书validation。

这已经在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

这个类现在默认执行所有必要的证书和主机名检查。 要恢复到之前未经validation的行为ssl._create_unverified_context()可以传递给上下文参数。

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

在版本3.4.3中进行了更改:此类现在默认执行所有必需的证书和主机名检查。 要恢复到之前未经validation的行为ssl._create_unverified_context()可以传递给上下文参数。

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

我已经在Python Package Index中添加了一个发行版,该发行版在Python的match_hostname()版本中使用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: ... 

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

不幸的是,Python和Twisted都没有提供实际执行HTTPSvalidation所需的一系列CA证书,也没有提供HTTPSvalidation逻辑。 由于PyOpenSSL的限制 ,你不能完全正确地做到这一点,但是由于几乎所有的证书都包含一个主题commonName,所以你可以足够接近。

以下是一个validationTwisted 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() 

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

你可能会想要configuration更多的选项,比如在哪里存储结果等等。但是不需要把这个例子与非必需品混淆起来。

可能引发什么exception的例子:

 (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文档。

以下是一个演示证书validation的示例脚本:

 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) 

关于它的用法还有几个字。

M2Crypto可以做validation 。 如果你喜欢,你也可以使用M2Crypto和Twisted 。 Chandler桌面客户端使用Twisted进行networking连接,使用M2Crypto进行SSL ,包括证书validation。

基于字形的评论,它似乎像M2Crypto默认更好的证书validation比你可以用pyOpenSSL目前做的,因为M2Crypto也检查subjectAltName字段。

我还在博客上讨论了如何获得 Mozilla Firefox在Python中提供的证书 ,以及如何使用Python SSL解决scheme。

Jython DOES默认进行证书validation,所以在jython中使用标准库模块(例如httplib.HTTPSConnection等)将validation证书并提供失败的例外,即身份不匹配,过期的证书等。

事实上,你必须做一些额外的工作来让jython像cpython一样行事,也就是让jython不要validationcert。

我已经写了一篇关于如何禁用jython的证书检查的博客文章,因为它可以在testing阶段等方面有用。

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

我遇到了同样的问题,但希望最大限度地减less第三方依赖(因为这个一次性脚本是由许多用户执行的)。 我的解决scheme是打包curl调用,并确保退出代码为0 。 像魅力一样工作。

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