Не може да получи партньорски сертификат в клиент на Python, използвайки ssl.SSSLContext() на OpenSSL

Аз съм потребител на Windows. Използвам Python 3.6.5 и импортирам тази версия на OpenSSL OpenSSL 1.0.2k.

Трябва да напиша скрипт за Python TLS клиент, който мога да персонализирам по отношение на поддържаните TLS версии и шифрови пакети и други конфигурации. Клиентът трябва да може да прави връзки със самоподписани сертификати. Затова смятам, че трябва да използвам: ssl.SSLContext(), за да създам своя контекст, а не ssl.create_default_context().

Със следния скрипт обаче никога не мога да получа сертификата на партньора. Моля, дайте ясни отговори с код, тъй като в противен случай опитах много решения и погледнах предишните публикации без надежда.

context = ssl.SSLContext() # ssl.create_default_context() 
#context.verify_mode = ssl.CERT_NONE
#context.check_hostname = True
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
domain="google.com"
ssl_sock = context.wrap_socket(s, server_hostname=domain)
ssl_sock.connect((domain, 443))

print("====== peer's certificate ======")
try:
    cert = ssl_sock.getpeercert()
    print ("issued to:", dict(itertools.chain(*cert["subject"]))["commonName"])
    print ("issued by:", dict(itertools.chain(*cert["issuer"]))["commonName"])
    print("issuance date:", cert["notBefore"])
    print("expairy date: ", cert["notAfter"])
    if (cert == None):
        print("no certificate")

except Exception as e:
    print("Error:",e)
ssl_sock.close()

Проблемът е, че не получавам сертификата на партньора, когато използвам ssl.SSLContext(), но когато използвам ssl.create_default_context(), той се получава правилно. Трябва обаче да мога да получавам самоподписани сертификати (т.е. непроверени сертификати), затова трябва да използвам ssl.SSLContext().

Благодаря за публикуваното решение. Но трябва да анализирам сертификата, дори ако не е проверен (самоподписан). Вярвам на този сертификат и имам нужда от информацията за него. Разгледах няколко публикации, включително тази. Направих тези стъпки:

  1. Взех .pem съдържанието на сертификата на моя сървър.
  2. Навигирах до: C:\Python36\Lib\site-packages\certifi
  3. Отворих cacert.pem, който е поставен в директорията (стъпка 2)
  4. Добавих .pem съдържание на сертификата на моя сървър, което започва с: -----BEGIN CERTIFICATE----- и завършва с -----END CERTIFICATE-----

Получавам тази грешка:

ssl.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:833)

person None    schedule 27.04.2018    source източник


Отговори (3)


Ето фрагмент от 9 реда, който грабва данни за сертификати от всеки URL адрес.

import ssl
import socket

def getcertmeta(url="",port=443):
    hostname = socket.gethostbyaddr(url)[0]
    context = ssl.create_default_context()
    with socket.create_connection((hostname, port)) as sock:
        with context.wrap_socket(sock, server_hostname=hostname) as ssock:
            return ssock.context.get_ca_certs()

Бележка от калаено фолио: Нещо подозрително става с тези неща със сертификати. Те са източник на всякакви проблеми, но технологичните компании са обсебени от тях. Те не осигуряват никаква полза за сигурността, всеки един орган за сертифициране е бил хакнат (с изключение на LetsEncrypt iirc) това е неопровержимо и може да се търси и намери онлайн за idk 5 минути, има нещо друго, което се случва, но те са толкова дяволски сложни и ненужно сложни, че е много трудно да разбера какво. Имо, те се използват заедно с intel ME за задна врата на сървърни данни към централни местоположения (като хака, разкрит от wileaks v7), сертификатите на телефона ми включват като 3-4 правителства, просто направо казва японското правителство или китайското правителство и има куп компании за разузнавателни агенции като "starlight technology" всичко е много сенчесто imo.

РЕДАКТИРАНЕ 2: Горният фрагмент е грешен, той получава само сертификатите на сертифициращия орган, а не действителния URL адрес, ето фрагмент от 13 реда за получаване на сертификата за действителния URL адрес.

import socket,ssl
from contextlib import contextmanager
@contextmanager
def fetch_certificate(url="", port=443, timeout=0.5):
    cxt = ssl.create_default_context()
    sslctxsock = cxt.wrap_socket(socket.socket(), server_hostname=hostname)
    sslctxsock.settimeout(timeout)
    sslctxsock.connect((url,port))
    cert = sslctxsock.getpeercert()
    try:
        yield cert
    finally:
        sslctxsock.close()

Използвайте го така:

with fetch_certificate(url=url) as cert:
   print(cert)
person SpellsOfTruth    schedule 08.06.2020

След редица опити, някои неуспешни, някои частично успешни, намерих начин, който трябва да работи (все пак не го тествах със самоподписани сертификати). Също така изтрих всичко от предишните опити.

Има 2 необходими стъпки:

  1. #P3#
    '-----BEGIN CERTIFICATE-----'
    'MIIIPjCCByagAwIBAgIICG/ofYt2G48wDQYJKoZIhvcNAQELBQAwSTELMAkGA1UE`
    'BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl'
    
    ...
    
    'L2KuOvWZ40sTVCJdWPUMtT9VP7VHfLNTFft/IhR+bUPkr33xjOa0Idq6cL89oufn'
    '-----END CERTIFICATE-----'
    
  2. Декодирайте сертификата с помощта на (!!!недокументиран!!!) ssl._ssl._test_decode_cert (присъства в Python 3 / Python 2< /em>)

  3. Due to the fact that ssl._ssl._test_decode_cert can only read the certificate from a file, 2 additional steps are needed:
    • Save the certificate from #1. in a temporary file (before #2., obviously)
    • Изтрийте този файл, когато приключите с него

Бих искал да наблегна на [Python 3.Docs] : SSLSocket.getpeercert(binary_form=False), който съдържа много информация (която пропуснах последния път(и)).
Също така разбрах за ssl._ssl._test_decode_cert, като разгледах имплементацията на SSLSocket.getpeercert ("${PYTHON_SRC_DIR}/Modules/_ssl.c").

code00.py:

#!/usr/bin/env python3

import sys
import os
import socket
import ssl
import itertools


def _get_tmp_cert_file_name(host, port):
    return os.path.join(os.path.dirname(os.path.abspath(__file__)), "_".join(("cert", host, str(port), str(os.getpid()), ".crt")))


def _decode_cert(cert_pem, tmp_cert_file_name):
    #print(tmp_cert_file_name)
    with open(tmp_cert_file_name, "w") as fout:
        fout.write(cert_pem)
    try:
        return ssl._ssl._test_decode_cert(tmp_cert_file_name)
    except Exception as e:
        print("Error decoding certificate:", e)
        return dict()
    finally:
        os.unlink(tmp_cert_file_name)


def get_srv_cert_0(host, port=443):
    try:
        cert_pem = ssl.get_server_certificate((host, port))
    except Exception as e:
        print("Error getting certificate:", e)
        return dict()
    tmp_cert_file_name = _get_tmp_cert_file_name(host, port)
    return _decode_cert(cert_pem, tmp_cert_file_name)


def get_srv_cert_1(host, port=443):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    context = ssl.SSLContext()
    ssl_sock = context.wrap_socket(sock, server_hostname=host)
    try:
        ssl_sock.connect((host, port))
    except Exception as e:
        print("Error connecting:\n", e)
        return dict()
    try:
        cert_der = ssl_sock.getpeercert(True)  # NOTE THE ARGUMENT!!!
    except Exception as e:
        print("Error getting cert:\n", e)
        return dict()
    tmp_cert_file_name = _get_tmp_cert_file_name(host, port)
    return _decode_cert(ssl.DER_cert_to_PEM_cert(cert_der), tmp_cert_file_name)


def main(argv):
    domain = "google.com"
    if argv:
        print("Using custom method")
        get_srv_cert_func = get_srv_cert_1
    else:
        print("Using regular method")
        get_srv_cert_func = get_srv_cert_0

    cert = get_srv_cert_func(domain)
    print("====== peer's certificate ======")
    try:
        print("Issued To:", dict(itertools.chain(*cert["subject"]))["commonName"])
        print("Issued By:", dict(itertools.chain(*cert["issuer"]))["commonName"])
        print("Valid From:", cert["notBefore"])
        print("Valid To:", cert["notAfter"])
        if (cert == None):
            print("no certificate")
    except Exception as e:
        print("Error getting certificate:", e)


if __name__ == "__main__":
    print("Python {:s} on {:s}\n".format(sys.version, sys.platform))
    main(sys.argv[1:])

Бележки:

  • _get_tmp_cert_file_name: генерира името на временния файл (разположен в същия каталог като скрипта), който ще съхранява сертификата
  • _decode_cert: записва сертификата във файла, след което декодира файла и връща получения dict
  • get_srv_cert_0: получава сървъра за формуляр на сертификат, след което го декодира
  • get_srv_cert_1: same thing that get_srv_cert_0 does, but "manually"
    • Its advantage is controlling the SSL context creation / manipulation (which I think was the main point of the question)
  • main:
    • Gets the server certificate using one of the 2 methods above (based on an argument being / not being passed to the script)
    • Отпечатва данни за сертификат (вашият код с някои малки корекции)

Резултат:

(py35x64_test) e:\Work\Dev\StackOverflow\q050055935>"e:\Work\Dev\VEnvs\py35x64_test\Scripts\python.exe" code00.py
Python 3.5.4 (v3.5.4:3f56838, Aug  8 2017, 02:17:05) [MSC v.1900 64 bit (AMD64)] on win32

Using regular method
====== peer's certificate ======
Issued To: *.google.com
Issued By: Google Internet Authority G2
Valid From: Apr 10 18:58:05 2018 GMT
Valid To: Jul  3 18:33:00 2018 GMT

(py35x64_test) e:\Work\Dev\StackOverflow\q050055935>"e:\Work\Dev\VEnvs\py35x64_test\Scripts\python.exe" code00.py 1
Python 3.5.4 (v3.5.4:3f56838, Aug  8 2017, 02:17:05) [MSC v.1900 64 bit (AMD64)] on win32

Using custom method
====== peer's certificate ======
Issued To: *.google.com
Issued By: Google Internet Authority G2
Valid From: Apr 10 18:55:13 2018 GMT
Valid To: Jul  3 18:33:00 2018 GMT

Проверете [SO]: Как мога да декодирам SSL сертификат с помощта на python? (Отговорът на @CristiFati) само за декодиращата част.

person CristiFati    schedule 27.04.2018
comment
Благодаря. За съжаление това не е това, което искам. Имам нужда от моя скрипт на python, за да мога да се доверя на (непроверения) самоподписан сертификат на моя сървър. Как да стане това? - person None; 27.04.2018
comment
Виждам. Само вашия сървър ли сте насочили или който и да е сървър в света? - person CristiFati; 27.04.2018
comment
Засега само този сървър. - person None; 27.04.2018
comment
Имам сертификат на сървъра. Но ще бъде полезно, ако отговорите и на двата случая. Може би в бъдеще трябва да обмисля самоподписан сертификат на всеки сървър. - person None; 27.04.2018
comment
Публикувах съвсем различно решение. Надявам се да работи за вас. - person CristiFati; 27.04.2018
comment
Ще го опитам, но намирам много по-прост начин, който изпълни целта ми. Аз също ще го публикувам. - person None; 02.05.2018
comment
FYI: Написах кода такъв, какъвто е (с функции, обработка на грешки и т.н. - което може да създаде погрешно впечатление за сложност), така че да е модулен и правилен от PoV< на дизайна /i>. Декодирането на сертификат може да се извърши в ~5 реда код (ако това означава, че е по-просто). - person CristiFati; 11.05.2018
comment
@None: Това отговори ли на въпроса ви? Ако да, моля, приемете отговора, така че и другите да могат да го потвърдят ([SO]: Какво трябва да направя, когато някой отговори въпросът ми?). - person CristiFati; 12.09.2019
comment
Този код е потенциално опасен, тъй като предполага, че get_server_certificate говори с правилния сървър (което не можете да знаете, освен ако не проверите сертификата - за какво служи CA). Някой може да MITM на тази стъпка и вие ще получите грешен сертификат и ще му се доверите сляпо. Имайки предвид това, ако имате доверие на мрежата (като цяло не е добра идея), можете просто да използвате HTTP и да пропуснете TLS - person nijave; 21.06.2021

Изглежда, че Python не анализира/съхранява сертификата на клиента, когато е зададено ssl.CERT_NONE, което е по подразбиране за SSLContext

import socket
import ssl

dest = ("www.google.com", 443)

sock = socket.socket(socket.AF_INET)
ctx = ssl.SSLContext()

# You will either need to add the self-signed certs to the OS cert store (varies by OS)
# See https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_verify_locations for loading non-defaults
ctx.set_default_verify_paths()

ctx.verify_mode = ssl.CERT_REQUIRED
ssock = ctx.wrap_socket(sock, server_hostname=dest[0])
result = ssock.connect(dest)
print(ssock.getpeercert())

Вижте https://github.com/python/cpython/blob/main/Modules/_ssl.c#L1823 Работи побитово &, така че когато режимът на проверка е CERT_NONE (0), тогава кодовият блок се пропуска. CERT_OPTIONAL=1 или CERT_REQUIRED=2 са необходими, за да работи анализирането/попълването на сертификата (в противен случай просто задава празен dict)

За вашите самоподписани сертификати трябва или да ги импортирате в хранилището на сертифициращия орган на OS, или да използвате метода SSLContext.load_verify_locations, за да заредите самоподписания сертификат (тъй като той е самоподписан, можете да заредите сървърния/листовия сертификат като CA сертификат)

Ако отидете по пътя на OS-store, вероятно можете просто да използвате контекста по подразбиране. Мисля, че в Windows има CA магазин за потребител (все пак не е положително). Не мисля, че такова нещо съществува в Linux, така че ще трябва да го добавите за цялата система. Не съм сигурен за macOS или други операционни системи

person nijave    schedule 21.06.2021