2009-07-06 171 views
72

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

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

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

如何在Python中验证证书?

+10

您对Twisted的评论不正确:Twisted使用pyopenssl,而不是Python的内置SSL支持。虽然HTTP客户端默认不验证HTTPS证书,但您可以使用“contextFactory”参数getPage和downloadPage构建验证上下文工厂。相比之下,据我所知,内置的“ssl”模块没有办法确信证书验证。 – Glyph 2009-07-06 14:56:59

+4

使用Python 2.6及更高版本中的SSL模块,您可以编写自己的证书验证程序。不是最佳的,但可行的。 – 2009-09-17 22:58:10

+2

情况改变了,Python现在默认验证证书。我在下面添加了一个新答案。 – 2015-02-04 15:53:56

回答

12

从发布版本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()可以传递给上下文参数。

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

改变在3.4.3版本:这个班的学生执行所有默认了必要的证书和主机名检查。要恢复到之前未经验证的行为ssl._create_unverified_context()可以传递给上下文参数。

请注意,新的内置验证是基于系统提供的证书数据库。相对于此,requests包装包装自己的证书包。这两种方法的优点和缺点在Trust database section of PEP 476中讨论。

-1

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

+0

OpenSSL不执行主机名匹配。它计划用于OpenSSL 1.1.0。 – jww 2014-03-18 03:54:50

26

您可以使用Twisted来验证证书。主要API是CertificateOptions,它可以作为contextFactory参数提供给各种功能,例如listenSSLstartTLS

不幸的是,Python和Twisted都没有附带一堆实际进行HTTPS验证所需的CA证书,也没有提供HTTPS验证逻辑。由于a limitation in 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() 
+0

你可以使它非阻塞? – 2009-07-06 17:36:19

+0

谢谢;现在我已经注意到我已经阅读并理解了这一点:验证回调在没有错误时应该返回True,在没有错误时应该返回False。当commonName不是localhost时,你的代码基本上会返回一个错误。我不确定这是否是您的意图,但在某些情况下做到这一点很有意义。我只是想,我会留下评论关于这个为未来的读者的利益这个答案。 – 2009-07-06 19:55:00

+0

“self.hostname”在这种情况下不是“localhost”;请注意`URLPath(url).netloc`:这意味着URL传递给secureGet的主机部分。换句话说,它检查主题的commonName是否与调用者请求的一致。 – Glyph 2009-07-09 10:31:20

25

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'") 

,我发现一些有用的链接是libcurl中,文档的SETOPT和程序getinfo。

14

下面是一个示例脚本,这表明证书验证:

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

我添加了一个分配到Python包索引这使得match_hostname()功能从Python 3.2 ssl软件包可用于以前版本的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: 
    ... 
4

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

事实上,你必须做一些额外的工作,让jython行为像cpython,即让jython不要验证证书。

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

安装Java和Jython的全信任安全提供商。
http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-java-and-jython/

0

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