Weryfikuj certyfikaty SSL za pomocą Pythona

85

Muszę napisać skrypt, który łączy się z wieloma witrynami w naszym firmowym intranecie za pośrednictwem protokołu HTTPS i sprawdza, czy ich certyfikaty SSL są ważne; że nie wygasły, że zostały wydane dla prawidłowego adresu itp. Korzystamy z własnego wewnętrznego, korporacyjnego urzędu certyfikacji dla tych witryn, więc mamy klucz publiczny CA do weryfikacji certyfikatów.

Python domyślnie po prostu akceptuje i używa certyfikatów SSL podczas korzystania z HTTPS, więc nawet jeśli certyfikat jest nieważny, biblioteki Pythona, takie jak urllib2 i Twisted, będą po prostu szczęśliwie korzystać z certyfikatu.

Czy jest gdzieś dobra biblioteka, która pozwoli mi połączyć się z witryną przez HTTPS i zweryfikować w ten sposób jej certyfikat?

Jak zweryfikować certyfikat w Pythonie?

Eli Courtwright
źródło
10
Twój komentarz na temat Twisted jest nieprawidłowy: Twisted używa pyopenssl, a nie wbudowanej obsługi SSL w Pythonie. Chociaż domyślnie nie sprawdza poprawności certyfikatów HTTPS w swoim kliencie HTTP, możesz użyć argumentu „contextFactory” do getPage i downloadPage do skonstruowania walidującej fabryki kontekstów. Z drugiej strony, o ile wiem, nie ma możliwości przekonania wbudowanego modułu „ssl” do weryfikacji certyfikatu.
Glyph
4
Dzięki modułowi SSL w Pythonie 2.6 i nowszych możesz napisać własny walidator certyfikatów. Nie optymalne, ale wykonalne.
Heikki Toivonen
3
Sytuacja się zmieniła, Python teraz domyślnie sprawdza poprawność certyfikatów. Poniżej dodałem nową odpowiedź.
Dr Jan-Philip Gehrcke
Sytuacja zmieniła się również w przypadku Twisted (w rzeczywistości nieco wcześniej niż w przypadku Pythona); Jeśli używasz treqlub twisted.web.client.Agentod wersji 14.0, Twisted domyślnie weryfikuje certyfikaty.
Glyph

Odpowiedzi:

19

Począwszy od wersji 2.7.9 / 3.4.3, Python domyślnie próbuje przeprowadzić weryfikację certyfikatu.

Zostało to zaproponowane w PEP 467, z którym warto przeczytać: https://www.python.org/dev/peps/pep-0476/

Zmiany dotyczą wszystkich odpowiednich modułów stdlib (urllib / urllib2, http, httplib).

Odpowiednia dokumentacja:

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

Ta klasa domyślnie wykonuje teraz wszystkie niezbędne testy certyfikatów i nazw hostów. Aby powrócić do poprzedniego, niezweryfikowanego zachowania, ssl._create_unverified_context () można przekazać do parametru context.

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

Zmieniono w wersji 3.4.3: Ta klasa domyślnie wykonuje teraz wszystkie niezbędne testy certyfikatów i nazw hostów. Aby powrócić do poprzedniego, niezweryfikowanego zachowania ssl._create_unverified_context () można przekazać do parametru context.

Należy zauważyć, że nowa wbudowana weryfikacja jest oparta na bazie danych certyfikatów dostarczonej przez system . W przeciwieństwie do tego, pakiet żądań zawiera własny pakiet certyfikatów. Zalety i wady obu podejść omówiono w sekcji bazy danych zaufania w PEP 476 .

Dr Jan-Philip Gehrcke
źródło
jakieś rozwiązania zapewniające weryfikację certyfikatu dla poprzedniej wersji Pythona? Nie zawsze można zaktualizować wersję Pythona.
vaab
nie weryfikuje unieważnionych certyfikatów. Np. Odwołane.badssl.com
Raz
Czy korzystanie z HTTPSConnectionklasy jest obowiązkowe ? Używałem SSLSocket. Jak mogę przeprowadzić walidację z SSLSocket? Czy muszę wyraźnie potwierdzić użycie, pyopenssljak wyjaśniono tutaj ?
anir
31

Dodałem dystrybucję do Python Package Index, która sprawia, że match_hostname()funkcja z sslpakietu Python 3.2 jest dostępna we wcześniejszych wersjach Pythona.

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

Możesz go zainstalować za pomocą:

pip install backports.ssl_match_hostname

Lub możesz ustawić to jako zależność wymienioną w pliku setup.py. Tak czy inaczej, można go używać w następujący sposób:

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:
    ...
Brandon Rhodes
źródło
1
Brakuje mi czegoś ... czy możesz wypełnić puste pola powyżej lub podać pełny przykład (dla witryny takiej jak Google)?
smholloway
Przykład będzie wyglądał inaczej w zależności od biblioteki, z której korzystasz, aby uzyskać dostęp do Google, ponieważ różne biblioteki umieszczają gniazdo SSL w różnych miejscach i to gniazdo SSL wymaga swojej getpeercert()metody nazywanej tak, aby można było przekazać dane wyjściowe match_hostname().
Brandon Rhodes,
12
W imieniu Pythona jestem zażenowany, że ktoś musi tego używać. Wbudowane w Pythonie biblioteki SSL HTTPS, które domyślnie nie weryfikują certyfikatów po wyjęciu z pudełka, są całkowicie szalone i trudno sobie wyobrazić, ile niezabezpieczonych systemów jest obecnie dostępnych.
Glenn Maynard
26

Możesz użyć Twisted do weryfikacji certyfikatów. Głównym interfejsem API jest CertificateOptions , które można podać jako contextFactoryargument dla różnych funkcji, takich jak ListenSSL i startTLS .

Niestety, ani Python, ani Twisted nie zawierają stosu certyfikatów CA wymaganych do faktycznego sprawdzenia poprawności HTTPS ani logiki walidacji HTTPS. Ze względu na ograniczenia w PyOpenSSL nie możesz jeszcze tego zrobić całkowicie poprawnie, ale dzięki temu, że prawie wszystkie certyfikaty zawierają temat commonName, możesz się wystarczająco zbliżyć.

Oto naiwna przykładowa implementacja weryfikującego klienta Twisted HTTPS, który ignoruje symbole wieloznaczne i rozszerzenia subjectAltName oraz używa certyfikatów urzędu certyfikacji obecnych w pakiecie „ca-Certificates” w większości dystrybucji Ubuntu. Wypróbuj to na swoich ulubionych ważnych i nieprawidłowych witrynach z certyfikatami :).

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()
Glif
źródło
czy możesz uczynić to nieblokującym?
Sean Riley
Dzięki; Mam teraz jedną notatkę, którą przeczytałem i zrozumiałem: sprawdź, czy wywołania zwrotne powinny zwracać True, gdy nie ma błędu i False, gdy występuje. Twój kod zasadniczo zwraca błąd, gdy commonName nie jest localhost. Nie jestem pewien, czy to właśnie zamierzałeś, chociaż w niektórych przypadkach miałoby to sens. Pomyślałem, że zostawię komentarz na ten temat z korzyścią dla przyszłych czytelników tej odpowiedzi.
Eli Courtwright,
„self.hostname” w tym przypadku nie jest „localhost”; zwróć uwagę na URLPath(url).netloc: oznacza to część hosta adresu URL przekazaną do secureGet. Innymi słowy, sprawdza, czy zwykła nazwa podmiotu jest taka sama, jak ta, o którą prosi dzwoniący.
Glyph
Używałem wersji tego kodu testowego i korzystałem z przeglądarki Firefox, wget i Chrome, aby uruchomić testowy serwer HTTPS. Jednak w moich testach widzę, że wywołanie zwrotne verifyHostname jest wywoływane 3-4 razy przy każdym połączeniu. Dlaczego nie działa tylko raz?
themaestro
2
URLPath (bla) .netloc to zawsze localhost: URLPath .__ init__ pobiera poszczególne składniki adresu URL, przekazujesz cały adres URL jako „schemat” i otrzymujesz domyślną wartość netloc „localhost”. Prawdopodobnie zamierzałeś użyć URLPath.fromString (url) .netloc. Niestety, to ujawnia, że ​​sprawdzenie w verifyHostName działa wstecz: zaczyna odrzucać, https://www.google.com/ponieważ jednym z tematów jest „www.google.com”, co powoduje, że funkcja zwraca False. Prawdopodobnie miało to na celu zwrócenie True (zaakceptowane), jeśli nazwy pasują, a False, jeśli nie?
mzz
25

PycURL robi to pięknie.

Poniżej znajduje się krótki przykład. Będzie to rzucić pycurl.error, jeśli coś jest podejrzany, gdzie można uzyskać krotki z kodem błędu i ludzkiej wiadomości czytelny.

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

Prawdopodobnie będziesz chciał skonfigurować więcej opcji, takich jak miejsce przechowywania wyników itp. Ale nie ma potrzeby zaśmiecania przykładu nieistotnymi.

Przykład wyjątków, które mogą zostać zgłoszone:

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

Niektóre linki, które uznałem za przydatne, to libcurl-docs dla setopt i getinfo.

plundra
źródło
15

Lub po prostu ułatw sobie życie, korzystając z biblioteki żądań :

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

Jeszcze kilka słów o jego zastosowaniu.

ufo
źródło
10
certArgumentem jest certyfikat po stronie klienta, a nie certyfikat serwera, aby sprawdzić przed. Chcesz użyć verifyargumentu.
Paŭlo Ebermann
2
żądania są sprawdzane domyślnie . Nie ma potrzeby używania verifyargumentu, z wyjątkiem wyraźniejszego lub wyłączania weryfikacji.
Dr Jan-Philip Gehrcke
1
To nie jest moduł wewnętrzny. Musisz uruchomić żądania instalacji pip
Robert Townley,
14

Oto przykładowy skrypt, który demonstruje walidację certyfikatu:

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()
Eli Courtwright
źródło
@tonfa: Dobry chwyt; Skończyło się na tym, że dodałem również sprawdzanie nazwy hosta i zredagowałem odpowiedź, aby zawierała kod, którego użyłem.
Eli Courtwright,
Nie mogę dotrzeć do oryginalnego linku (tj. „Ta strona”). Czy się poruszył?
Matt Ball
@Matt: Myślę, że tak, ale FWIW oryginalny link nie jest potrzebny, ponieważ mój program testowy jest kompletnym, samodzielnym, działającym przykładem. Podałem link do strony, która pomogła mi napisać ten kod, ponieważ wydawało się, że to przyzwoita rzecz, aby podać źródło. Ale ponieważ już nie istnieje, edytuję mój post, aby usunąć link, dzięki za wskazanie tego.
Eli Courtwright,
Nie działa to z dodatkowymi programami obsługi, takimi jak programy obsługi proxy, z powodu ręcznego połączenia gniazda w programie CertValidatingHTTPSConnection.connect. Zobacz to żądanie ściągnięcia, aby uzyskać szczegółowe informacje (i poprawkę).
schlamar
2
Oto oczyszczone i działające rozwiązanie z backports.ssl_match_hostname.
schlamar
8

M2Crypto może przeprowadzić walidację . Jeśli chcesz, możesz również użyć M2Crypto z Twisted . Klient stacjonarny Chandler korzysta z Twisted do obsługi sieci i M2Crypto do SSL , w tym do weryfikacji certyfikatu.

Na podstawie komentarza Glyphs wydaje się, że M2Crypto domyślnie przeprowadza lepszą weryfikację certyfikatu niż to, co można obecnie zrobić z pyOpenSSL, ponieważ M2Crypto sprawdza również pole subjectAltName.

Napisałem również na blogu o tym, jak uzyskać certyfikaty, które Mozilla Firefox dostarcza w Pythonie i które można używać z rozwiązaniami Python SSL.

Heikki Toivonen
źródło
4

Jython domyślnie przeprowadza weryfikację certyfikatów, więc używając standardowych modułów bibliotecznych, np. Httplib.HTTPSConnection, itp. Z jythonem będzie weryfikować certyfikaty i dawać wyjątki w przypadku awarii, tj. Niezgodnych tożsamości, wygasłych certyfikatów itp.

W rzeczywistości musisz wykonać dodatkową pracę, aby jython zachowywał się jak cpython, tj. Aby jython NIE weryfikował certyfikatów.

Napisałem post na blogu o tym, jak wyłączyć sprawdzanie certyfikatów w jythonie, ponieważ może to być przydatne w fazach testowania itp.

Instalowanie zaufanego dostawcy zabezpieczeń w Javie i Jythonie.
http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-java-and-jython/

Alan Kennedy
źródło
2

Poniższy kod umożliwia korzystanie ze wszystkich sprawdzeń poprawności SSL (np. Ważność daty, łańcuch certyfikatów CA ...) Z WYJĄTKIEM podłączanego kroku weryfikacji, np. W celu weryfikacji nazwy hosta lub wykonania innych dodatkowych kroków weryfikacji certyfikatu.

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()
Carl D'Halluin
źródło
-1

pyOpenSSL jest interfejsem do biblioteki OpenSSL. Powinien zapewnić wszystko, czego potrzebujesz.

PrzesiedleniAussie
źródło
OpenSSL nie dopasowuje nazwy hosta. Jest planowany dla OpenSSL 1.1.0.
jww
-1

Miałem ten sam problem, ale chciałem zminimalizować zależności stron trzecich (ponieważ ten jednorazowy skrypt miał być wykonywany przez wielu użytkowników). Moim rozwiązaniem było zawinięcie curlpołączenia i upewnienie się, że kod zakończenia to 0. Działał jak urok.

Ztyx
źródło
Powiedziałbym, że stackoverflow.com/a/1921551/1228491 przy użyciu pycurl jest wtedy znacznie lepszym rozwiązaniem.
Marian