├── .gitignore ├── README.md ├── boring_backend.py ├── core_h2.py ├── frozen_requirements.txt ├── http11_client.py ├── requests_example.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## BoringSSL with Python CFFI to look like Chrome 2 | 3 | Using `requests` in `requests_example.py`, you can see by default 43 suites are sent in the Client Hello, which doesn't look anything like Chrome's 16 (or Firefox). Using `set_ciphers`, you can get closer, but the order isn't the same, and GREASE isn't included. 4 | 5 | `Requests` https://github.com/psf/requests uses `urllib3` https://github.com/urllib3/urllib3 which depends on `pyOpenSSL` / `cryptography`, which uses Python's standard library SSL module https://docs.python.org/3/library/ssl.html . It's likely to be linked to OpenSSL 1.1.1, but that probably depends on your OS. 6 | 7 | Go has its own standard TLS library, so it was forked to make modifications https://github.com/refraction-networking/utls . You could call a Go executable from python, but that might not cleanly integrate with your existing python code. 8 | 9 | Looking for alternative TLS libraries, Chrome uses BoringSSL, and Firefox uses NSS. BoringSSL is a fork of OpenSSL, so it should be easier to swap in than NSS. 10 | 11 | I figured that if I built BoringSSL as a shared library, I should be able to use it from Python via ctypes/cffi/cython. I used manylinux and Github Actions to create a binary wheel and upload it to testpypi: https://github.com/jonatron/boringssl/commit/75241956b3d748888ae4906f45ded14b120dc999 . There's only a wheel for x86_64 linux python3.6 currently, but it should be widely compatible. 12 | 13 | I validated the idea by writing a very basic HTTP1.1 client using a raw socket, in `http11_client.py`. 14 | 15 | Looking at alternatives to requests, I found the excellent https://github.com/encode/httpcore and https://github.com/encode/httpx/ . 16 | It was easy to write a NetworkBackend for httpcore, `boring_backend.py` is less than 150 lines total. 17 | 18 | **This isn't ready to use, for example certificate validation and closing sockets is missing.** 19 | 20 | You can try out `core_h2.py` by running: 21 | ``` 22 | python3 -m venv venv 23 | source venv/bin/activate 24 | pip install -i https://test.pypi.org/simple/ boringssl-binary-build 25 | pip install -r requirements.txt 26 | python core_h2.py 27 | ``` 28 | 29 | In the future, if requirements.txt breaks, use frozen_requirements.txt . 30 | -------------------------------------------------------------------------------- /boring_backend.py: -------------------------------------------------------------------------------- 1 | import os 2 | import typing 3 | from types import SimpleNamespace 4 | 5 | import boringssl_binary_build 6 | from cffi import FFI 7 | from httpcore.backends.base import NetworkBackend, NetworkStream 8 | from httpcore import ConnectError 9 | 10 | ffi = FFI() 11 | 12 | ffi.cdef(""" 13 | 14 | //void * memset ( void * ptr, int value, size_t num ); 15 | 16 | // boringssl/include/openssl/base.h 17 | typedef struct ssl_st SSL; 18 | typedef struct ssl_ctx_st SSL_CTX; 19 | typedef struct ssl_method_st SSL_METHOD; 20 | 21 | typedef struct bio_st BIO; 22 | typedef struct bio_method_st BIO_METHOD; 23 | 24 | // boringssl/include/openssl/ssl.h 25 | SSL *SSL_new(SSL_CTX *ctx); 26 | SSL_CTX *SSL_CTX_new(const SSL_METHOD *method); 27 | void SSL_set_bio(SSL *ssl, BIO *rbio, BIO *wbio); 28 | int SSL_connect(SSL *ssl); 29 | const SSL_METHOD *TLS_method(void); 30 | 31 | int SSL_set_tlsext_host_name(SSL *ssl, const char *name); 32 | void SSL_CTX_set_grease_enabled(SSL_CTX *ctx, int enabled); 33 | int SSL_CTX_set_strict_cipher_list(SSL_CTX *ctx, const char *str); 34 | int SSL_CTX_set_cipher_list(SSL_CTX *ctx, const char *str); 35 | int SSL_CTX_set_alpn_protos(SSL_CTX *ctx, const uint8_t *protos, unsigned protos_len); 36 | void SSL_get0_alpn_selected(const SSL *ssl, const uint8_t **out_data, unsigned *out_len); 37 | int SSL_set_alpn_protos(SSL *ssl, const uint8_t *protos, unsigned protos_len); 38 | int SSL_write(SSL *ssl, const void *buf, int num); 39 | int SSL_read(SSL *ssl, void *buf, int num); 40 | 41 | int SSL_do_handshake(SSL *ssl); 42 | int SSL_get_error(const SSL *ssl, int ret_code); 43 | 44 | // BIO = basic input output 45 | // include/openssl/bio.h 46 | BIO *BIO_new(const BIO_METHOD *method); 47 | BIO *BIO_new_socket(int fd, int close_flag); 48 | BIO *BIO_new_connect(const char *host_and_optional_port); 49 | 50 | int BIO_write_all(BIO *bio, const void *data, size_t len); 51 | int BIO_read(BIO *bio, void *data, int len); 52 | """) 53 | 54 | BIO_NOCLOSE = 0 55 | BIO_CLOSE = 1 56 | 57 | lib_path = os.path.join(boringssl_binary_build.__path__[0], '..', 'boringssl.cpython-36m-x86_64-linux-gnu.so') 58 | bssl = ffi.dlopen(lib_path) 59 | 60 | 61 | class BoringStream(NetworkStream): 62 | def __init__(self, ssl_p, ctx_p) -> None: 63 | self._ssl_p = ssl_p 64 | self._ctx_p = ctx_p 65 | 66 | def read(self, max_bytes: int, timeout: float = None) -> bytes: 67 | buf = max_bytes * b'\0' 68 | bytes_read = bssl.SSL_read(self._ssl_p, buf, max_bytes) 69 | print("bytes_read", bytes_read) 70 | # print("read buf", buf[:bytes_read]) 71 | return buf[:bytes_read] 72 | 73 | def write(self, buffer: bytes, timeout: float = None) -> None: 74 | if not buffer: 75 | return 76 | while buffer: 77 | # print("writing buffer", buffer) 78 | bytes_written = bssl.SSL_write(self._ssl_p, buffer, len(buffer)) 79 | print("bytes_written", bytes_written) 80 | buffer = buffer[bytes_written:] 81 | 82 | def close(self) -> None: 83 | pass 84 | 85 | def start_tls( 86 | self, 87 | ssl_context, 88 | server_hostname: str = None, 89 | timeout: float = None, 90 | ) -> NetworkStream: 91 | bssl.SSL_set_tlsext_host_name(self._ssl_p, server_hostname.encode('ascii')) 92 | 93 | ssl_connect_success = bssl.SSL_connect(self._ssl_p) 94 | print("ssl_connect_success", ssl_connect_success) 95 | 96 | client_ret = bssl.SSL_do_handshake(self._ssl_p) 97 | print("client_ret", client_ret) 98 | if client_ret: 99 | client_err = bssl.SSL_get_error(self._ssl_p, client_ret) 100 | print("client_err", client_err) 101 | 102 | return self 103 | 104 | def get_extra_info(self, info: str) -> typing.Any: 105 | if info == "ssl_object": 106 | buf = ffi.new("uint8_t[]", b"\0" * 10) 107 | out_data = ffi.new("uint8_t **", buf) 108 | out_len = ffi.new("unsigned *") 109 | bssl.SSL_get0_alpn_selected(self._ssl_p, out_data, out_len) 110 | if out_len[0]: 111 | proto = bytes(out_data[0][0:out_len[0]]).decode('ascii') 112 | print("negotiated proto", proto) 113 | return SimpleNamespace(selected_alpn_protocol=lambda: proto) 114 | return None 115 | 116 | 117 | class BoringBackend(NetworkBackend): 118 | 119 | def connect_tcp( 120 | self, host: str, port: int, timeout: float = None, local_address: str = None 121 | ) -> NetworkStream: 122 | address = (host, port) 123 | source_address = None if local_address is None else (local_address, 0) 124 | 125 | ctx_p = bssl.SSL_CTX_new(bssl.TLS_method()) 126 | ssl_p = bssl.SSL_new(ctx_p) 127 | bio_p = bssl.BIO_new_connect(f"{host}:{port}".encode('ascii')) 128 | bssl.SSL_set_bio(ssl_p, bio_p, bio_p) 129 | alpn = b'\x02h2\x08http/1.1' 130 | # alpn = b'\x08http/1.1' 131 | alpn_err = bssl.SSL_set_alpn_protos(ssl_p, alpn, len(alpn)) 132 | bssl.SSL_CTX_set_grease_enabled(ctx_p, 1) 133 | if alpn_err: 134 | raise ConnectError(f"SSL alpn set err: {alpn_err}") 135 | ciphers_err = bssl.SSL_CTX_set_strict_cipher_list(ctx_p, b"ALL:!aPSK:!ECDSA+SHA1:!3DES") 136 | 137 | return BoringStream(ssl_p, ctx_p) 138 | -------------------------------------------------------------------------------- /core_h2.py: -------------------------------------------------------------------------------- 1 | import httpcore 2 | from boring_backend import BoringBackend, BoringStream 3 | 4 | http = httpcore.ConnectionPool(network_backend=BoringBackend()) 5 | response = http.request("GET", "https://www.example.com/") 6 | print("response", response) 7 | print("response.content", response.content) 8 | print("response.headers", response.headers) 9 | -------------------------------------------------------------------------------- /frozen_requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==3.5.0 2 | certifi==2021.10.8 3 | cffi==1.15.0 4 | h11==0.12.0 5 | h2==4.1.0 6 | hpack==4.0.0 7 | httpcore==0.14.7 8 | hyperframe==6.0.1 9 | idna==3.3 10 | pycparser==2.21 11 | sniffio==1.2.0 12 | -------------------------------------------------------------------------------- /http11_client.py: -------------------------------------------------------------------------------- 1 | from cffi import FFI 2 | import os 3 | import socket 4 | 5 | import boringssl_binary_build 6 | ffi = FFI() 7 | 8 | # see client.cc 9 | 10 | ffi.cdef(""" 11 | // boringssl/include/openssl/base.h 12 | typedef struct ssl_st SSL; 13 | typedef struct ssl_ctx_st SSL_CTX; 14 | typedef struct ssl_method_st SSL_METHOD; 15 | 16 | typedef struct bio_st BIO; 17 | typedef struct bio_method_st BIO_METHOD; 18 | 19 | // boringssl/include/openssl/ssl.h 20 | SSL *SSL_new(SSL_CTX *ctx); 21 | SSL_CTX *SSL_CTX_new(const SSL_METHOD *method); 22 | void SSL_set_bio(SSL *ssl, BIO *rbio, BIO *wbio); 23 | int SSL_connect(SSL *ssl); 24 | const SSL_METHOD *TLS_method(void); 25 | 26 | int SSL_set_tlsext_host_name(SSL *ssl, const char *name); 27 | void SSL_CTX_set_grease_enabled(SSL_CTX *ctx, int enabled); 28 | int SSL_CTX_set_strict_cipher_list(SSL_CTX *ctx, const char *str); 29 | int SSL_CTX_set_cipher_list(SSL_CTX *ctx, const char *str); 30 | int SSL_set_alpn_protos(SSL *ssl, const uint8_t *protos, unsigned protos_len); 31 | 32 | int SSL_write(SSL *ssl, const void *buf, int num); 33 | int SSL_read(SSL *ssl, void *buf, int num); 34 | 35 | int SSL_do_handshake(SSL *ssl); 36 | int SSL_get_error(const SSL *ssl, int ret_code); 37 | 38 | // BIO = basic input output 39 | // include/openssl/bio.h 40 | BIO *BIO_new(const BIO_METHOD *method); 41 | BIO *BIO_new_socket(int fd, int close_flag); 42 | BIO *BIO_new_connect(const char *host_and_optional_port); 43 | 44 | int BIO_write_all(BIO *bio, const void *data, size_t len); 45 | int BIO_read(BIO *bio, void *data, int len); 46 | """) 47 | BIO_NOCLOSE = 0 48 | BIO_CLOSE = 1 49 | 50 | lib_path = os.path.join(boringssl_binary_build.__path__[0], '..', 'boringssl.cpython-36m-x86_64-linux-gnu.so') 51 | bssl = ffi.dlopen(lib_path) 52 | 53 | ctx_p = bssl.SSL_CTX_new(bssl.TLS_method()) 54 | ssl_p = bssl.SSL_new(ctx_p) 55 | 56 | ip_addr = socket.gethostbyname("example.com") 57 | bio_p = bssl.BIO_new_connect(f"{ip_addr}:443".encode('ascii')) 58 | 59 | bssl.SSL_set_bio(ssl_p, bio_p, bio_p) 60 | 61 | bssl.SSL_set_tlsext_host_name(ssl_p, "example.com".encode("ascii")) 62 | bssl.SSL_CTX_set_grease_enabled(ctx_p, 1) 63 | # search chrome for SSL_CTX_set_strict_cipher_list or SSL_CTX_set_cipher_list 64 | # https://github.com/chromium/chromium/blob/7f45ed9654759d01f8fa4aa289b5421843320b86/net/socket/ssl_client_socket_impl.cc#L869 65 | # std::string command("ALL:!aPSK:!ECDSA+SHA1:!3DES"); 66 | ciphers_err = bssl.SSL_CTX_set_strict_cipher_list(ctx_p, b"ALL:!aPSK:!ECDSA+SHA1:!3DES") 67 | # alpn 68 | alpn = b'\x08http/1.1' 69 | alpn_err = bssl.SSL_set_alpn_protos(ssl_p, alpn, len(alpn)) 70 | 71 | # client, calls do handshake 72 | ssl_connect_success = bssl.SSL_connect(ssl_p) 73 | print("ssl_connect_success", ssl_connect_success) 74 | 75 | client_ret = bssl.SSL_do_handshake(ssl_p) 76 | print("client_ret", client_ret) 77 | client_err = bssl.SSL_get_error(ssl_p, client_ret) 78 | print("client_err", client_err) 79 | 80 | get_data = b'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n' 81 | bytes_written = bssl.SSL_write(ssl_p, get_data, len(get_data)) 82 | print("bytes_written", bytes_written) 83 | 84 | tmpbuf_len = 256 85 | 86 | read_len = 256 87 | data = b'' 88 | headers_recieved = False 89 | while (read_len == tmpbuf_len) or not headers_recieved: 90 | headers_recieved = b'\r\n\r\n' in data 91 | tmpbuf = tmpbuf_len * b'\0' 92 | read_len = bssl.SSL_read(ssl_p, tmpbuf, tmpbuf_len) 93 | data += tmpbuf[:read_len] 94 | 95 | print(data) 96 | import pdb; pdb.set_trace() 97 | print("end") 98 | -------------------------------------------------------------------------------- /requests_example.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | 3 | import requests 4 | import requests.adapters 5 | 6 | CIPHERS = 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256' 7 | CIPHERS += ':ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305' 8 | CIPHERS += ':ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384' 9 | CIPHERS += ':ECDHE-ECDSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA' 10 | CIPHERS += ':ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA' 11 | CIPHERS += ':AES128-GCM-SHA256:AES256-GCM-SHA384' 12 | CIPHERS += ':AES128-SHA:AES256-SHA' 13 | 14 | 15 | class SSLContextAdapter(requests.adapters.HTTPAdapter): 16 | def init_poolmanager(self, *args, **kwargs): 17 | ssl_context = ssl.create_default_context() 18 | ssl_context.set_ciphers(CIPHERS) 19 | kwargs['ssl_context'] = ssl_context 20 | return super(SSLContextAdapter, self).init_poolmanager(*args, **kwargs) 21 | 22 | 23 | # use defaults 24 | resp = requests.get("https://example.com") 25 | print("resp 1", resp) 26 | 27 | # Cipher Suites (43 suites) 28 | # Cipher Suite: TLS_AES_256_GCM_SHA384 (0x1302) 29 | # Cipher Suite: TLS_CHACHA20_POLY1305_SHA256 (0x1303) 30 | # Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301) 31 | # Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (0xc02c) 32 | # Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030) 33 | # Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b) 34 | # Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f) 35 | # Cipher Suite: TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca9) 36 | # Cipher Suite: TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca8) 37 | # Cipher Suite: TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 (0x009f) 38 | # Cipher Suite: TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 (0x009e) 39 | # Cipher Suite: TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xccaa) 40 | # Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8 (0xc0af) 41 | # Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CCM (0xc0ad) 42 | # Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8 (0xc0ae) 43 | # Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CCM (0xc0ac) 44 | # Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 (0xc024) 45 | # Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 (0xc028) 46 | # Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 (0xc023) 47 | # Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 (0xc027) 48 | # Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a) 49 | # Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014) 50 | # Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009) 51 | # Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013) 52 | # Cipher Suite: TLS_DHE_RSA_WITH_AES_256_CCM_8 (0xc0a3) 53 | # Cipher Suite: TLS_DHE_RSA_WITH_AES_256_CCM (0xc09f) 54 | # Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CCM_8 (0xc0a2) 55 | # Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CCM (0xc09e) 56 | # Cipher Suite: TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 (0x006b) 57 | # Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 (0x0067) 58 | # Cipher Suite: TLS_DHE_RSA_WITH_AES_256_CBC_SHA (0x0039) 59 | # Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA (0x0033) 60 | # Cipher Suite: TLS_RSA_WITH_AES_256_GCM_SHA384 (0x009d) 61 | # Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c) 62 | # Cipher Suite: TLS_RSA_WITH_AES_256_CCM_8 (0xc0a1) 63 | # Cipher Suite: TLS_RSA_WITH_AES_256_CCM (0xc09d) 64 | # Cipher Suite: TLS_RSA_WITH_AES_128_CCM_8 (0xc0a0) 65 | # Cipher Suite: TLS_RSA_WITH_AES_128_CCM (0xc09c) 66 | # Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA256 (0x003d) 67 | # Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA256 (0x003c) 68 | # Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0x0035) 69 | # Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f) 70 | # Cipher Suite: TLS_EMPTY_RENEGOTIATION_INFO_SCSV (0x00ff) 71 | 72 | 73 | s = requests.Session() 74 | s.mount('https://example.com', SSLContextAdapter()) 75 | resp = s.get('https://example.com') 76 | print("resp 2", resp) 77 | 78 | # Cipher Suites (18 suites) 79 | # Cipher Suite: TLS_AES_256_GCM_SHA384 (0x1302) 80 | # Cipher Suite: TLS_CHACHA20_POLY1305_SHA256 (0x1303) 81 | # Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301) 82 | # Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b) 83 | # Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f) 84 | # Cipher Suite: TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca9) 85 | # Cipher Suite: TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca8) 86 | # Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (0xc02c) 87 | # Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030) 88 | # Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a) 89 | # Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009) 90 | # Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013) 91 | # Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014) 92 | # Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c) 93 | # Cipher Suite: TLS_RSA_WITH_AES_256_GCM_SHA384 (0x009d) 94 | # Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f) 95 | # Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0x0035) 96 | # Cipher Suite: TLS_EMPTY_RENEGOTIATION_INFO_SCSV (0x00ff) 97 | 98 | 99 | # Chrome: 100 | 101 | # Cipher Suites (16 suites) 102 | # Cipher Suite: Reserved (GREASE) (0x2a2a) 103 | # Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301) 104 | # Cipher Suite: TLS_AES_256_GCM_SHA384 (0x1302) 105 | # Cipher Suite: TLS_CHACHA20_POLY1305_SHA256 (0x1303) 106 | # Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b) 107 | # Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f) 108 | # Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (0xc02c) 109 | # Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030) 110 | # Cipher Suite: TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca9) 111 | # Cipher Suite: TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca8) 112 | # Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013) 113 | # Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014) 114 | # Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c) 115 | # Cipher Suite: TLS_RSA_WITH_AES_256_GCM_SHA384 (0x009d) 116 | # Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f) 117 | # Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0x0035) 118 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | httpcore[http2] 2 | cffi 3 | 4 | --------------------------------------------------------------------------------