Как перечислить все шифры openssl, доступные в статически связанных версиях python?

В обновлении python 2.7.8 до 2.7.9 модуль ssl изменился с

_DEFAULT_CIPHERS = 'DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2'

к

_DEFAULT_CIPHERS = (
    'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:'
    'DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:RSA+3DES:ECDH+RC4:'
    'DH+RC4:RSA+RC4:!aNULL:!eNULL:!MD5'
)

Я хотел бы знать, как это влияет на фактический "упорядоченный список предпочтений шифрования SSL", который используется при установлении соединений SSL/TLS с моими установками python в Windows.

Например, чтобы выяснить, что "упорядочил список привилегий SSL-шифрования", список расшифровки расширений, я обычно использовал бы командную строку openssl ciphers (см. man-страница), например, с openssl v1.0.1k. Я вижу, что этот список шифрования по умолчанию для python 2.7.8 расширяется до:

$ openssl ciphers -v 'DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2'
ECDHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH     Au=RSA  Enc=AESGCM(256) Mac=AEAD
ECDHE-ECDSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH     Au=ECDSA Enc=AESGCM(256) Mac=AEAD
ECDHE-RSA-AES256-SHA384 TLSv1.2 Kx=ECDH     Au=RSA  Enc=AES(256)  Mac=SHA384
ECDHE-ECDSA-AES256-SHA384 TLSv1.2 Kx=ECDH     Au=ECDSA Enc=AES(256)  Mac=SHA384
ECDHE-RSA-AES256-SHA    SSLv3 Kx=ECDH     Au=RSA  Enc=AES(256)  Mac=SHA1
ECDHE-ECDSA-AES256-SHA  SSLv3 Kx=ECDH     Au=ECDSA Enc=AES(256)  Mac=SHA1
SRP-DSS-AES-256-CBC-SHA SSLv3 Kx=SRP      Au=DSS  Enc=AES(256)  Mac=SHA1
SRP-RSA-AES-256-CBC-SHA SSLv3 Kx=SRP      Au=RSA  Enc=AES(256)  Mac=SHA1
...
snip!

Это отлично работает, когда в Linux, где python динамически загружает ту же библиотеку OpenSSL, что openssl ciphers использует:

$ ldd /usr/lib/python2.7/lib-dynload/_ssl.x86_64-linux-gnu.so | grep libssl
        libssl.so.1.0.0 => /lib/x86_64-linux-gnu/libssl.so.1.0.0 (0x00007ff75d6bf000)
$ ldd /usr/bin/openssl | grep libssl
        libssl.so.1.0.0 => /lib/x86_64-linux-gnu/libssl.so.1.0.0 (0x00007fa48f0fe000)

Однако, в Windows сборка Python статически связывает библиотеку OpenSSL. Это означает, что команда openssl ciphers не может мне помочь, потому что она использует другую версию библиотеки, которая может поддерживать разные шифры, чем библиотека, встроенная в python.

Я могу узнать, какая версия OpenSSL была использована для создания каждого из двух выпусков python:

$ python-2.7.8/python -c 'import ssl; print ssl.OPENSSL_VERSION'
OpenSSL 1.0.1h 5 Jun 2014

$ python-2.7.9/python -c 'import ssl; print ssl.OPENSSL_VERSION'
OpenSSL 1.0.1j 15 Oct 2014

Но даже если бы я смог найти и загрузить сборку командной строки openssl для выпусков 1.0.1h и 1.0.1j, я не могу быть уверен, что они были скомпилированы с теми же параметрами, что и lib, встроенный в python, и из man page мы знаем, что

Некоторые скомпилированные версии OpenSSL могут не включать в себя все шифры, перечисленные здесь, потому что во время компиляции исключаются некоторые шифры.

Итак, есть ли способ получить модуль ssl python, чтобы дать мне вывод, подобный тому, что из команды openssl ciphers -v?

Ответ 1

Возможно, вам стоит взглянуть на исходный код openssl cipher на https://github.com/openssl/openssl/blob/master/apps/ciphers.c

Решающими шагами являются:

  • meth = SSLv23_server_method();
  • ctx = SSL_CTX_new(meth);
  • SSL_CTX_set_cipher_list(ctx, ciphers), тогда как ciphers - это ваша строка
  • ssl = SSL_new(ctx);
  • sk = SSL_get1_supported_ciphers(ssl);
  • for (i = 0; i < sk_SSL_CIPHER_num(sk); i++) { print SSL_CIPHER_get_name(sk_SSL_CIPHER_value(sk, i)); }

Функция SSL_CTX_set_cipher_list вызывается в Python 3.4 в _ssl set_ciphers для контекстов. Вы можете добиться того же, используя:

import socket
from ssl import SSLSocket
sslsock = SSLSocket(socket.socket(socket.AF_INET, socket.SOCK_STREAM))
sslsock.context.set_ciphers('DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2')

Следующим шагом будет вызов SSL_get1_supported_ciphers(), который, к сожалению, не используется в Python _ssl.c. Самое близкое, что вы можете получить, это метод shared_ciphers() экземпляров SSLSocket. Реализация (текущая)

static PyObject *PySSL_shared_ciphers(PySSLSocket *self)
{
    [...]
    ciphers = sess->ciphers;
    res = PyList_New(sk_SSL_CIPHER_num(ciphers));
    for (i = 0; i < sk_SSL_CIPHER_num(ciphers); i++) {
        PyObject *tup = cipher_to_tuple(sk_SSL_CIPHER_value(ciphers, i));
        [...]
        PyList_SET_ITEM(res, i, tup);
    }
    return res;
}

То есть этот цикл очень похож, как в реализации ciphers.c выше, и возвращает список шифров Python в том же порядке, что и цикл в ciphers.c.

Продолжая описанный выше пример sslsock = SSLSocket(...), вы не можете вызвать sslsock.shared_ciphers() перед подключением сокета. В противном случае модуль Python _ssl не создает низкоуровневый объект OpenSSL SSL, который необходим для чтения шифров. Это отличается от реализации в ciphers.c, которая создает объект SSL низкого уровня без необходимости подключения.

Именно так я и получил, надеюсь, это поможет, и, может быть, вы сможете понять, что вам нужно, основываясь на этих результатах.

Ответ 2

Ответ Jan-Philip Gehrcke требует использования еще неизданной версии python (см. комментарии), что делает его нецелесообразным для ответа на вопрос о более старых версиях python. Но этот параграф вдохновил меня:

... вы не можете вызвать sslsock.shared_ciphers() перед подключением сокета. В противном случае модуль Python _ssl не создает низкоуровневый объект OpenSSL SSL, который необходим для чтения шифров.

Это заставило меня задуматься о возможном решении. Все в одной программе python:

  • Создайте сокет сервера, который принимает любой шифр (ciphers='ALL:aNULL:eNULL').
  • Подключитесь к серверному сокету с клиентским сокетом, настроенным с помощью списка шифрования, который мы хотим проверить (скажем 'DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2', если мы хотим проверить значение по умолчанию из python 2.7.8)
  • Как только соединение установлено, проверьте шифр, который фактически был выбран клиентом, и распечатайте его, например. 'AES256-GCM-SHA384'. Клиент будет выбирать шифр с наивысшим приоритетом из его настроенного списка шифров, который соответствует серверу. Сервер принимает любой шифр и работает в той же программе python с той же самой библиотекой OpenSSL, что и список серверов, как гарантируется, является надмножеством списка клиентов. Поэтому используемый шифра должен быть наивысшим приоритетом из расширенного списка, поставляемого в клиентский сокет. Hooray.
  • Теперь повторите попытку, снова подключившись к серверному сокету, но на этот раз исключите шифр, который был выбран в предыдущем раунде, добавив отрицание его в список шифрования сокета клиента, например. 'DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2:!AES256-GCM-SHA384')
  • Повторяйте до тех пор, пока квитирование SSL не пройдет, потому что у нас закончились шифры.

Вот код (также доступен как github gist):

"""An attempt to produce similar output to "openssl ciphers -v", but for
python built-in ssl.

To answer https://stackoverflow.com/q/28332448/445073
"""
from __future__ import print_function

import argparse
import logging
import multiprocessing
import os
import socket
import ssl
import sys

def server(log_level, queue):
    logging.basicConfig(level=log_level)
    logger = logging.getLogger("server")

    logger.debug("Creating bind socket")
    bind_sock = socket.socket()
    bind_sock.bind(('127.0.0.1', 0))
    bind_sock.listen(5)

    bind_addr = bind_sock.getsockname()
    logger.debug("Listening on %r", bind_addr)
    queue.put(bind_addr)

    while True:
        logger.debug("Waiting for connection")
        conn_sock, fromaddr = bind_sock.accept()
        conn_sock = ssl.wrap_socket(conn_sock,
                                    ssl_version=ssl.PROTOCOL_SSLv23,
                                    server_side=True,
                                    certfile="server.crt",
                                    keyfile="server.key",
                                    ciphers="ALL:aNULL:eNULL")

        data = conn_sock.read()
        logger.debug("Read %r", data)
        conn_sock.close()
    logger.debug("Done")

def parse_args(argv):
    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument("--verbose", "-v", action="store_true",
                        help="Turn on debug logging")
    parser.add_argument("--ciphers", "-c",
                        default=ssl._DEFAULT_CIPHERS,
                        help="Cipher list to test. Defaults to this python "
                        "default client list")
    args = parser.parse_args(argv[1:])
    return args

if __name__ == "__main__":
    args = parse_args(sys.argv)

    log_level = logging.DEBUG if args.verbose else logging.INFO

    logging.basicConfig(level=log_level)
    logger = logging.getLogger("client")

    if not os.path.isfile('server.crt') or not os.path.isfile('server.key'):
        print("Must generate server.crt and server.key before running")
        print("Try:")
        print("openssl req -x509 -newkey rsa:2048 -keyout server.key -out server.crt -nodes -days 365  -subj '/CN=127.0.0.1'")
        sys.exit(1)

    queue = multiprocessing.Queue()
    server_proc = multiprocessing.Process(target=server, args=(log_level, queue))
    server_proc.start()
    logger.debug("Waiting for server address")
    server_addr = queue.get()

    chosen_ciphers = []
    try:
        cipher_list = args.ciphers
        while True:
            client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            client_sock = ssl.wrap_socket(client_sock,
                                          ssl_version=ssl.PROTOCOL_SSLv23,
                                          ciphers=cipher_list)
            logger.debug("Connecting to %r", server_addr)
            client_sock.connect(server_addr)
            logger.debug("Connected")

            chosen_cipher = client_sock.cipher()
            chosen_ciphers.append(chosen_cipher)

            client_sock.write("ping")
            client_sock.close()

            # Exclude the first choice cipher from the list, to see what we get
            # next time.
            cipher_list += ':!' + chosen_cipher[0]
    except ssl.SSLError as err:
        if 'handshake failure' in str(err):
            logger.debug("Handshake failed - no more ciphers to try")
        else:
            logger.exception("Something bad happened")
    except Exception:
        logger.exception("Something bad happened")
    else:
        server_proc.join()
    finally:
        server_proc.terminate()

    print("Python: {}".format(sys.version))
    print("OpenSSL: {}".format(ssl.OPENSSL_VERSION))
    print("Expanding cipher list: {}".format(args.ciphers))
    print("{} ciphers found:".format(len(chosen_ciphers)))
    print("\n".join(repr(cipher) for cipher in chosen_ciphers))

Обратите внимание на то, как он по умолчанию проверяет встроенный в python список шифрования по умолчанию:

[email protected] ~/test
$ python --version
Python 2.7.8

[email protected] ~/test
$ python ssltest.py -h
usage: ssltest.py [-h] [--verbose] [--ciphers CIPHERS]

optional arguments:
  -h, --help            show this help message and exit
  --verbose, -v         Turn on debug logging (default: False)
  --ciphers CIPHERS, -c CIPHERS
                        Cipher list to test. Defaults to this python default
                        client list (default:
                        DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2)

поэтому мы можем легко увидеть, к чему расширяется список клиентских шифров по умолчанию, и как это изменилось с python 2.7.8 до 2.7.9:

[email protected] ~/test
$ ~/dists/python-2.7.8-with-pywin32-218-x86/python ssltest.py
Python: 2.7.8 (default, Jun 30 2014, 16:03:49) [MSC v.1500 32 bit (Intel)]
OpenSSL: OpenSSL 1.0.1h 5 Jun 2014
Expanding cipher list: DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2
12 ciphers found:
('AES256-GCM-SHA384', 'TLSv1/SSLv3', 256)
('AES256-SHA256', 'TLSv1/SSLv3', 256)
('AES256-SHA', 'TLSv1/SSLv3', 256)
('CAMELLIA256-SHA', 'TLSv1/SSLv3', 256)
('DES-CBC3-SHA', 'TLSv1/SSLv3', 168)
('AES128-GCM-SHA256', 'TLSv1/SSLv3', 128)
('AES128-SHA256', 'TLSv1/SSLv3', 128)
('AES128-SHA', 'TLSv1/SSLv3', 128)
('SEED-SHA', 'TLSv1/SSLv3', 128)
('CAMELLIA128-SHA', 'TLSv1/SSLv3', 128)
('RC4-SHA', 'TLSv1/SSLv3', 128)
('RC4-MD5', 'TLSv1/SSLv3', 128)

[email protected] ~/test
$ ~/dists/python-2.7.9-with-pywin32-219-x86/python ssltest.py
Python: 2.7.9 (default, Dec 10 2014, 12:24:55) [MSC v.1500 32 bit (Intel)]
OpenSSL: OpenSSL 1.0.1j 15 Oct 2014
Expanding cipher list: ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:RSA+3DES:ECDH+RC4:DH+RC4:RSA+RC4:!aNULL:!eNULL:!MD5
18 ciphers found:
('ECDHE-RSA-AES256-GCM-SHA384', 'TLSv1/SSLv3', 256)
('ECDHE-RSA-AES128-GCM-SHA256', 'TLSv1/SSLv3', 128)
('ECDHE-RSA-AES256-SHA384', 'TLSv1/SSLv3', 256)
('ECDHE-RSA-AES256-SHA', 'TLSv1/SSLv3', 256)
('ECDHE-RSA-AES128-SHA256', 'TLSv1/SSLv3', 128)
('ECDHE-RSA-AES128-SHA', 'TLSv1/SSLv3', 128)
('ECDHE-RSA-DES-CBC3-SHA', 'TLSv1/SSLv3', 112)
('AES256-GCM-SHA384', 'TLSv1/SSLv3', 256)
('AES128-GCM-SHA256', 'TLSv1/SSLv3', 128)
('AES256-SHA256', 'TLSv1/SSLv3', 256)
('AES256-SHA', 'TLSv1/SSLv3', 256)
('AES128-SHA256', 'TLSv1/SSLv3', 128)
('AES128-SHA', 'TLSv1/SSLv3', 128)
('CAMELLIA256-SHA', 'TLSv1/SSLv3', 256)
('CAMELLIA128-SHA', 'TLSv1/SSLv3', 128)
('DES-CBC3-SHA', 'TLSv1/SSLv3', 112)
('ECDHE-RSA-RC4-SHA', 'TLSv1/SSLv3', 128)
('RC4-SHA', 'TLSv1/SSLv3', 128)

И я думаю, что это отвечает на мой вопрос. Разве никто не может увидеть проблему с этим подходом?