在Python

时间:2017-02-01 21:53:38

标签: python ssl pyopenssl

我的目标是允许ssl客户端从服务器中选择多个有效的证书对。客户端具有CA证书,用于验证来自服务器的证书。

因此,为了尝试实现此目的,我将服务器上的ssl.SSLContext.set_servername_callback()ssl.SSLSocket.wrap_socket's parameter: server_hostname`结合使用,以尝试允许客户端指定要使用的密钥对。这是代码的样子:

服务器代码:

import sys
import pickle
import ssl
import socket
import select

request = {'msgtype': 0, 'value': 'Ping', 'test': [chr(i) for i in range(256)]}
response = {'msgtype': 1, 'value': 'Pong'}

def handle_client(c, a):
    print("Connection from {}:{}".format(*a))
    req_raw = c.recv(10000)
    req = pickle.loads(req_raw)
    print("Received message: {}".format(req))
    res = pickle.dumps(response)
    print("Sending message: {}".format(response))
    c.send(res)

def run_server(hostname, port):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind((hostname, port))
    s.listen(8)
    print("Serving on {}:{}".format(hostname, port))

    try:
        while True:
            (c, a) = s.accept()

            def servername_callback(sock, req_hostname, cb_context, as_callback=True):
                print('Loading certs for {}'.format(req_hostname))
                server_cert = "ssl/{}/server".format(req_hostname)  # NOTE: This use of socket input is INSECURE
                cb_context.load_cert_chain(certfile="{}.crt".format(server_cert), keyfile="{}.key".format(server_cert))

                # Seems like this is designed usage: https://github.com/python/cpython/blob/3.4/Modules/_ssl.c#L1469
                sock.context = cb_context
                return None

            context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
            context.set_servername_callback(servername_callback)
            default_cert = "ssl/3.1/server"
            context.load_cert_chain(certfile="{}.crt".format(default_cert), keyfile="{}.key".format(default_cert))
            ssl_sock = context.wrap_socket(c, server_side=True)

            try:
                handle_client(ssl_sock, a)
            finally:
                c.close()

    except KeyboardInterrupt:
        s.close()

if __name__ == '__main__':
    hostname = ''
    port = 6789
    run_server(hostname, port)

客户代码:

import sys
import pickle
import socket
import ssl

request = {'msgtype': 0, 'value': 'Ping', 'test': [chr(i) for i in range(256)]}
response = {'msgtype': 1, 'value': 'Pong'}


def client(hostname, port):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    print("Connecting to {}:{}".format(hostname, port))
    s.connect((hostname, port))

    ssl_sock = ssl.SSLSocket(sock=s, ca_certs="server_old.crt", cert_reqs=ssl.CERT_REQUIRED, server_hostname='3.2')

    print("Sending message: {}".format(request))
    req = pickle.dumps(request)
    ssl_sock.send(req)

    resp_raw = ssl_sock.recv(10000)
    resp = pickle.loads(resp_raw)
    print("Received message: {}".format(resp))

    ssl_sock.close()

if __name__ == '__main__':
    hostname = 'localhost'
    port = 6789
    client(hostname, port)

但它没有用。似乎正在发生的事情是servername_callback被调用,正在获取指定的"主机名",并且回调中对context.load_cert_chain的调用没有失败(尽管它确实失败了,如果& #39;给定的路径不存在)。但是,服务器始终返回在调用context.wrap_socket(c, server_side=True)之前加载的证书对。所以我的问题是:在servername_callback内是否有某种方法来修改ssl上下文使用的密钥对,并获得用于连接的密钥对证书?

我还应该注意,我检查了流量,并且在servername_callback函数返回之后才发送服务器的证书(如果无法成功完成,将永远不会发送,或者返回a"失败"价值)。

2 个答案:

答案 0 :(得分:3)

在您的回调中,cb_context与调用wrap_socket()的上下文相同,与socket.context相同,因此socket.context = cb_context将上下文设置为与其相同之前。

更改上下文的证书链不会影响用于当前wrap_socket()操作的证书。对此的解释在于openssl如何创建其底层对象,在这种情况下,已经创建了底层SSL结构并使用copies of the chains

  

注意

     

调用SSL_new()时,与SSL_CTX结构关联的链将复制到任何SSL结构。 SSL结构不会受到父SSL_CTX中随后更改的任何链的影响。

设置新上下文时,SSL结构会更新,但new context is equal to the old one时不会执行更新。

您需要将sock.context设置为不同的上下文才能使其正常工作。您当前在每个新传入连接上实例化一个新上下文,这是不需要的。相反,您应该只将实例化标准上下文一次并重用它。对于动态加载的上下文也是如此,您可以在启动时创建它们并将它们放在dict中,这样您就可以进行查找,例如:

...

contexts = {}

for hostname in os.listdir("ssl"):
    print('Loading certs for {}'.format(hostname))
    server_cert = "ssl/{}/server".format(hostname)
    context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
    context.load_cert_chain(certfile="{}.crt".format(server_cert),
                            keyfile="{}.key".format(server_cert))
    contexts[hostname] = context

def servername_callback(sock, req_hostname, cb_context, as_callback=True):
    context = contexts.get(req_hostname)
    if context is not None:
        sock.context = context
    else:
        pass  # handle unknown hostname case

def run_server(hostname, port):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind((hostname, port))
    s.listen(8)
    print("Serving on {}:{}".format(hostname, port))

    context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
    context.set_servername_callback(servername_callback)
    default_cert = "ssl/3.1/server"
    context.load_cert_chain(certfile="{}.crt".format(default_cert),
                            keyfile="{}.key".format(default_cert))

    try:
        while True:
            (c, a) = s.accept()
            ssl_sock = context.wrap_socket(c, server_side=True)
            try:
                handle_client(ssl_sock, a)
            finally:
                c.close()

    except KeyboardInterrupt:
        s.close()

答案 1 :(得分:0)

因此,在看完这篇文章和其他一些在线文章之后,我整理了上面的代码版本,该版本对我来说非常有效...所以我只是想分享一下。万一它对其他人有帮助。

import sys
import ssl
import socket
import os

from pprint import pprint

DOMAIN_CONTEXTS = {}

ssl_root_path = "c:/ssl/"

# ----------------------------------------------------------------------------------------------------------------------
#
# As an example create domains in the ssl root path...ie
#
# c:/ssl/example.com
# c:/ssl/johndoe.com
# c:/ssl/test.com
#
# And then create self signed ssl certificates for each domain to test... and put them in the corresponding domain 
# directory... in this case the cert and key files are called cert.pem, and key.pem.... 
#

def setup_ssl_certs():

    global DOMAIN_CONTEXTS

    for hostname in os.listdir(ssl_root_path):

        #print('Loading certs for {}'.format(hostname))

        # Establish the certificate and key folder...for the various domains...
        server_cert = '{rp}{hn}/'.format(rp=ssl_root_path, hn=hostname)

        # Setup the SSL Context manager object, for authentication
        context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)

        # Load the certificate file, and key file...into the context manager.
        context.load_cert_chain(certfile="{}cert.pem".format(server_cert), keyfile="{}key.pem".format(server_cert))

        # Set the context object to the global dictionary
        DOMAIN_CONTEXTS[hostname] = context

    # Uncomment for testing only.
    #pprint(contexts)

# ----------------------------------------------------------------------------------------------------------------------

def servername_callback(sock, req_hostname, cb_context, as_callback=True):
    """
    This is a callback function for the SSL Context manager, this is what does the real work of pulling the
    domain name in the origional request.
    """

    # Uncomment for testing only
    #print(sock)
    #print(req_hostname)
    #print(cb_context)

    context = DOMAIN_CONTEXTS.get(req_hostname)

    if context:

        try:
            sock.context = context
        except Exception as error:
            print(error)
        else:
            sock.server_hostname = req_hostname

    else:
        pass  # handle unknown hostname case


def handle_client(conn, a):

    request_domain = conn.server_hostname

    request = conn.recv()

    client_ip = conn.getpeername()[0]

    resp = 'Hello {cip} welcome, from domain {d} !'.format(cip=client_ip, d=request_domain)

    conn.write(b'HTTP/1.1 200 OK\n\n%s' % resp.encode())


def run_server(hostname, port):

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    s.bind((hostname, port))

    s.listen(8)

    #print("Serving on {}:{}".format(hostname, port))

    context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)

    # For Python 3.4+
    context.set_servername_callback(servername_callback)

    # Only available in 3.7 !!!! have not tested it yet...
    #context.sni_callback(servername_callback)

    default_cert = "{rp}default/".format(rp=ssl_root_path)

    context.load_cert_chain(certfile="{}cert.pem".format(default_cert), keyfile="{}key.pem".format(default_cert))

    context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1  # optional

    context.set_ciphers('EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH')

    try:
        while True:

            ssock, addr = s.accept()

            try:
                conn = context.wrap_socket(ssock, server_side=True)

            except Exception as error:
                print('!!! Error, {e}'.format(e=error))

            except ssl.SSLError as e:
                print(e)

            else:
                handle_client(conn, addr)

                if conn:
                    conn.close()
                    #print('Connection closed !')

    except KeyboardInterrupt:
        s.close()

# ----------------------------------------------------------------------------------------------------------------------

def main():

    setup_ssl_certs()

    # Don't forget to update your static name resolution...  ie example.com = 127.0.0.1
    run_server('example.com', 443)

# ----------------------------------------------------------------------------------------------------------------------

if __name__ == '__main__':
    main()