Convalida i certificati SSL con Python
-
23-08-2019 - |
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?
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()
O semplicemente rendere la vita più facile utilizzando le richieste rel="nofollow"> libreria:
import requests
requests.get('https://somesite.com', cert='/path/server.crt', verify=True)
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.