Python ile SSL sertifikalarını doğrulayın


85

Kurumsal intranetimizdeki bir dizi siteye HTTPS üzerinden bağlanan ve SSL sertifikalarının geçerli olduğunu doğrulayan bir komut dosyası yazmam gerekiyor; sürelerinin dolmadığını, doğru adres için verildiğini, vb. Bu siteler için kendi dahili kurumsal Sertifika Yetkilimizi kullanırız, bu nedenle sertifikaları doğrulamak için CA'nın genel anahtarına sahibiz.

Python, HTTPS kullanırken varsayılan olarak sadece SSL sertifikalarını kabul eder ve kullanır, bu nedenle bir sertifika geçersiz olsa bile, urllib2 ve Twisted gibi Python kitaplıkları sertifikayı mutlu bir şekilde kullanır.

Bir siteye HTTPS üzerinden bağlanmama ve sertifikasını bu şekilde doğrulamama izin verecek iyi bir kitaplık var mı?

Python'da bir sertifikayı nasıl doğrularım?


10
Twisted hakkındaki yorumunuz yanlış: Twisted, Python'un yerleşik SSL desteğini değil, pyopenssl'yi kullanıyor. HTTPS sertifikalarını HTTP istemcisinde varsayılan olarak doğrulamasa da, bir doğrulama bağlam fabrikası oluşturmak için getPage ve downloadPage için "contextFactory" bağımsız değişkenini kullanabilirsiniz. Aksine, bildiğim kadarıyla yerleşik "ssl" modülünün sertifika doğrulaması yapmaya ikna edilmesi mümkün değil.
Glyph

4
Python 2.6 ve sonraki sürümlerdeki SSL modülü ile kendi sertifika doğrulayıcınızı yazabilirsiniz. Optimal değil ama yapılabilir.
Heikki Toivonen

3
Durum değişti, Python artık varsayılan olarak sertifikaları doğrular. Aşağıya yeni bir cevap ekledim.
Dr Jan-Philip Gehrcke

Durum Twisted için de değişti (aslında Python için olduğundan biraz önce); 14.0 sürümünü kullanıyorsanız treqveya twisted.web.client.Agentbu tarihten sonra, Twisted sertifikaları varsayılan olarak doğrular.
Glyph

Yanıtlar:


19

Varsayılan olarak Python 2.7.9 / 3.4.3 sürümünden itibaren sertifika doğrulaması yapmaya çalışır.

Bu, okumaya değer olan PEP 467'de önerilmiştir: https://www.python.org/dev/peps/pep-0476/

Değişiklikler ilgili tüm stdlib modüllerini (urllib / urllib2, http, httplib) etkiler.

İlgili belgeler:

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

Bu sınıf artık tüm gerekli sertifika ve ana bilgisayar adı kontrollerini varsayılan olarak gerçekleştirir. Önceki doğrulanmamış davranışa dönmek için ssl._create_unverified_context (), bağlam parametresine geçirilebilir.

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

3.4.3 sürümünde değiştirildi: Bu sınıf artık varsayılan olarak gerekli tüm sertifika ve ana bilgisayar adı kontrollerini gerçekleştiriyor. Önceki doğrulanmamış davranışa dönmek için ssl._create_unverified_context (), bağlam parametresine geçirilebilir.

Yeni yerleşik doğrulamanın, sistem tarafından sağlanan sertifika veritabanına dayandığını unutmayın . Buna karşılık, istek paketi kendi sertifika paketini gönderir. Her iki yaklaşımın artıları ve eksileri , PEP 476'nın Güven veritabanı bölümünde tartışılmaktadır .


önceki python sürümü için sertifika doğrulamasını sağlamak için herhangi bir çözüm var mı? Python sürümü her zaman yükseltilemez.
vaab

iptal edilmiş sertifikaları doğrulamaz. Örneğin revoked.badssl.com
Raz

Ders kullanmak zorunlu HTTPSConnectionmu? Kullanıyordum SSLSocket. İle nasıl doğrulama yapabilirim SSLSocket? Buradapyopenssl açıklandığı gibi kullanmayı açıkça doğrulamalı mıyım?
anir

31

Python Paket Dizinine match_hostname(), Python 3.2 sslpaketindeki işlevi Python'un önceki sürümlerinde kullanılabilir hale getiren bir dağıtım ekledim .

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

Şununla kurabilirsiniz:

pip install backports.ssl_match_hostname

Ya da projenizde listelenen bir bağımlılık yapabilirsiniz setup.py. Her iki durumda da şu şekilde kullanılabilir:

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
Eksik bir şeyim var ... Lütfen yukarıdaki boşlukları doldurur musunuz veya eksiksiz bir örnek (Google gibi bir site için) verebilir misiniz?
smholloway

Örnek, Google'a erişmek için hangi kitaplığı kullandığınıza bağlı olarak farklı görünecektir, çünkü farklı kitaplıklar SSL soketini farklı yerlere koyar ve getpeercert()çıktının aktarılabilmesi için yönteminin çağrılması gereken SSL soketidir match_hostname().
Brandon Rhodes

12
Python adına herhangi birinin bunu kullanmak zorunda kalmasından utanıyorum. Python'un yerleşik SSL HTTPS kitaplıkları, sertifikaları varsayılan olarak kutunun dışında doğrulamamak tamamen çılgınca ve sonuç olarak şu anda orada kaç tane güvensiz sistemin olduğunu hayal etmek acı verici.
Glenn Maynard


26

Sertifikaları doğrulamak için Twisted'ı kullanabilirsiniz. Ana API CertificateOptions olarak sağlanabilir, contextFactoryörneğin, çeşitli fonksiyonlara argüman listenSSL ve StartTLS .

Ne yazık ki, ne Python ne de Twisted, HTTPS doğrulamasını gerçekten yapmak için gereken çok sayıda CA sertifikası veya HTTPS doğrulama mantığıyla birlikte gelmiyor. PyOpenSSL'deki bir sınırlama nedeniyle , henüz tam olarak doğru bir şekilde yapamazsınız, ancak neredeyse tüm sertifikaların bir commonName konusu içermesi sayesinde, yeterince yakınlaşabilirsiniz.

Burada, joker karakterleri ve subjectAltName uzantılarını yok sayan ve çoğu Ubuntu dağıtımında 'ca-sertifikaları' paketinde bulunan sertifika-yetki sertifikalarını kullanan, doğrulanan bir Twisted HTTPS istemcisinin naif bir örnek uygulaması verilmiştir. Favori geçerli ve geçersiz sertifika sitelerinizle deneyin :).

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

engellemesiz yapabilir misin?
sean riley

Teşekkürler; Şimdi bunu okuyup anladığıma göre bir notum var: geri aramaların hata olmadığında True, varsa False döndürmesi gerektiğini doğrulayın. CommonName localhost olmadığında kodunuz temelde bir hata döndürür. Bazı durumlarda bunu yapmak mantıklı olsa da, amaçladığınız şeyin bu olup olmadığından emin değilim. Bu cevabın gelecekteki okuyucularının yararına bunun hakkında bir yorum bırakacağımı düşündüm.
Eli Courtwright

Bu durumda "self.hostname", "localhost" değildir; not URLPath(url).netloc: Bu, secureGet'e iletilen URL'nin ana bilgisayar kısmı anlamına gelir. Başka bir deyişle, konunun ortakAdı'nın arayan tarafından talep edilenle aynı olup olmadığını kontrol etmektir.
Glyph

Bu test kodunun bir sürümünü çalıştırıyorum ve bir HTTPS Sunucusunu test etmek için Firefox, wget ve Chrome kullandım. Yine de test çalıştırmalarımda, callback confirmHostname'in her bağlantıda 3-4 kez çağrıldığını görüyorum. Neden sadece bir kez çalışmıyor?
themaestro

2
URLPath (blah) .netloc her zaman localhost'tur: URLPath .__ init__, tek tek url bileşenlerini alır, tüm bir url'yi "şema" olarak iletirsiniz ve onunla birlikte gitmek için "localhost" varsayılan ağını alırsınız. Muhtemelen URLPath.fromString (url) .netloc kullanmayı düşündünüz. Maalesef bu, doğrulamaHostName'deki kontrolün geriye dönük olduğunu ortaya çıkarır: Konulardan https://www.google.com/biri "www.google.com" olduğu için reddetmeye başlar ve işlevin False sonucunu vermesine neden olur. Muhtemelen isimler eşleşirse True (kabul edildi), eşleşmiyorsa False döndürmek anlamına mı geliyordu?
mzz

25

PycURL bunu çok güzel yapıyor.

Aşağıda kısa bir örnek var. Bir pycurl.errorşey balıklıysa, hata kodu ve insan tarafından okunabilir bir mesaj içeren bir tuple aldığınız yere atar .

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

Muhtemelen sonuçların nerede saklanacağı vb. Gibi daha fazla seçenek yapılandırmak isteyeceksiniz. Ancak örneği gerekli olmayanlarla karıştırmaya gerek yok.

Hangi istisnaların ortaya çıkabileceğine dair örnek:

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

Faydalı bulduğum bazı bağlantılar setopt ve getinfo için libcurl-docs.


15

Veya istekler kitaplığını kullanarak hayatınızı kolaylaştırın :

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

Kullanımı hakkında birkaç kelime daha.


10
Bağımsız certdeğişken, kontrol edilecek bir sunucu sertifikası değil, istemci tarafı sertifikasıdır. verifyArgümanı kullanmak istiyorsunuz .
Paŭlo Ebermann

2
istekleri varsayılan olarak doğrular . verifyDaha açık olmak veya doğrulamayı devre dışı bırakmak dışında argümanı kullanmaya gerek yoktur .
Dr Jan-Philip Gehrcke

1
Dahili bir modül değil. Pip yükleme isteklerini çalıştırmanız gerekiyor
Robert Townley

14

Sertifika doğrulamasını gösteren örnek bir komut dosyası:

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: İyi yakaladı; Ana bilgisayar adı kontrolünü de ekledim ve yanıtımı kullandığım kodu içerecek şekilde düzenledim.
Eli Courtwright

Orijinal bağlantıya (yani 'bu sayfa') ulaşamıyorum. Taşındı mı?
Matt Ball

@Matt: Sanırım öyle, ancak FWIW orijinal bağlantı gerekli değil, çünkü test programım eksiksiz, bağımsız, çalışan bir örnek. Bu kodu yazmama yardımcı olan sayfaya bağlantı verdim, çünkü bu, atıfta bulunmak için uygun bir şey gibi görünüyordu. Ancak artık mevcut olmadığından, bağlantıyı kaldırmak için gönderimi düzenleyeceğim, bunu belirttiğin için teşekkürler
Eli Courtwright

Bu, içindeki manuel soket bağlantısı nedeniyle proxy işleyicileri gibi ek işleyicilerle çalışmaz CertValidatingHTTPSConnection.connect. Ayrıntılar (ve bir düzeltme) için bu çekme isteğine bakın .
schlamar

2
İşte temizlenmiş ve çalışan bir çözüm backports.ssl_match_hostname.
schlamar

8

M2Crypto doğrulamayı yapabilir . Dilerseniz M2Crypto'yu Twisted ile de kullanabilirsiniz . Chandler masaüstü istemcisi ağ iletişimi için Twisted'ı ve SSL için M2Crypto kullanıyor , sertifika doğrulaması dahil olmak üzere .

Glyphs yorumuna göre, M2Crypto varsayılan olarak şu anda pyOpenSSL ile yapabileceğinizden daha iyi sertifika doğrulaması yapıyor gibi görünüyor, çünkü M2Crypto subjectAltName alanını da kontrol ediyor.

Ayrıca Mozilla Firefox'un Python'da birlikte gönderdiği ve Python SSL çözümleriyle kullanılabilen sertifikaları nasıl alacağımı da yazdım .


4

Jython varsayılan olarak sertifika doğrulaması yapar, bu nedenle standart kütüphane modüllerini, örneğin httplib.HTTPSConnection, vb. Jython ile kullanmak sertifikaları doğrular ve uyumsuz kimlikler, süresi dolmuş sertifikalar vb. Gibi hatalar için istisnalar verir.

Aslında, jython'un cpython gibi davranmasını sağlamak için fazladan iş yapmanız gerekir, yani jython'un sertifikaları doğrulamaması için.

Jython'da sertifika kontrolünün nasıl devre dışı bırakılacağına dair bir blog yazısı yazdım, çünkü test aşamalarında vb. Yararlı olabilir.

Java ve jython'a tamamen güvenen bir güvenlik sağlayıcısı kurma.
http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-java-and-jython/


2

Aşağıdaki kod, örneğin ana bilgisayar adını doğrulamak veya diğer ek sertifika doğrulama adımlarını gerçekleştirmek için takılabilir bir doğrulama adımı HARİÇ tüm SSL doğrulama kontrollerinden (örn. Tarih geçerliliği, CA sertifika zinciri ...) yararlanmanıza olanak tanır.

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 kitaplığı için bir arayüzdür. İhtiyacınız olan her şeyi sağlamalıdır.


OpenSSL, ana bilgisayar adı eşleştirmesi gerçekleştirmez. OpenSSL 1.1.0 için planlanmıştır.
jww

-1

Aynı sorunu yaşıyordum ancak 3. taraf bağımlılıklarını en aza indirmek istiyordum (çünkü bu tek seferlik komut dosyası birçok kullanıcı tarafından yürütülecekti). Çözümüm, bir curlaramayı sarmak ve çıkış kodunun olduğundan emin olmaktı 0. Bir cazibe gibi çalıştı.


Stackoverflow.com/a/1921551/1228491'in pycurl kullanmanın o zaman çok daha iyi bir çözüm olduğunu söyleyebilirim .
Marian
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.