使用Python验证SSL证书

使用Python验证SSL证书,python,https,ssl-certificate,verification,Python,Https,Ssl Certificate,Verification,我需要编写一个脚本,通过HTTPS连接到我们公司内部网上的多个站点,并验证它们的SSL证书是否有效;这些证书没有过期,是为正确的地址颁发的,等等。我们对这些网站使用我们自己的内部公司证书颁发机构,因此我们拥有CA的公钥来验证证书 默认情况下,Python在使用HTTPS时只接受并使用SSL证书,因此即使证书无效,诸如urllib2和Twisted之类的Python库也会乐于使用该证书 有没有一个好的库可以让我通过HTTPS连接到一个站点并用这种方式验证它的证书 如何用Python验证证书?是Op

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

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

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


如何用Python验证证书?

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

您可以使用Twisted来验证证书。主API是,它可以作为
contextFactory
参数提供给各种函数,如和

不幸的是,Python和Twisted都没有提供实际执行HTTPS验证所需的一堆CA证书,也没有提供HTTPS验证逻辑。由于,您目前还不能完全正确地执行此操作,但由于几乎所有证书都包含一个主题commonName,因此您可以接近主题commonName

下面是一个简单的HTTPS客户端示例实现,它忽略了通配符和subjectAltName扩展,并在大多数Ubuntu发行版中使用“ca证书”包中的证书颁发机构证书。请使用您最喜欢的有效和无效证书站点进行尝试:)

可以。如果您愿意,也可以使用。Chandler桌面客户端,包括证书验证

根据Glyphs注释,似乎M2Crypto在默认情况下比当前使用pyOpenSSL进行的证书验证做得更好,因为M2Crypto也检查subjectAltName字段

我还写了一篇关于如何在Python中使用Mozilla Firefox并与Python SSL解决方案一起使用的博客。

做得很好

下面是一个简短的例子。如果有可疑的东西,它会抛出一个
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文档


    • 下面是一个演示证书验证的示例脚本:

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

      我已经在Python包索引中添加了一个发行版,使Python 3.2
      ssl
      包中的
      match_hostname()
      函数在Python的早期版本中可用

      您可以通过以下方式安装它:

      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:
          ...
      

      Jython在默认情况下确实执行证书验证,因此使用标准库模块(如httplib.HTTPSConnection等)和Jython将验证证书并给出失败的例外情况,即身份不匹配、证书过期等

      事实上,您必须做一些额外的工作才能让jython表现得像cpython一样,即让jython不验证证书

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

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

      或使用图书馆让您的生活更轻松:


      我也遇到了同样的问题,但希望尽量减少对第三方的依赖(因为这个一次性脚本将由许多用户执行)。我的解决方案是包装一个
      curl
      调用,并确保退出代码是
      0
      。工作起来很有魅力。

      从2.7.9/3.4.3版开始,Python默认情况下尝试执行证书验证

      PEP 467中提出了这一点,值得一读:

      这些更改会影响所有相关的stdlib模块(urllib/urllib2、http、httplib)

      相关文件:

      默认情况下,此类现在执行所有必要的证书和主机名检查。要还原到上一个未验证的行为ssl,可以将\u create\u unverified\u context()传递给context参数

      在版本3.4.3中更改:该类现在默认执行所有必要的证书和主机名检查。要还原到上一个未验证的行为ssl,可以将\u create\u unverified\u context()传递给context参数


      请注意,新的内置验证基于系统提供的证书数据库。与此相反,该包提供自己的证书包。这两种方法的优缺点将在中讨论。

      以下代码允许您受益于所有SSL验证检查(例如日期有效性、CA证书链…),但可插入验证步骤除外,例如验证主机名或执行其他附加证书验证步骤

      from httplib import HTTPSConnection
      import ssl
      
      
      def create_custom_HTTPSConnection(host):
      
          def verify_cert(cert, host):
              # Write your code here
              # You can certainly base yourself on ssl.match_hostname
              # Raise ssl.CertificateError if verification fails
              print 'Host:', host
              print 'Peer cert:', cert
      
          class CustomHTTPSConnection(HTTPSConnection, object):
              def connect(self):
                  super(CustomHTTPSConnection, self).connect()
                  cert = self.sock.getpeercert()
                  verify_cert(cert, host)
      
          context = ssl.create_default_context()
          context.check_hostname = False
          return CustomHTTPSConnection(host=host, context=context)
      
      
      if __name__ == '__main__':
          # try expired.badssl.com or self-signed.badssl.com !
          conn = create_custom_HTTPSConnection('badssl.com')
          conn.request('GET', '/')
          conn.getresponse().read()
      

      您对Twisted的评论不正确:Twisted使用pyopenssl,而不是Python的内置SSL支持。虽然默认情况下它不会在HTTP客户端中验证HTTPS证书,但您可以使用“contextFactory”参数来获取页面和下载页面,以构造验证上下文工厂。相比之下,据我所知,无法说服内置的“ssl”模块进行证书验证。使用Python 2.6及更高版本中的ssl模块,您可以编写自己的证书验证器。不是最优的,但是可行的。情况改变了,Python现在默认验证证书。我在下面添加了一个新的答案;如果您使用或从版本14开始。
      import requests
      requests.get('https://somesite.com', cert='/path/server.crt', verify=True)
      
      from httplib import HTTPSConnection
      import ssl
      
      
      def create_custom_HTTPSConnection(host):
      
          def verify_cert(cert, host):
              # Write your code here
              # You can certainly base yourself on ssl.match_hostname
              # Raise ssl.CertificateError if verification fails
              print 'Host:', host
              print 'Peer cert:', cert
      
          class CustomHTTPSConnection(HTTPSConnection, object):
              def connect(self):
                  super(CustomHTTPSConnection, self).connect()
                  cert = self.sock.getpeercert()
                  verify_cert(cert, host)
      
          context = ssl.create_default_context()
          context.check_hostname = False
          return CustomHTTPSConnection(host=host, context=context)
      
      
      if __name__ == '__main__':
          # try expired.badssl.com or self-signed.badssl.com !
          conn = create_custom_HTTPSConnection('badssl.com')
          conn.request('GET', '/')
          conn.getresponse().read()