【问题标题】:How to list all openssl ciphers available in statically linked python releases?如何列出静态链接的 python 版本中可用的所有 openssl 密码?
【发布时间】:2015-04-04 15:04:53
【问题描述】:

python 2.7.8 到 2.7.9 升级中,ssl 模块由 using 改变

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

我想知道这对在 Windows 上与我的 python 安装建立 SSL/TLS 连接时使用的实际“有序 SSL 密码首选项列表”有何影响。

例如,要弄清楚密码列表扩展为什么“有序 SSL 密码首选项列表”,我通常会使用 openssl ciphers 命令行(参见 man page),例如使用 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!

这在 Python 动态加载 openssl ciphers 使用的同一 OpenSSL 库的 Linux 上非常有用:

$ 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 内置库不同的密码。

我可以很容易地找到用于构建两个 python 版本的 OpenSSL 版本:

$ 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

但是,即使我可以找到并下载 1.0.1h 和 1.0.1j 版本的 openssl 命令行版本,我也不能确定它们是使用与 python 中内置的 lib 相同的选项进行编译的,从man page我们知道

OpenSSL 的某些编译版本可能不包括此处列出的所有密码,因为某些密码在编译时已被排除。

那么,有没有办法让 python 的 ssl 模块给我类似于 openssl ciphers -v 命令的输出?

【问题讨论】:

  • 其他影响这一点的因素...服务器通过选择与它们(服务器)相交的第一个客户端密码来尊重客户端选择。您可以使用SSL_OP_CIPHER_SERVER_PREFERENCE 强制服务器进行选择。在这种情况下,服务器会更喜欢它的密码列表,并会选择与客户端列表中的某些内容相交的最高密码。

标签: python windows security python-2.7 openssl


【解决方案1】:

您可能想在https://github.com/openssl/openssl/blob/master/apps/ciphers.c 上查看openssl cipher 的源代码

关键步骤似乎是:

  1. meth = SSLv23_server_method();
  2. ctx = SSL_CTX_new(meth);
  3. SSL_CTX_set_cipher_list(ctx, ciphers),而 ciphers 是你的字符串
  4. ssl = SSL_new(ctx);
  5. sk = SSL_get1_supported_ciphers(ssl);
  6. 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 中没有使用它。您可以获得的最接近的是SSLSocket 实例的shared_ciphers() 方法。 (当前)实现是

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 对象。

这就是我的进展,我希望能有所帮助,也许你可以根据这些发现找出你需要什么。

【讨论】:

  • 啊,谢谢。这很有用,我也许可以在此基础上得到答案。但是SSLContext 在 python 2.7.9 中是新的,所以我不能使用这种方法来找出 python 2.7.8 版本中可用的密码,所以我仍然不知道是什么在这两个版本之间发生了变化:(
  • 啊... 和 SSLSocket.shared_ciphers 本身直到 python 3.5 才被添加——这个版本太新了,还没有发布。其实the patch to add this上个月才登陆:(
  • 我很害怕,Python 中的整个 SSL 架构在 Python 2.7.9 和 3.4 中经历了一次严重的重新设计,导致社区提出了许多进一步的更改请求。也许您可以使用 ctypes 并访问 Python for Windows 附带的静态 OpenSSL 构建?
【解决方案2】:

Jan-Philip Gehrcke's answer 需要一个尚未发布的 python 版本才能使用(请参阅 cmets),这使得回答有关旧版本 python 的问题不切实际。但这段话启发了我:

...在套接字连接之前,您不能调用 sslsock.shared_ciphers()。否则,Python 的 _ssl 模块不会创建读取密码所需的低级 OpenSSL SSL 对象。

这让我想到了一个可能的解决方案。都在同一个python程序中:

  • 创建一个接受任何密码 (ciphers='ALL:aNULL:eNULL') 的服务器套接字。
  • 使用配置了我们要检查的密码列表的客户端套接字连接到服务器套接字(如果我们想从 python 2.7.8 测试默认值,例如 'DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2'
  • 一旦建立连接,检查客户端实际选择的密码并打印它,例如'AES256-GCM-SHA384'。客户端将从其配置的密码列表中选择与服务器提供的匹配的最高优先级密码。服务器接受任何密码并在具有相同 OpenSSL 库的相同 python 程序中运行,因此服务器的列表保证是客户端列表的超集。因此,使用的密码必须是提供给客户端套接字的扩展列表中优先级最高的密码。万岁。
  • 现在重复一遍,再次连接到服务器套接字但是这次排除了上一轮选择的密码,将它的否定附加到客户端套接字的密码列表中,例如'DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2:!AES256-GCM-SHA384')
  • 重复直到 SSL 握手失败,因为我们已经用完了密码。

这里是代码(也可以作为a github gist):

"""An attempt to produce similar output to "openssl ciphers -v", but for
python's 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's "
                        "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内置的默认密码列表的:

day@laptop ~/test
$ python --version
Python 2.7.8

day@laptop ~/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's default
                        client list (default:
                        DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2)

所以我们可以很容易地看到默认客户端密码列表扩展为什么,以及它如何从 python 2.7.8 更改为 2.7.9:

day@laptop ~/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)

day@laptop ~/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)

我认为这回答了我的问题。除非有人能看出这种方法有问题吗?

【讨论】:

  • 有趣的方法!至于所涉及的复杂程度,您应该进行进一步的测试以支持“openssl ciphers -v”的语句等效用于python的内置ssl”。那太好了。
  • 好点。不幸的是,我已经在 linux 上对此进行了测试,其中 python is 使用与 openssl ciphers -v 相同的库,并且我的 python 返回的密码比 openssl 命令行少得多。所以看起来我所说的充其量只是误导。我可能不得不更改为 "An attempt 为 python 的内置 ssl 生成与 "openssl ciphers -v" 类似的输出"
猜你喜欢
  • 2016-05-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-08-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-29
相关资源
最近更新 更多