https - 使用Python验证SSL证书

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

默认情况下,Python在使用HTTPS时接受并使用SSL证书,因此即使证书无效,诸如urllib2和Twisted之类的Python库也会很乐意使用证书。

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

如何在Python中验证证书?

10个解决方案

29 votes

我已经在Python Package Index中添加了一个发行版,它使得Python 3.2 setup.py包中的match_hostname()函数可用于以前版本的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:

...

Brandon Rhodes answered 2019-04-14T00:03:27Z

26 votes

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

不幸的是,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()

Glyph answered 2019-04-14T00:04:06Z

25 votes

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。

[http://curl.haxx.se/libcurl/c/curl_easy_setopt.html]

[http://curl.haxx.se/libcurl/c/curl_easy_getinfo.html]

plundra answered 2019-04-14T00:05:13Z

14 votes

这是一个演示证书验证的示例脚本:

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

Eli Courtwright answered 2019-04-14T00:05:38Z

13 votes

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

Jan-Philip Gehrcke answered 2019-04-14T00:02:48Z

12 votes

或者通过使用请求库简化您的生活:

import requests

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

关于其用法的更多话。

ufo answered 2019-04-14T00:06:08Z

8 votes

M2Crypto可以进行验证。 如果您愿意,也可以使用M2Crypto和Twisted。 Chandler桌面客户端使用Twisted进行网络连接,使用M2Crypto进行SSL,包括证书验证。

基于Glyphs注释,似乎M2Crypto默认情况下比默认使用pyOpenSSL做更好的证书验证,因为M2Crypto也会检查subjectAltName字段。

我还在博客上写了如何获得Mozilla Firefox附带的证书,并使用Python SSL解决方案。

Heikki Toivonen answered 2019-04-14T00:06:46Z

4 votes

Jython默认情况下会执行证书验证,因此使用标准库模块,例如 使用jython的httplib.HTTPSConnection等将验证证书并为失败提供例外,即不匹配的身份,过期的证书等。

事实上,你必须做一些额外的工作才能使jython像cpython一样运行,即让jython不验证证书。

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

在java和jython上安装一个完全信任的安全提供程序。

[http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-java-and-jython/]

Alan Kennedy answered 2019-04-14T00:07:39Z

-1 votes

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

DisplacedAussie answered 2019-04-14T00:08:03Z

-1 votes

我遇到了同样的问题但想要最小化第三方依赖(因为这个一次性脚本是由许多用户执行的)。 我的解决方案是打包curl,并确保退出代码为0。工作就像一个魅力。

Ztyx answered 2019-04-14T00:08:29Z