Convalida i certificati SSL con Python


85

Devo scrivere uno script che si connetta a una serie di siti sulla nostra intranet aziendale tramite HTTPS e verifichi che i loro certificati SSL siano validi; che non siano scaduti, che siano emessi per l'indirizzo corretto, ecc. Per questi siti utilizziamo la nostra Autorità di certificazione aziendale interna, quindi abbiamo la chiave pubblica della CA per verificare i certificati.

Python per impostazione predefinita accetta e utilizza i certificati SSL quando si utilizza HTTPS, quindi anche se un certificato non è valido, le librerie Python come urllib2 e Twisted useranno felicemente il certificato.

Esiste una buona libreria da qualche parte che mi consenta di connettermi a un sito tramite HTTPS e di verificarne il certificato in questo modo?

Come si verifica un certificato in Python?


10
Il tuo commento su Twisted non è corretto: Twisted utilizza pyopenssl, non il supporto SSL integrato di Python. Sebbene non convalidi i certificati HTTPS per impostazione predefinita nel suo client HTTP, puoi utilizzare l'argomento "contextFactory" per getPage e downloadPage per costruire una fabbrica del contesto di convalida. Al contrario, per quanto ne so non c'è modo che il modulo "ssl" integrato possa essere convinto a fare la convalida del certificato.
Glifo

4
Con il modulo SSL in Python 2.6 e versioni successive, puoi scrivere il tuo validatore di certificati. Non ottimale, ma fattibile.
Heikki Toivonen

3
La situazione è cambiata, Python ora per impostazione predefinita convalida i certificati. Ho aggiunto una nuova risposta di seguito.
Dr. Jan-Philip Gehrcke

La situazione è cambiata anche per Twisted (un po 'prima di Python, infatti); Se utilizzi treqo twisted.web.client.Agentdalla versione 14.0, Twisted verifica i certificati per impostazione predefinita.
Glifo

Risposte:


19

Dalla versione di rilascio 2.7.9 / 3.4.3 in poi, Python per impostazione predefinita tenta di eseguire la convalida del certificato.

Questo è stato proposto in PEP 467, che vale la pena leggere: https://www.python.org/dev/peps/pep-0476/

Le modifiche interessano tutti i moduli stdlib rilevanti (urllib / urllib2, http, httplib).

Documentazione pertinente:

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

Questa classe ora esegue tutti i controlli necessari del certificato e del nome host per impostazione predefinita. Per ripristinare il comportamento precedente, non verificato, ssl._create_unverified_context () può essere passato al parametro context.

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

Modificato nella versione 3.4.3: questa classe ora esegue tutti i controlli necessari del certificato e del nome host per impostazione predefinita. Per ripristinare il comportamento precedente, non verificato, ssl._create_unverified_context () può essere passato al parametro context.

Si noti che la nuova verifica incorporata si basa sul database dei certificati fornito dal sistema . Al contrario, il pacchetto delle richieste spedisce il proprio bundle di certificati. Pro e contro di entrambi gli approcci sono discussi nella sezione del database Trust di PEP 476 .


qualche soluzione per garantire le verifiche del certificato per la versione precedente di python? Non è sempre possibile aggiornare la versione di python.
vaab

non convalida i certificati revocati. Ad esempio revoked.badssl.com
Raz

È obbligatorio utilizzare la HTTPSConnectionclasse? Stavo usando SSLSocket. Come posso fare la convalida con SSLSocket? Devo convalidare esplicitamente l'utilizzo pyopensslcome spiegato qui ?
anir

31

Ho aggiunto una distribuzione all'indice del pacchetto Python che rende disponibile la match_hostname()funzione dal sslpacchetto Python 3.2 nelle versioni precedenti di Python.

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

Puoi installarlo con:

pip install backports.ssl_match_hostname

Oppure puoi renderlo una dipendenza elencata nel tuo progetto setup.py. Ad ogni modo, può essere usato in questo modo:

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
Mi manca qualcosa ... puoi riempire gli spazi sopra o fornire un esempio completo (per un sito come Google)?
smholloway

L'esempio avrà un aspetto diverso a seconda della libreria che stai utilizzando per accedere a Google, poiché diverse librerie collocano il socket SSL in posizioni diverse, ed è il socket SSL che necessita del suo getpeercert()metodo chiamato in modo che l'output possa essere passato match_hostname().
Brandon Rhodes

12
Sono imbarazzato per conto di Python che qualcuno debba usarlo. Le librerie SSL HTTPS integrate di Python che non verificano i certificati immediatamente per impostazione predefinita è completamente folle, ed è doloroso immaginare quanti sistemi insicuri ci sono ora come risultato.
Glenn Maynard


26

Puoi utilizzare Twisted per verificare i certificati. L'API principale è CertificateOptions , che può essere fornito come contextFactoryargomento per varie funzioni come listenSSL e startTLS .

Sfortunatamente, né Python né Twisted vengono forniti con una pila di certificati CA necessari per eseguire effettivamente la convalida HTTPS, né la logica di convalida HTTPS. A causa di una limitazione in PyOpenSSL , non puoi ancora farlo completamente correttamente, ma grazie al fatto che quasi tutti i certificati includono un oggetto commonName, puoi avvicinarti abbastanza.

Ecco un'implementazione di esempio ingenua di un client HTTPS Twisted di verifica che ignora i caratteri jolly e le estensioni subjectAltName e utilizza i certificati dell'autorità di certificazione presenti nel pacchetto "ca-certificates" nella maggior parte delle distribuzioni Ubuntu. Provalo con i tuoi siti di certificati validi e non validi preferiti :).

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

puoi renderlo non bloccante?
sean riley

Grazie; Ho una nota ora che ho letto e compreso: verifica che i callback restituiscano True quando non ci sono errori e False quando c'è. Il tuo codice fondamentalmente restituisce un errore quando commonName non è localhost. Non sono sicuro che sia quello che volevi, anche se in alcuni casi avrebbe senso farlo. Ho solo pensato di lasciare un commento su questo a beneficio dei futuri lettori di questa risposta.
Eli Courtwright

"self.hostname" in questo caso non è "localhost"; nota URLPath(url).netloc: che significa la parte host dell'URL passato a secureGet. In altre parole, verifica che il commonName dell'oggetto sia lo stesso di quello richiesto dal chiamante.
Glifo

Ho eseguito una versione di questo codice di prova e ho utilizzato Firefox, wget e Chrome per eseguire un test HTTPS Server. Tuttavia, durante i miei test, vedo che il callback verifyHostname viene chiamato 3-4 volte ogni connessione. Perché non funziona solo una volta?
themaestro

2
URLPath (blah) .netloc è sempre localhost: URLPath .__ init__ accetta singoli componenti URL, stai passando un intero URL come "schema" e ottieni il netloc predefinito di 'localhost' per accompagnarlo. Probabilmente volevi usare URLPath.fromString (url) .netloc. Sfortunatamente questo espone il check in verifyHostName all'indietro: inizia a rifiutare https://www.google.com/perché uno dei soggetti è "www.google.com", facendo sì che la funzione restituisca False. Probabilmente significava restituire True (accettato) se i nomi corrispondono e False se non lo fanno?
mzz

25

PycURL lo fa magnificamente.

Di seguito è riportato un breve esempio. Verrà lanciato un messaggio pycurl.errorse qualcosa è strano, dove si ottiene una tupla con codice di errore e un messaggio leggibile dall'uomo.

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

Probabilmente vorrai configurare più opzioni, come dove memorizzare i risultati, ecc. Ma non c'è bisogno di ingombrare l'esempio con elementi non essenziali.

Esempio di quali eccezioni potrebbero essere sollevate:

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

Alcuni link che ho trovato utili sono libcurl-docs per setopt e getinfo.


15

O semplicemente semplifica la vita utilizzando la libreria delle richieste :

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

Qualche parola in più sul suo utilizzo.


10
L' certargomento è il certificato lato client, non un certificato server con cui eseguire il controllo. Vuoi usare l' verifyargomento.
Paŭlo Ebermann

2
le richieste vengono convalidate per impostazione predefinita . Non è necessario utilizzare l' verifyargomento, tranne per essere più esplicito o disabilitare la verifica.
Dr. Jan-Philip Gehrcke

1
Non è un modulo interno. È necessario eseguire le richieste di installazione di pip
Robert Townley

14

Ecco uno script di esempio che dimostra la convalida del certificato:

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: buona cattura; Ho finito per aggiungere anche il controllo del nome host e ho modificato la mia risposta per includere il codice che ho usato.
Eli Courtwright

Non riesco a raggiungere il link originale (cioè "questa pagina"). Si è spostato?
Matt Ball

@ Matt: Penso di sì, ma FWIW il collegamento originale non è necessario, poiché il mio programma di test è un esempio funzionante completo, autonomo. Mi sono collegato alla pagina che mi ha aiutato a scrivere quel codice poiché sembrava la cosa decente per fornire l'attribuzione. Ma poiché non esiste più, modifico il mio post per rimuovere il collegamento, grazie per averlo segnalato.
Eli Courtwright

Questo non funziona con gestori aggiuntivi come i gestori proxy a causa della connessione manuale del socket in CertValidatingHTTPSConnection.connect. Vedi questa richiesta di pull per i dettagli (e una correzione).
schlamar

2
Ecco una soluzione pulita e funzionante con backports.ssl_match_hostname.
schlamar

8

M2Crypto può eseguire la convalida . Puoi anche usare M2Crypto con Twisted, se lo desideri. Il client desktop Chandler utilizza Twisted per il networking e M2Crypto per SSL , inclusa la convalida del certificato.

Sulla base del commento di Glyphs sembra che M2Crypto esegua una verifica del certificato migliore per impostazione predefinita rispetto a ciò che puoi fare con pyOpenSSL attualmente, perché M2Crypto controlla anche il campo subjectAltName.

Ho anche scritto sul blog come ottenere i certificati forniti da Mozilla Firefox in Python e utilizzabili con le soluzioni SSL di Python.


4

Jython esegue la verifica dei certificati per impostazione predefinita, quindi utilizzando i moduli della libreria standard, ad esempio httplib.HTTPSConnection, ecc., Con jython verificherà i certificati e fornirà eccezioni per gli errori, ovvero identità non corrispondenti, certificati scaduti, ecc.

In effetti, devi fare del lavoro extra per fare in modo che jython si comporti come cpython, cioè per fare in modo che jython NON verifichi i certificati.

Ho scritto un post sul blog su come disabilitare il controllo dei certificati su jython, perché può essere utile nelle fasi di test, ecc.

Installazione di un provider di sicurezza affidabile su java e jython.
http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-java-and-jython/


2

Il codice seguente consente di beneficiare di tutti i controlli di convalida SSL (ad esempio, validità della data, catena di certificati CA ...) TRANNE un passaggio di verifica collegabile, ad esempio per verificare il nome host o eseguire altri passaggi di verifica del certificato aggiuntivi.

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 è un'interfaccia per la libreria OpenSSL. Dovrebbe fornire tutto ciò di cui hai bisogno.


OpenSSL non esegue la corrispondenza del nome host. È previsto per OpenSSL 1.1.0.
jww

-1

Avevo lo stesso problema ma volevo ridurre al minimo le dipendenze di terze parti (perché questo script una tantum doveva essere eseguito da molti utenti). La mia soluzione era concludere una curlchiamata e assicurarmi che il codice di uscita fosse 0. Ha funzionato come un fascino.


Direi stackoverflow.com/a/1921551/1228491 utilizzando pycurl è una soluzione molto meglio allora.
Marian
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.