Xác thực chứng chỉ SSL bằng Python


85

Tôi cần viết một tập lệnh kết nối với một loạt các trang web trên mạng nội bộ công ty của chúng tôi qua HTTPS và xác minh rằng chứng chỉ SSL của họ là hợp lệ; rằng chúng chưa hết hạn, chúng được cấp cho địa chỉ chính xác, v.v. Chúng tôi sử dụng Cơ quan cấp chứng chỉ nội bộ của công ty cho các trang web này, vì vậy chúng tôi có khóa công khai của CA để xác minh các chứng chỉ dựa trên.

Python theo mặc định chỉ chấp nhận và sử dụng chứng chỉ SSL khi sử dụng HTTPS, vì vậy ngay cả khi chứng chỉ không hợp lệ, các thư viện Python như urllib2 và Twisted sẽ vui vẻ sử dụng chứng chỉ.

Có thư viện tốt ở đâu đó sẽ cho phép tôi kết nối với một trang web qua HTTPS và xác minh chứng chỉ của nó theo cách này không?

Làm cách nào để xác minh chứng chỉ bằng Python?


10
Nhận xét của bạn về Twisted không chính xác: Twisted sử dụng pyopenssl, không hỗ trợ SSL tích hợp của Python. Mặc dù nó không xác thực chứng chỉ HTTPS theo mặc định trong ứng dụng HTTP của nó, bạn có thể sử dụng đối số "contextFactory" để getPage và downloadPage để xây dựng nhà máy ngữ cảnh xác thực. Ngược lại, theo hiểu biết của tôi, không có cách nào mà mô-đun "ssl" được tích hợp sẵn có thể được thuyết phục để thực hiện xác thực chứng chỉ.
Glyph

4
Với mô-đun SSL trong Python 2.6 trở lên, bạn có thể viết trình xác thực chứng chỉ của riêng mình. Không phải là tối ưu, nhưng có thể làm được.
Heikki Toivonen 17/09/09

3
Tình hình đã thay đổi, Python giờ theo mặc định xác nhận các chứng chỉ. Tôi đã thêm một câu trả lời mới bên dưới.
Tiến sĩ Jan-Philip Gehrcke

Tình hình cũng thay đổi đối với Twisted (trên thực tế, phần nào trước khi nó xảy ra đối với Python); Nếu bạn sử dụng treqhoặc twisted.web.client.Agentkể từ phiên bản 14.0, Twisted xác minh chứng chỉ theo mặc định.
Glyph

Câu trả lời:


19

Từ phiên bản phát hành 2.7.9 / 3.4.3 trở đi, Python theo mặc định cố gắng thực hiện xác thực chứng chỉ.

Điều này đã được đề xuất trong PEP 467, rất đáng để đọc: https://www.python.org/dev/peps/pep-0476/

Các thay đổi ảnh hưởng đến tất cả các mô-đun stdlib có liên quan (urllib / urllib2, http, httplib).

Tài liệu liên quan:

https://docs.python.org/2/library/httplib.html#httplib.HTTPSConnection

Lớp này hiện thực hiện tất cả các kiểm tra chứng chỉ và tên máy chủ cần thiết theo mặc định. Để hoàn nguyên về trước đó, chưa được xác minh, hành vi ssl._create_unverified_context () có thể được chuyển cho tham số ngữ cảnh.

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

Đã thay đổi trong phiên bản 3.4.3: Lớp này hiện thực hiện tất cả các kiểm tra chứng chỉ và tên máy chủ cần thiết theo mặc định. Để hoàn nguyên về trước đó, chưa được xác minh, hành vi ssl._create_unverified_context () có thể được chuyển cho tham số ngữ cảnh.

Lưu ý rằng xác minh tích hợp mới dựa trên cơ sở dữ liệu chứng chỉ do hệ thống cung cấp . Đối lập với điều đó, gói yêu cầu vận chuyển gói chứng chỉ của riêng nó. Ưu và nhược điểm của cả hai cách tiếp cận được thảo luận trong phần Cơ sở dữ liệu tin cậy của PEP 476 .


bất kỳ giải pháp nào để đảm bảo xác minh chứng chỉ cho phiên bản trước của python? Không phải lúc nào người ta cũng có thể nâng cấp phiên bản của python.
vaab

nó không xác thực các chứng chỉ đã thu hồi. Ví dụ: revoked.badssl.com
Raz

Có bắt buộc phải sử dụng HTTPSConnectionlớp học không? Tôi đã sử dụng SSLSocket. Làm cách nào để xác thực với SSLSocket? Tôi có phải xác thực rõ ràng bằng cách sử dụng pyopensslnhư được giải thích ở đây không?
anir

31

Tôi đã thêm một bản phân phối vào Chỉ mục gói Python để làm cho match_hostname()hàm từ sslgói Python 3.2 có sẵn trên các phiên bản Python trước.

http://pypi.python.org/pypi/backports.ssl_match_hostname/

Bạn có thể cài đặt nó với:

pip install backports.ssl_match_hostname

Hoặc bạn có thể biến nó thành một phần phụ thuộc được liệt kê trong dự án của bạn setup.py. Dù bằng cách nào, nó có thể được sử dụng như thế này:

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

1
Tôi đang thiếu một cái gì đó ... bạn có thể vui lòng điền vào chỗ trống ở trên hoặc cung cấp một ví dụ đầy đủ (cho một trang web như Google)?
smholloway

Ví dụ sẽ trông khác nhau tùy thuộc vào thư viện bạn đang sử dụng để truy cập Google, vì các thư viện khác nhau đặt SSL socket ở những vị trí khác nhau và chính socket SSL cần getpeercert()phương thức của nó được gọi để đầu ra có thể được chuyển đến match_hostname().
Brandon Rhodes

12
Tôi xấu hổ thay mặt cho Python rằng bất kỳ ai cũng phải sử dụng cái này. Các thư viện SSL HTTPS tích hợp của Python không xác minh chứng chỉ ra khỏi hộp theo mặc định là hoàn toàn điên rồ và kết quả là thật đau đớn khi tưởng tượng có bao nhiêu hệ thống không an toàn hiện có.
Glenn Maynard


26

Bạn có thể sử dụng Twisted để xác minh chứng chỉ. API chính là CertificateOptions , có thể được cung cấp làm contextFactoryđối số cho các hàm khác nhau như listeningSSLstartTLS .

Thật không may, cả Python và Twisted đều không đi kèm với một đống chứng chỉ CA cần thiết để thực sự xác thực HTTPS, cũng như logic xác thực HTTPS. Do một hạn chế trong PyOpenSSL , bạn chưa thể làm điều đó hoàn toàn chính xác, nhưng nhờ thực tế là hầu hết tất cả các chứng chỉ đều bao gồm tên chủ đề là commonName, bạn có thể đến đủ gần.

Đây là một triển khai mẫu ngây thơ của một ứng dụng khách HTTPS Twisted xác minh bỏ qua các ký tự đại diện và phần mở rộng subjectAltName và sử dụng các chứng chỉ của tổ chức phát hành chứng chỉ có trong gói 'ca-certificate' trong hầu hết các bản phân phối Ubuntu. Hãy thử nó với các trang web chứng chỉ hợp lệ và không hợp lệ yêu thích của bạn :).

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

bạn có thể làm cho nó không bị chặn không?
sean riley

Cảm ơn; Bây giờ tôi có một lưu ý mà tôi đã đọc và hiểu điều này: xác minh các lệnh gọi lại sẽ trả về True khi không có lỗi và False khi có. Về cơ bản, mã của bạn trả về lỗi khi commonName không phải là localhost. Tôi không chắc đó có phải là những gì bạn dự định hay không, mặc dù sẽ rất hợp lý nếu làm điều này trong một số trường hợp. Tôi chỉ nghĩ rằng tôi sẽ để lại một bình luận về điều này vì lợi ích của những độc giả tương lai của câu trả lời này.
Eli Courtwright

"self.hostname" trong trường hợp đó không phải là "localhost"; lưu ý URLPath(url).netloc: điều đó có nghĩa là phần máy chủ của URL được chuyển vào secureGet. Nói cách khác, nó kiểm tra xem CommonName của chủ thể có giống với tên được người gọi yêu cầu hay không.
Glyph

Tôi đang chạy một phiên bản của mã thử nghiệm này và đã sử dụng Firefox, wget và Chrome để truy cập Máy chủ HTTPS thử nghiệm. Tuy nhiên, trong các lần chạy thử nghiệm của tôi, tôi thấy rằng tên gọi lại verifyHostname đang được gọi 3-4 lần mỗi kết nối. Tại sao nó không chỉ chạy một lần?
themaestro

2
URLPath (blah) .netloc luôn localhost: URLPath .__ init__ nhận các thành phần url riêng lẻ, bạn đang chuyển toàn bộ url dưới dạng "lược đồ" và lấy netloc mặc định của 'localhost' đi kèm với nó. Có thể bạn muốn sử dụng URLPath.fromString (url) .netloc. Thật không may, điều đó cho thấy đăng ký verifyHostName bị ngược: nó bắt đầu từ chối https://www.google.com/vì một trong các chủ đề là 'www.google.com', khiến hàm trả về False. Nó có thể có nghĩa là trả về True (được chấp nhận) nếu tên khớp và False nếu chúng không khớp?
mzz

25

PycURL làm điều này rất hay.

Dưới đây là một ví dụ ngắn. Nó sẽ ném ra pycurl.errornếu có gì đó khó hiểu, nơi bạn nhận được một bộ dữ liệu với mã lỗi và một thông báo con người có thể đọc được.

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

Bạn có thể sẽ muốn định cấu hình nhiều tùy chọn hơn, như nơi lưu trữ kết quả, v.v. Nhưng không cần phải làm lộn xộn ví dụ với những thứ không cần thiết.

Ví dụ về những ngoại lệ nào có thể được nêu ra:

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

Một số liên kết mà tôi thấy hữu ích là libcurl-docs cho setopt và getinfo.


15

Hoặc đơn giản là làm cho cuộc sống của bạn dễ dàng hơn bằng cách sử dụng thư viện yêu cầu :

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

Một vài lời nói thêm về cách sử dụng của nó.


10
Đối certsố là chứng chỉ phía máy khách, không phải chứng chỉ máy chủ để kiểm tra. Bạn muốn sử dụng verifyđối số.
Paŭlo Ebermann

2
yêu cầu xác thực theo mặc định . Không cần sử dụng verifyđối số, ngoại trừ việc xác minh rõ ràng hơn hoặc vô hiệu hóa.
Tiến sĩ Jan-Philip Gehrcke

1
Nó không phải là một mô-đun nội bộ. Bạn cần chạy các yêu cầu cài đặt pip
Robert Townley

14

Đây là một tập lệnh ví dụ minh họa xác thực chứng chỉ:

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

@tonfa: Bắt tốt; Tôi cũng đã thêm kiểm tra tên máy chủ và tôi đã chỉnh sửa câu trả lời của mình để bao gồm mã mà tôi đã sử dụng.
Eli Courtwright

Tôi không thể truy cập liên kết ban đầu (tức là 'trang này'). Nó đã di chuyển chưa?
Matt Ball

@Matt: Tôi đoán vậy, nhưng FWIW liên kết ban đầu là không cần thiết, vì chương trình thử nghiệm của tôi là một ví dụ hoạt động hoàn chỉnh, khép kín. Tôi đã liên kết với trang đã giúp tôi viết mã đó vì nó có vẻ như là một điều tốt để cung cấp ghi công. Nhưng vì nó không còn tồn tại nữa, tôi sẽ chỉnh sửa bài đăng của mình để xóa liên kết, cảm ơn vì đã chỉ ra điều này.
Eli Courtwright

Điều này không hoạt động với trình xử lý bổ sung như trình xử lý proxy vì kết nối ổ cắm thủ công trong CertValidatingHTTPSConnection.connect. Xem yêu cầu kéo này để biết chi tiết (và bản sửa lỗi).
schlamar

2
Đây là một giải pháp làm sạch và làm việc với backports.ssl_match_hostname.
schlamar

8

M2Crypto có thể thực hiện xác thực . Bạn cũng có thể sử dụng M2Crypto với Twisted nếu muốn. Máy khách Chandler trên máy tính để bàn sử dụng Twisted cho mạng và M2Crypto cho SSL , bao gồm xác thực chứng chỉ.

Dựa trên nhận xét của Glyphs, có vẻ như M2Crypto thực hiện xác minh chứng chỉ theo mặc định tốt hơn những gì bạn có thể làm với pyOpenSSL hiện tại, vì M2Crypto cũng kiểm tra trường subjectAltName.

Tôi cũng đã viết blog về cách lấy các chứng chỉ mà Mozilla Firefox cung cấp bằng Python và có thể sử dụng được với các giải pháp Python SSL.


4

Jython KHÔNG thực hiện xác minh chứng chỉ theo mặc định, vì vậy việc sử dụng các mô-đun thư viện tiêu chuẩn, chẳng hạn như httplib.HTTPSConnection, v.v., với jython sẽ xác minh chứng chỉ và đưa ra các ngoại lệ cho các lỗi, tức là danh tính không khớp, chứng chỉ hết hạn, v.v.

Trên thực tế, bạn phải làm thêm một số công việc để jython hoạt động giống như cpython, tức là để jython KHÔNG xác minh chứng chỉ.

Tôi đã viết một bài đăng trên blog về cách tắt kiểm tra chứng chỉ trên jython, vì nó có thể hữu ích trong các giai đoạn kiểm tra, v.v.

Cài đặt nhà cung cấp bảo mật đáng tin cậy trên java và jython.
http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-java-and-jython/


2

Đoạn mã sau cho phép bạn hưởng lợi từ tất cả các kiểm tra xác thực SSL (ví dụ: hiệu lực ngày, chuỗi chứng chỉ CA ...) NGOẠI TRỪ bước xác minh có thể cắm thêm, ví dụ: xác minh tên máy chủ hoặc thực hiện các bước xác minh chứng chỉ bổ sung khác.

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

-1

pyOpenSSL là một giao diện của thư viện OpenSSL. Nó sẽ cung cấp mọi thứ bạn cần.


OpenSSL không thực hiện đối sánh tên máy chủ. Nó được lên kế hoạch cho OpenSSL 1.1.0.
jww

-1

Tôi đang gặp vấn đề tương tự nhưng muốn giảm thiểu sự phụ thuộc của bên thứ 3 (vì tập lệnh một lần này được nhiều người dùng thực thi). Giải pháp của tôi là kết thúc curlcuộc gọi và đảm bảo rằng mã thoát là 0. Làm việc như người ở.


Tôi muốn nói rằng stackoverflow.com/a/1921551/1228491 sử dụng pycurl là một giải pháp tốt hơn nhiều.
Marian
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.