2009-07-06 177 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。像魅力一樣工作。