Domanda

Ho bisogno di scrivere uno script che si connette a un gruppo di siti sulla nostra intranet aziendale tramite HTTPS e verifica che i loro certificati SSL sono validi; che non sono scaduti, che sono rilasciati per l'indirizzo corretto, ecc usare la nostra autorità di certificazione aziendale interna per questi siti, quindi abbiamo la chiave pubblica del CA per verificare i certificati contro.

Python di default solo accetta e utilizza certificati SSL quando si utilizza HTTPS, quindi, anche se un certificato non è valido, librerie Python quali urllib2 e Twisted sarà solo felicemente utilizzare il certificato.

C'è una buona biblioteca da qualche parte che mi permette di connettersi a un sito tramite HTTPS e verificare il suo certificato in questo modo?

Come faccio a verificare un certificato in Python?

È stato utile?

Soluzione

A partire dalla versione 2.7.9 di rilascio / 3.4.3 su, Python di default cerca di eseguire la convalida dei certificati.

Questa è stata proposta nel PEP 467, che merita una lettura: https: //www.python. org / dev / PEP / pep-0476 /

I cambiamenti riguardano tutti i moduli rilevanti stdlib (urllib / urllib2, http, httplib).

documentazione pertinente:

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

  

Questa classe esegue ora tutti i necessari certificati e hostname controlli per impostazione predefinita. Per ripristinare la ssl._create_unverified_context precedente, non verificata, comportamento () può essere passato al parametro di contesto.

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 certificati e hostname per impostazione predefinita. Per ripristinare la ssl._create_unverified_context precedente, non verificata, comportamento () può essere passato al parametro di contesto.

Si noti che la nuova verifica built-in si basa sulla di database fornito dal sistema di certificazione. Opposto a questo, il richiede navi pacchetto il proprio fascio certificato. Pro e contro di entrambi gli approcci sono discussi nel database di fiducia di PEP 476 .

Altri suggerimenti

Ho aggiunto una distribuzione al Python Package Index, che rende la funzione match_hostname() dal pacchetto di 3,2 ssl Python disponibile per le versioni precedenti di Python.

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

È possibile installarlo con:

pip install backports.ssl_match_hostname

In alternativa si può rendere una dipendenza elencati nel setup.py del progetto. In entrambi i casi, può essere utilizzato 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:
    ...

È possibile utilizzare ritorto per verificare i certificati. L'API principale è CertificateOptions , che può essere fornito come argomento contextFactory a varie funzioni come listenSSL e STARTTLS .

Purtroppo, né Python nè torto viene fornito con un mucchio di certificati CA necessari per fare in realtà HTTPS convalida, né la logica di convalida HTTPS. A causa di una limitazione nella pyOpenSSL , non si può fare completamente correttamente appena ancora, ma grazie al fatto che quasi tutti i certificati includono un soggetto commonName, è possibile ottenere abbastanza vicino.

Ecco un esempio di implementazione ingenuo di un cliente ritorto HTTPS verificando che ignora i caratteri jolly e le estensioni subjectAltName, e utilizza i certificati su certificati di autorità presenti nel pacchetto 'ca-certificates' nella maggior parte delle distribuzioni di Ubuntu. Prova con i tuoi siti 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()

pycurl fa questo in modo bello.

Di seguito è riportato un breve esempio. Sarà gettare un pycurl.error se qualcosa è pescoso, dove si ottiene una tupla con codice di errore e un messaggio leggibile.

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 si desidera configurare più opzioni, come dove memorizzare i risultati, ecc Ma non c'è bisogno di ingombrare l'esempio con i non-essenziali.

Esempio di ciò che potrebbero essere sollevate eccezioni:

(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 i libcurl-docs per setopt e getinfo.

Ecco uno script di esempio che dimostra la convalida dei certificati:

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

M2Crypto può fare la convalida . È inoltre possibile utilizzare M2Crypto con Twisted , se volete. Il client desktop Chandler utilizza ritorto per il networking e M2Crypto per SSL, tra cui la convalida dei certificati.

In base a Glifi commentare sembra M2Crypto fa meglio la verifica del certificato di default di quello che si può fare con pyOpenSSL attualmente, a causa M2Crypto controlla campo subjectAltName troppo.

Inoltre ho bloggato su come ottenere i certificati navi di Mozilla Firefox con in Python ed utilizzabile con le soluzioni SSL Python.

Jython FA effettuare la verifica del certificato di default, in modo da utilizzare i moduli della libreria standard, per esempio httplib.HTTPSConnection, ecc, con Jython verificherà i certificati e dare eccezioni per i guasti, vale a dire le identità non corrispondenti, scaduto certs, ecc.

In realtà, quello che dovete fare qualche lavoro extra per arrivare Jython a comportarsi come CPython, vale a dire per arrivare a Jython non verifica certs.

ho scritto un post sul blog su come disabilitare il certificato controllo su Jython, perché può essere utile in fasi di test, ecc.

L'installazione di un provider di protezione all-confidando su Java e Jython.
http: //jython.xhaus. com / installazione-an-all-confidando-security-provider-on-java-e-Jython /

Il seguente codice consente di beneficiare di tutti i controlli di convalida SSL (ad esempio data di validità, CA catena di certificati ...) ad eccezione di una fase di verifica per esempio pluggable per verificare il nome host o fare altre operazioni ulteriore verifica del certificato.

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

pyOpenSSL è un'interfaccia alla libreria OpenSSL. Essa dovrebbe fornire tutto il necessario.

ho avuto lo stesso problema, ma volevo di ridurre al minimo le dipendenze 3rd parti (perché questo script una tantum è stato che deve essere eseguito da molti utenti). La mia soluzione era quella di avvolgere una chiamata curl e assicurarsi che il codice di uscita è stata 0. Ha lavorato come un fascino.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top