2017-02-01 3 views
3

私は、sslクライアントがサーバからの有効な証明書ペアをいくつか選択できるようにすることを目標にしています。クライアントには、サーバーからの証明書を検証するために使用するCA証明書があります。Pythonでssl context.set_servername_callbackを使用する

これを達成するために、ssl.SSLSocket.wrap_socket's parameter: server_hostnameと組み合わせてサーバー上でssl.SSLContext.set_servername_callback()を使用して、クライアントが使用するキーペアを指定できるようにします。

Serverコード:

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への呼び出しが失敗していないと思われるものがあります(存在しないパスが指定されていると失敗します)。ただし、サーバーは常にcontext.wrap_socket(c, server_side=True)を呼び出す前に読み込まれた証明書のペアを返します。だから私の質問は:servername_callbackの中には、sslコンテキストで使用される鍵ペアを変更し、その鍵ペアの証明書を接続に使用する方法がありますか?

また、servername_callback関数が返されるまで、トラフィックがチェックされ、サーバーの証明書が送信されないことに注意してください。正常に完了しなかった場合、または「失敗」値を返した場合は送信されません。あなたのコールバックで

+0

は 'set_servername_callback'は、サーバーではなく、クライアントによって使用されます。 C:[サーバー名表示(SNI)の実装方法](http://stackoverflow.com/q/5113333/608639)と[SNIを使用して複数のドメインを1つのボックスに表示する](http: /stackoverflow.com/q/22373332/608639)。サーバを 's_client'で接続することでテストできます:' openssl s_client -connect -tls1 -servername '。 '-servername'はSNI拡張です。 – jww

答えて

2

cb_contextwrap_socket()が呼び出されたのと同じ状況で、socket.contextと同じなので、socket.context = cb_contextすることは、以前と同じにコンテキストを設定します。

コンテキストの証明書チェーンを変更しても、現在のwrap_socket()操作で使用されている証明書には影響しません。 SSL_CTX構造の

NOTES

チェーンの仲間は、任意にコピーされます。これについての説明は、OpenSSLが、この場合、基本となるSSL構造が既に作成してcopies of the chainsを使用してきた、その基礎となるオブジェクトを作成する方法にありSSL_new()が呼び出されたときのSSL構造。 SSL構造体は、親SSL_CTXで後に変更されたチェーンの影響を受けません。

新しいコンテキストを設定すると、SSL構造が更新されますが、new context is equal to the old oneの場合は更新が実行されません。

sock.context異なるコンテキストに設定して動作させる必要があります。現在、それぞれの新しい着信接続で新しいコンテキストをインスタンス化しますが、これは不要です。代わりに、標準コンテキストを一度だけインスタンス化して再利用する必要があります。同じことは、例えば、あなたが起動時にそれらすべてを作成して、あなただけの検索を行うことができますので辞書でそれらを置くことができ、動的にロードされたコンテキストのために行く:

... 

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

ありがとうございます!これはまさに問題でした。私が変更していたコンテキストは、既に使用されているのと同じコンテキストであるため、ソケットに設定されていませんでした。 – caleb