用Python验证SSL证书
我需要写一个脚本,连接我们公司内部网络上的一些网站,通过HTTPS来检查它们的SSL证书是否有效;比如说,证书没有过期,证书是为正确的地址发放的等等。我们使用自己内部的公司证书颁发机构(CA)来管理这些网站,所以我们有CA的公钥来验证这些证书。
在Python中,默认情况下,当使用HTTPS时,它会直接接受并使用SSL证书,所以即使证书无效,像urllib2和Twisted这样的Python库也会毫无顾忌地使用这些证书。
那么,我该如何在Python中验证一个证书呢?
11 个回答
你可以使用Twisted来验证证书。主要的接口是CertificateOptions,这个可以作为contextFactory
参数提供给一些函数,比如listenSSL和startTLS。
不过,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()
我在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:
...
从版本 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 中有讨论。