Der Versuch, das SSL-Zertifikat aus einer Antwort in requests abzurufen.

Was ist ein guter Weg, um dies zu tun?

24
Juan Carlos Coto 3 Juni 2013 im 22:37

5 Antworten

Beste Antwort

requests schließt absichtlich solche Dinge auf niedriger Ebene ab. Normalerweise möchten Sie nur überprüfen dass die Zertifikate gültig sind. Übergeben Sie dazu einfach verify=True. Wenn Sie ein nicht standardmäßiges Cacert-Bundle verwenden möchten, können Sie dies auch übergeben. Beispielsweise:

resp = requests.get('https://example.com', verify=True, cert=['/path/to/my/ca.crt'])

Außerdem ist requests in erster Linie eine Reihe von Wrappern für andere Bibliotheken, hauptsächlich urllib3 und die http.client der stdlib (oder für 2.x. , httplib) und ssl.

Manchmal besteht die Antwort nur darin, an die untergeordneten Objekte zu gelangen (z. B. resp.raw ist das urllib3.response.HTTPResponse), aber in vielen Fällen ist dies unmöglich.

Und dies ist einer dieser Fälle. Die einzigen Objekte, die jemals die Zertifikate sehen, sind ein http.client.HTTPSConnection (oder ein urllib3.connectionpool.VerifiedHTTPSConnection, aber das ist nur eine Unterklasse der ersteren) und ein ssl.SSLSocket, und keines davon existiert zu diesem Zeitpunkt mehr Die Anfrage kehrt zurück. (Wie der Name connectionpool andeutet, wird das Objekt HTTPSConnection in einem Pool gespeichert und kann sofort wiederverwendet werden. Das SSLSocket ist ein Mitglied des HTTPSConnection .)

Sie müssen also Dinge patchen, damit Sie die Daten in der Kette kopieren können. Es kann so einfach sein:

HTTPResponse = requests.packages.urllib3.response.HTTPResponse
orig_HTTPResponse__init__ = HTTPResponse.__init__
def new_HTTPResponse__init__(self, *args, **kwargs):
    orig_HTTPResponse__init__(self, *args, **kwargs)
    try:
        self.peercert = self._connection.sock.getpeercert()
    except AttributeError:
        pass
HTTPResponse.__init__ = new_HTTPResponse__init__

HTTPAdapter = requests.adapters.HTTPAdapter
orig_HTTPAdapter_build_response = HTTPAdapter.build_response
def new_HTTPAdapter_build_response(self, request, resp):
    response = orig_HTTPAdapter_build_response(self, request, resp)
    try:
        response.peercert = resp.peercert
    except AttributeError:
        pass
    return response
HTTPAdapter.build_response = new_HTTPAdapter_build_response

Das ist ungetestet, also keine Garantien; Möglicherweise müssen Sie mehr als das patchen.

Außerdem wäre das Unterklassen und Überschreiben wahrscheinlich sauberer als das Monkeypatching (insbesondere da HTTPAdapter für die Unterklasse ausgelegt ist).

Oder, noch besser, indem Sie urllib3 und requests gabeln, Ihre Gabel modifizieren und (wenn Sie dies für legitim nützlich halten) Pull-Anfragen stromaufwärts senden.

Wie auch immer, jetzt können Sie anhand Ihres Codes Folgendes tun:

resp.peercert

Dies gibt Ihnen ein Diktat mit den Schlüsseln 'subject' und 'subjectAltName', wie von pyopenssl.WrappedSocket.getpeercert zurückgegeben. Wenn Sie stattdessen weitere Informationen zum Zertifikat wünschen, versuchen Sie es mit Christophe Vandeplas 'Variante dieser Antwort, mit der Sie eine {{X3} erhalten. } Objekt. Informationen zum Abrufen der gesamten Peer-Zertifikatkette finden Sie unter Antwort von GoldenStake.

Natürlich möchten Sie möglicherweise auch alle Informationen weitergeben, die zur Überprüfung des Zertifikats erforderlich sind, aber das ist noch einfacher, da es bereits die oberste Ebene durchläuft.

19
Mark Amery 13 Jän. 2019 im 13:54

Zu Beginn ist die Antwort von abarnert sehr vollständig

Aber ich möchte hinzufügen, dass Sie für den Fall, dass Sie nach der Peer-Cert-Kette suchen, noch einen weiteren Code patchen müssen

import requests
sock_requests = requests.packages.urllib3.contrib.pyopenssl.WrappedSocket
def new_getpeercertchain(self,*args, **kwargs):
    x509 = self.connection.get_peer_cert_chain()
    return x509
sock_requests.getpeercertchain = new_getpeercertchain

Danach können Sie es auf sehr ähnliche Weise wie die akzeptierte Antwort aufrufen

HTTPResponse = requests.packages.urllib3.response.HTTPResponse
orig_HTTPResponse__init__ = HTTPResponse.__init__
def new_HTTPResponse__init__(self, *args, **kwargs):
    orig_HTTPResponse__init__(self, *args, **kwargs)
    try:
        self.peercertchain = self._connection.sock.getpeercertchain()
    except AttributeError:
        pass
HTTPResponse.__init__ = new_HTTPResponse__init__

HTTPAdapter = requests.adapters.HTTPAdapter
orig_HTTPAdapter_build_response = HTTPAdapter.build_response
def new_HTTPAdapter_build_response(self, request, resp):
    response = orig_HTTPAdapter_build_response(self, request, resp)
    try:
        response.peercertchain = resp.peercertchain
    except AttributeError:
        pass
    return response
HTTPAdapter.build_response = new_HTTPAdapter_build_response

Sie erhalten resp.peercertchain, das ein tuple von OpenSSL.crypto.X509 Objekte

2
GoldenStake 21 Dez. 2017 im 18:55

Zu Beginn ist die Antwort von abarnert sehr vollständig. Bei der Verfolgung der vorgeschlagenen connection-close Ausgabe von Kalkran stellte ich tatsächlich fest, dass die peercert keine enthielt detaillierte Informationen zum SSL-Zertifikat.

Ich habe mich eingehender mit den Verbindungs- und Socket-Informationen befasst und die self.sock.connection.get_peer_certificate() -Funktion extrahiert, die großartige Funktionen enthält wie:

  • get_subject() für CN
  • get_notAfter() und get_notBefore() für Ablaufdaten
  • get_serial_number() und get_signature_algorithm() für kryptobezogene technische Details
  • ...

Beachten Sie, dass diese nur verfügbar sind, wenn Sie pyopenssl auf Ihrem System installiert haben. Unter der Haube verwendet urllib3 pyopenssl, falls verfügbar, und ansonsten das ssl -Modul der Standardbibliothek. Das unten gezeigte Attribut self.sock.connection existiert nur, wenn self.sock ein urllib3.contrib.pyopenssl.WrappedSocket ist, nicht wenn es ein ssl.SSLSocket ist. Sie können pyopenssl mit pip install pyopenssl installieren.

Sobald dies erledigt ist, wird der Code:

import requests

HTTPResponse = requests.packages.urllib3.response.HTTPResponse
orig_HTTPResponse__init__ = HTTPResponse.__init__
def new_HTTPResponse__init__(self, *args, **kwargs):
    orig_HTTPResponse__init__(self, *args, **kwargs)
    try:
        self.peer_certificate = self._connection.peer_certificate
    except AttributeError:
        pass
HTTPResponse.__init__ = new_HTTPResponse__init__

HTTPAdapter = requests.adapters.HTTPAdapter
orig_HTTPAdapter_build_response = HTTPAdapter.build_response
def new_HTTPAdapter_build_response(self, request, resp):
    response = orig_HTTPAdapter_build_response(self, request, resp)
    try:
        response.peer_certificate = resp.peer_certificate
    except AttributeError:
        pass
    return response
HTTPAdapter.build_response = new_HTTPAdapter_build_response

HTTPSConnection = requests.packages.urllib3.connection.HTTPSConnection
orig_HTTPSConnection_connect = HTTPSConnection.connect
def new_HTTPSConnection_connect(self):
    orig_HTTPSConnection_connect(self)
    try:
        self.peer_certificate = self.sock.connection.get_peer_certificate()
    except AttributeError:
        pass
HTTPSConnection.connect = new_HTTPSConnection_connect

Sie können leicht auf das Ergebnis zugreifen:

r = requests.get('https://yourdomain.tld', timeout=0.1)
print('Expires on: {}'.format(r.peer_certificate.get_notAfter()))
print(dir(r.peer_certificate))

Wenn Sie wie ich SSL-Zertifikat-Warnungen ignorieren möchten, fügen Sie einfach oben in der Datei Folgendes hinzu und überprüfen Sie SSL nicht:

from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

r = requests.get('https://yourdomain.tld', timeout=0.1, verify=False)
print(dir(r.peer_certificate))
9
Mark Amery 15 Jän. 2019 im 14:06

Dies funktioniert, obwohl überhaupt nicht schön:

import requests

req = requests.get('https://httpbin.org')
pool = req.connection.poolmanager.connection_from_url('https://httpbin.org')
conn = pool.pool.get()
# get() removes it from the pool, so put it back in
pool.pool.put(conn)
print(conn.sock.getpeercert())
3
t-8ch 3 Juni 2013 im 19:44

Vielen Dank für die tollen Antworten aller.

Es hat mir geholfen, eine Antwort auf diese Frage zu finden:

Wie füge ich dem von Python in Windows verwendeten CA Store ein benutzerdefiniertes CA-Stammzertifikat hinzu?

UPDATE 2019-02-12

Bitte werfen Sie einen Blick auf Cert Human: SSL-Zertifikate für Menschen, um eine beeindruckende Neufassung meiner https://github.com/neozenith/get-ca-py Projekt von lifehackjim.

Ich habe jetzt das ursprüngliche Repository archiviert.

Stand Alone Snippet

#! /usr/bin/env python
# -*- coding: utf-8 -*-
"""
Get Certificates from a request and dump them.
"""

import argparse
import sys

import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning

requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

"""
Inspired by the answers from this Stackoverflow question:
https://stackoverflow.com/questions/16903528/how-to-get-response-ssl-certificate-from-requests-in-python

What follows is a series of patching the low level libraries in requests.
"""

"""
https://stackoverflow.com/a/47931103/622276
"""

sock_requests = requests.packages.urllib3.contrib.pyopenssl.WrappedSocket


def new_getpeercertchain(self, *args, **kwargs):
    x509 = self.connection.get_peer_cert_chain()
    return x509


sock_requests.getpeercertchain = new_getpeercertchain

"""
https://stackoverflow.com/a/16904808/622276
"""

HTTPResponse = requests.packages.urllib3.response.HTTPResponse
orig_HTTPResponse__init__ = HTTPResponse.__init__


def new_HTTPResponse__init__(self, *args, **kwargs):
    orig_HTTPResponse__init__(self, *args, **kwargs)
    try:
        self.peercertchain = self._connection.sock.getpeercertchain()
    except AttributeError:
        pass


HTTPResponse.__init__ = new_HTTPResponse__init__

HTTPAdapter = requests.adapters.HTTPAdapter
orig_HTTPAdapter_build_response = HTTPAdapter.build_response


def new_HTTPAdapter_build_response(self, request, resp):
    response = orig_HTTPAdapter_build_response(self, request, resp)
    try:
        response.peercertchain = resp.peercertchain
    except AttributeError:
        pass
    return response


HTTPAdapter.build_response = new_HTTPAdapter_build_response

"""
Attempt to wrap in a somewhat usable CLI
"""


def cli(args):
    parser = argparse.ArgumentParser(description="Request any URL and dump the certificate chain")
    parser.add_argument("url", metavar="URL", type=str, nargs=1, help="Valid https URL to be handled by requests")

    verify_parser = parser.add_mutually_exclusive_group(required=False)
    verify_parser.add_argument("--verify", dest="verify", action="store_true", help="Explicitly set SSL verification")
    verify_parser.add_argument(
        "--no-verify", dest="verify", action="store_false", help="Explicitly disable SSL verification"
    )
    parser.set_defaults(verify=True)

    return vars(parser.parse_args(args))


def dump_pem(cert, outfile="ca-chain.crt"):
    """Use the CN to dump certificate to PEM format"""
    PyOpenSSL = requests.packages.urllib3.contrib.pyopenssl
    pem_data = PyOpenSSL.OpenSSL.crypto.dump_certificate(PyOpenSSL.OpenSSL.crypto.FILETYPE_PEM, cert)
    issuer = cert.get_issuer().get_components()

    print(pem_data.decode("utf-8"))

    with open(outfile, "a") as output:
        for part in issuer:
            output.write(part[0].decode("utf-8"))
            output.write("=")
            output.write(part[1].decode("utf-8"))
            output.write(",\t")
        output.write("\n")
        output.write(pem_data.decode("utf-8"))


if __name__ == "__main__":
    cli_args = cli(sys.argv[1:])

    url = cli_args["url"][0]
    req = requests.get(url, verify=cli_args["verify"])
    for cert in req.peercertchain:
        dump_pem(cert)
3
Josh Peak 11 Feb. 2019 im 22:43