Проверить сертификаты 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» выполнить проверку сертификата.
Glyph
4
С помощью модуля SSL в Python 2.6 и новее вы можете написать свой собственный валидатор сертификатов. Не оптимально, но выполнимо.
Хейкки Тойвонен
3
Ситуация изменилась, Python теперь по умолчанию проверяет сертификаты. Я добавил новый ответ ниже.
Д-р Ян-Филип Герке
Ситуация также изменилась для Twisted (фактически несколько раньше, чем для Python); Если вы используете версию 14.0 treqили twisted.web.client.Agentстарше, Twisted проверяет сертификаты по умолчанию.
Glyph

Ответы:

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как описано здесь ?
анир
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().
Brandon Rhodes
12
От имени Python мне неловко, что кто-то должен это использовать. Встроенные в Python библиотеки SSL HTTPS, которые не проверяют сертификаты из коробки по умолчанию, совершенно безумие, и трудно представить, сколько небезопасных систем сейчас существует в результате.
Glenn Maynard
26

Вы можете использовать Twisted для проверки сертификатов. Главный API - это CertificateOptions , который может быть предоставлен в качестве contextFactoryаргумента для различных функций, таких как listenSSL и startTLS .

К сожалению, ни Python, ни Twisted не содержат ни кучи сертификатов CA, необходимых для фактической проверки HTTPS, ни логики проверки HTTPS. Из- за ограничений в PyOpenSSL вы пока не можете сделать это полностью правильно, но благодаря тому факту, что почти все сертификаты включают субъект commonName, вы можете подойти достаточно близко.

Вот наивный образец реализации проверяющего клиента Twisted HTTPS, который игнорирует подстановочные знаки и расширения subjectAltName и использует сертификаты центра сертификации, присутствующие в пакете ca-Certificates в большинстве дистрибутивов 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. Я не уверен, что вы намеревались это сделать, хотя в некоторых случаях имеет смысл сделать это. Я просто подумал, что оставлю комментарий по этому поводу для будущих читателей этого ответа.
Эли Кортрайт, 06
"self.hostname" в этом случае не является "localhost"; обратите внимание URLPath(url).netloc: это означает, что часть URL-адреса, переданного в secureGet, является хостом. Другими словами, он проверяет, совпадает ли commonName субъекта с тем, которое запрашивает вызывающий.
Glyph
Я запускал версию этого тестового кода и использовал Firefox, wget и Chrome для работы с тестовым сервером HTTPS. Однако в моих тестовых прогонах я вижу, что обратный вызов verifyHostname вызывается 3-4 раза при каждом подключении. Почему он не запускается один раз?
themaestro
2
URLPath (бла) .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Если что-то не так, он выдаст a , где вы получите кортеж с кодом ошибки и удобочитаемым сообщением.

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.

Plundra
источник
15

Или просто упростите себе жизнь с помощью библиотеки запросов :

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

Еще несколько слов о его использовании.

НЛО
источник
10
certАргумент сертификат на стороне клиента, а не сертификат сервера , чтобы проверить против. Вы хотите использовать verifyаргумент.
Паоло Эберманн
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: Хороший улов; В итоге я добавил также проверку имени хоста и отредактировал свой ответ, включив в него код, который использовал.
Эли Кортрайт,
Я не могу перейти по исходной ссылке (например, «эта страница»). Он переехал?
Мэтт Болл,
@Matt: Думаю, да, но FWIW исходная ссылка не нужна, поскольку моя тестовая программа представляет собой полный, автономный рабочий пример. Я связался со страницей, которая помогла мне написать этот код, поскольку мне показалось, что это достойная вещь для указания авторства. Но поскольку его больше не существует, я отредактирую свой пост, чтобы удалить ссылку, спасибо, что указали на это.
Эли Кортрайт,
Это не работает с дополнительными обработчиками, такими как обработчики прокси, из-за ручного подключения сокета в 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. Работал как шарм.

Ztyx
источник
Я бы сказал, что stackoverflow.com/a/1921551/1228491 с использованием pycurl - гораздо лучшее решение.
Мариан