Перевірте SSL-сертифікати за допомогою Python


85

Мені потрібно написати сценарій, який підключається до безлічі веб-сайтів у нашій корпоративній інтрамережі через HTTPS і перевіряє, чи правильні їх сертифікати SSL; що термін їх дії не минув, що вони видані за правильною адресою тощо. Ми використовуємо власний внутрішній корпоративний центр сертифікації для цих веб-сайтів, тому для перевірки сертифікатів ми маємо відкритий ключ ЦС.

Python за замовчуванням просто приймає та використовує SSL-сертифікати при використанні HTTPS, тому навіть якщо сертифікат недійсний, бібліотеки Python, такі як urllib2 та Twisted, просто із задоволенням використовуватимуть сертифікат.

Чи є десь хороша бібліотека, яка дозволить мені підключитися до сайту через HTTPS і перевірити його сертифікат таким чином?

Як перевірити сертифікат на Python?


10
Ваш коментар щодо Twisted неправильний: Twisted використовує pyopenssl, а не вбудовану підтримку SSL у Python. Хоча він не перевіряє сертифікати HTTPS за замовчуванням у своєму HTTP-клієнті, ви можете використовувати аргумент "contextFactory" для getPage і downloadPage для побудови перевіряючої фабрики контексту. На відміну від цього, наскільки мені відомо, вбудований модуль "ssl" не може бути переконаний здійснити перевірку сертифіката.
Гліф

4
За допомогою модуля SSL у Python 2.6 та новіших версіях ви можете написати власний валідатор сертифікатів. Не оптимально, але здійсненно.
Heikki Toivonen

3
Ситуація змінилася, тепер Python за замовчуванням перевіряє сертифікати. Я додав нову відповідь нижче.
Доктор Ян-Філіп Герке,

Ситуація також змінилася для Twisted (дещо раніше, ніж для Python, насправді); Якщо ви використовуєте версію 14.0 treqабо twisted.web.client.Agentпізніше, Twisted перевіряє сертифікати за замовчуванням.
Гліф

Відповіді:


19

Починаючи з версії 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 () можна передати параметру контексту.

Зверніть увагу, що нова вбудована перевірка базується на наданій системою базі сертифікатів. На відміну від цього, пакет запитів постачає власний пакет сертифікатів. Плюси та мінуси обох підходів обговорюються в розділі бази даних довіри PEP 476 .


будь-які рішення для забезпечення перевірки сертифіката для попередньої версії python? Не завжди можна оновити версію python.
vaab

він не перевіряє анульовані сертифікати. Наприклад, revoked.badssl.com
Raz

Чи обов’язково використовувати HTTPSConnectionклас? Я використовував SSLSocket. Як я можу зробити перевірку за допомогою SSLSocket? Чи потрібно явно перевіряти використання, pyopensslяк пояснено тут ?
anir

31

Я додав розподіл до індексу пакетів Python, що робить match_hostname()функцію з sslпакета Python 3.2 доступною в попередніх версіях 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:
    ...

1
Щось мені не вистачає ... Ви можете заповнити пропуски вище або надати повний приклад (для такого веб-сайту, як Google)?
smholloway

Приклад буде виглядати по-різному, залежно від того, яку бібліотеку ви використовуєте для доступу до Google, оскільки різні бібліотеки розміщують сокет SSL в різних місцях, і саме сокет SSL потребує свого getpeercert()методу, що називається, щоб вихідний файл можна було передати match_hostname().
Брендон Роудс,

12
Мені незручно від імені Python, що хтось повинен цим користуватися. Вбудовані бібліотеки Python SSL HTTPS, які не перевіряють сертифікати з коробки за замовчуванням, абсолютно божевільні, і боляче уявляти, скільки в результаті є небезпечних систем.
Гленн Мейнард,


26

Ви можете використовувати Twisted для перевірки сертифікатів. Основним API є CertificateOptions , який може бути contextFactoryаргументом для різних функцій, таких як listeSSL та startTLS .

На жаль, ні Python, ні Twisted не мають купу сертифікатів ЦС, необхідних для фактичної перевірки HTTPS, а також логіки перевірки HTTPS. Через обмеження в PyOpenSSL ви поки що не можете зробити це повністю коректно, але завдяки тому, що майже всі сертифікати містять тему commonName, ви можете наблизитися.

Ось наївний зразок реалізації перевіряючого клієнта Twisted 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()

ти можеш зробити його неблокуючим?
Шон Рілі,

Дякую; У мене є одна примітка, коли я це прочитав і зрозумів: перевірте, що зворотні виклики повинні повертати True, коли помилки немає, і False, коли є. Ваш код в основному повертає помилку, коли commonName не є localhost. Я не впевнений, чи саме це ви задумали, хоча в деяких випадках це мало б сенс робити. Я просто припустив, що залишу коментар з цього приводу на користь майбутніх читачів цієї відповіді.
Елі Кортрайт

"self.hostname" в цьому випадку не є "localhost"; зверніть увагу URLPath(url).netloc: це означає, що основна частина URL-адреси, передана до secureGet. Іншими словами, це перевірка того, що commonName суб'єкта є таким самим, як і те, що вимагає абонент.
Гліф

Я запускав версію цього тестового коду і використовував Firefox, wget та Chrome для потрапляння на тестовий сервер HTTPS. У ході мого тестування я бачу, що зворотний виклик verifyHostname викликається 3-4 рази за кожне з'єднання. Чому це не просто один раз?
Теместро

2
URLPath (blah) .netloc - це завжди localhost: URLPath .__ init__ бере окремі компоненти URL-адреси, ви передаєте цілу URL-адресу як "схему" і отримуєте за замовчуванням netloc "localhost", щоб пройти з нею. Ви, мабуть, мали намір використовувати URLPath.fromString (url) .netloc. На жаль, це виявляє зворотну перевірку в verifyHostName: він починає відхиляти, https://www.google.com/оскільки одним із суб’єктів є „www.google.com”, через що функція повертає False. Можливо, це означало повернути True (прийнято), якщо імена збігаються, і False, якщо вони не відповідають?
mzz

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-docs для setopt та getinfo.


15

Або просто спростіть своє життя за допомогою бібліотеки запитів :

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

Ще кілька слів про його використання.


10
certАргумент сертифікат на стороні клієнта, а НЕ сертифікат сервера , щоб перевірити проти. Ви хочете використати verifyаргумент.
Paŭlo Ebermann

2
запити перевіряє за замовчуванням . Не потрібно використовувати verifyаргумент, за винятком того, що він є більш явним або відключає перевірку.
Доктор Ян-Філіп Герке,

1
Це не внутрішній модуль. Вам потрібно запустити запити на встановлення pip
Роберт Таунлі

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

@tonfa: Хороший улов; У підсумку я також додав перевірку імені хосту, і я відредагував свою відповідь, включивши використаний код.
Eli Courtwright

Я не можу отримати оригінальне посилання (тобто "ця сторінка"). Зрушилось?
Matt Ball

@Matt: Я думаю, що так, але ПЕРЕГЛЯД оригінального посилання не потрібно, оскільки моя тестова програма є повним, автономним, робочим прикладом. Я зробив посилання на сторінку, яка допомогла мені написати цей код, оскільки, здавалося б, це гідно забезпечити атрибуцію. Але оскільки воно вже не існує, я відредагую свою публікацію, щоб видалити посилання, дякую, що вказали на це.
Елі Кортрайт,

Це не працює з додатковими обробниками, такими як проксі-обробники, через ручне підключення до розетки в CertValidatingHTTPSConnection.connect. Докладніше (і виправлення) див. У цьому запиті на витягування .
schlamar

2
Ось очищений і робочий розчин з backports.ssl_match_hostname.
schlamar

8

M2Crypto може виконати перевірку . Ви також можете використовувати M2Crypto з Twisted, якщо хочете. Клієнт робочого столу Chandler використовує Twisted для роботи в мережі та M2Crypto для SSL , включаючи перевірку сертифіката.

На основі коментаря Glyphs, схоже, M2Crypto робить кращу перевірку сертифіката за замовчуванням, ніж те, що ви можете зробити з pyOpenSSL в даний час, оскільки M2Crypto також перевіряє поле subjectAltName.

Я також писав у блозі про те, як отримати сертифікати, з якими постачається Mozilla Firefox, на Python і які можна використовувати з рішеннями Python SSL.


4

Jython ДОПОВІДАЄ перевірку сертифікатів за замовчуванням, тому, використовуючи стандартні бібліотечні модулі, наприклад httplib.HTTPSConnection тощо, jython перевірятиме сертифікати та надаватиме винятки щодо помилок, тобто невідповідних ідентифікаційних даних, закінчених терміну дії тощо.

Насправді вам потрібно зробити додаткову роботу, щоб jython поводився як cpython, тобто щоб jython НЕ перевіряв сертифікати.

Я написав допис у блозі про те, як вимкнути перевірку сертифікатів на jython, оскільки це може бути корисним на етапах тестування тощо.

Встановлення надійного постачальника безпеки на java та jython.
http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-java-and-jython/


2

Наступний код дозволяє вам скористатися всіма перевірками перевірки 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()

-1

pyOpenSSL - це інтерфейс до бібліотеки OpenSSL. Він повинен забезпечити все необхідне.


OpenSSL не виконує зіставлення імен хостів. Його заплановано для OpenSSL 1.1.0.
jww

-1

У мене була та сама проблема, але я хотів мінімізувати залежності від сторонніх розробників (оскільки цей одноразовий сценарій повинен був виконуватися багатьма користувачами). Моє рішення було завершити curlдзвінок і переконатися, що код виходу був 0. Працював як оберіг.


Я б сказав, що stackoverflow.com/a/1921551/1228491 з використанням pycurl є набагато кращим рішенням.
Маріан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.