web-dev-qa-db-ja.com

Pythonのリクエストから応答SSL証明書を取得する方法は?

requests の応答からSSL証明書を取得しようとしています。

これを行う良い方法は何ですか?

16

requestsは、このような低レベルのものを意図的にラップします。通常、実行したいのは 証明書が有効であることを確認する だけです。これを行うには、verify=Trueを渡します。非標準のcacertバンドルを使用する場合は、それも渡すことができます。例えば:

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

また、requestsは主に他のライブラリのラッパーのセットであり、ほとんどが urllib3 とstdlibの http.client (または2.xの場合はhttplibです。 )および ssl

時には、答えは下位レベルのオブジェクトを取得することです(たとえば、resp.rawurllib3.response.HTTPResponseです)。しかし、多くの場合、それは不可能です。

そして、これはそれらのケースの1つです。これまでに証明書を見る唯一のオブジェクトはhttp.client.HTTPSConnection(またはurllib3.connectionpool.VerifiedHTTPSConnectionですが、それは前者のサブクラスにすぎません)とssl.SSLSocketであり、それらのいずれも現時点では存在しませんリクエストが返されます。 (connectionpoolという名前が意味するように、HTTPSConnectionオブジェクトはプールに格納され、それが完了するとすぐに再利用できます。SSLSocketHTTPSConnectionのメンバーです。)

したがって、チェーンにデータをコピーできるように、パッチを適用する必要があります。これは次のように簡単な場合があります。

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

これはテストされていないため、保証はありません。それ以上のパッチが必要になる場合があります。

また、サブクラス化とオーバーライドは、モンキーパッチングよりもクリーンだと思われます(特にHTTPAdapterはサブクラス化するように設計されているため)。

または、urllib3requestsをフォークしてフォークを変更し、(これが正当に役立つと思われる場合は)アップストリームにプルリクエストを送信することもできます。

とにかく、今、あなたのコードから、あなたはこれを行うことができます:

resp.peercert

これにより、'subject'から返される'subjectAltName'キーとpyopenssl.WrappedSocket.getpeercertキーを含む辞書が得られます。代わりに証明書に関する詳細情報が必要な場合は、 この回答のクリストフヴァンデプラスの変形 を試してください。これにより、OpenSSL.crypto.X509オブジェクトを取得できます。ピア証明書チェーン全体を取得する場合は、 GoldenStakeの回答 を参照してください。

もちろん、証明書の検証に必要なすべての情報を渡すこともできますが、すでに最上位レベルを通過しているため、さらに簡単です。

20
abarnert

はじめに、 abarnertの回答 は非常に完全です。 Kalkran の提案された_connection-close_問題を追跡しているときに、peercertにSSLに関する詳細情報が含まれていないことが実際にわかりました証明書。

接続とソケット情報をさらに掘り下げ、次のような優れた関数を含むself.sock.connection.get_peer_certificate()関数を抽出しました。

  • CNのget_subject()
  • get_notAfter()およびget_notBefore()(有効期限)
  • get_serial_number()およびget_signature_algorithm()(暗号関連の技術詳細)
  • ...

これらは、システムにpyopensslがインストールされている場合にのみ使用できることに注意してください。内部では、_urllib3_は、使用可能な場合はpyopensslを使用し、それ以外の場合は標準ライブラリのsslモジュールを使用します。以下に示す_self.sock.connection_属性は、_self.sock_が_urllib3.contrib.pyopenssl.WrappedSocket_の場合にのみ存在し、_ssl.SSLSocket_の場合には存在しません。 _pip install pyopenssl_を使用してpyopensslをインストールできます。

これが完了すると、コードは次のようになります。

_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
_

結果に簡単にアクセスできます:

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

私のように、SSL証明書の警告を無視したい場合は、ファイルの先頭に以下を追加し、SSL検証は行わないでください。

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

みんなの素晴らしい答えをありがとう。

それは私にこの質問への答えを設計する上で役立ちました:

Python)で使用されるCAストアにカスタムCAルート証明書を追加する方法

アップデート2019-02-12

Cert Human:SSL Certificates for Humans を見てください-私の印象的な書き換え https://github.com/neozenith/get-ca-py プロジェクト- lifehackjim

元のリポジトリをアーカイブしました。

スタンドアロンスニペット

#! /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

これはまったくきれいではありませんが、うまくいきます:

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

まず、 abarnertの回答 は非常に完全です

ただし、ピア証明書チェーンを探している場合は、さらに別のコードにパッチを適用する必要があることを付け加えておきます。

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

その後、あなたはそれを受け入れられた答えと同じように呼び出すことができます

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

resp.peercertchain オブジェクトのTupleを含むOpenSSL.crypto.X509を取得します

2
GoldenStake