用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库的接口。 它应该提供你需要的一切。