1

Currently using a really simple Twisted NameVirtualHost coupled with some JSON config files to serve really basic content in one Site object. The resources being served by Twisted are all WSGI objects built in flask.

I was wondering on how to go about wrapping the connections to these domains with an SSLContext, since reactor.listenSSL takes one and only one context, it isn't readily apparent how to give each domain/subdomain it's own crt/key pair. Is there any way to set up named virtual hosting with ssl for each domain that doesn't require proxying? I can't find any Twisted examples that use NameVirtualHost with SSL, and they only thing I could get to work is hook on the reactor listening on port 443 with only one domain's context?

I was wondering if anyone has attempted this?

My simple server without any SSL processing:

https://github.com/DeaconDesperado/twsrv/blob/master/service.py

DeaconDesperado
  • 9,977
  • 9
  • 47
  • 77

3 Answers3

6

TLS (the name for the modern protocol which replaces SSL) only very recently supports the feature you're looking for. The feature is called Server Name Indication (or SNI). It is supported by modern browsers on modern platforms, but not some older but still widely used platforms (see the wikipedia page for a list of browsers with support).

Twisted has no specific, built-in support for this. However, it doesn't need any. pyOpenSSL, upon which Twisted's SSL support is based, does support SNI.

The set_tlsext_servername_callback pyOpenSSL API gives you the basic mechanism to build the behavior you want. This lets you define a callback which is given access to the server name requested by the client. At this point, you can specify the key/certificate pair you want to use for the connection. You can find an example demonstrating the use of this API in pyOpenSSL's examples directory.

Here's an excerpt from that example to give you the gist:

def pick_certificate(connection):
    try:
        key, cert = certificates[connection.get_servername()]
    except KeyError:
        pass
    else:
        new_context = Context(TLSv1_METHOD)
        new_context.use_privatekey(key)
        new_context.use_certificate(cert)
        connection.set_context(new_context)

server_context = Context(TLSv1_METHOD)
server_context.set_tlsext_servername_callback(pick_certificate)

You can incorporate this approach into a customized context factory and then supply that context factory to the listenSSL call.

Jean-Paul Calderone
  • 47,755
  • 6
  • 94
  • 122
  • 1
    Not sure if I'm using your advice right... I couldn't find the examples in the PyOpenSSL docs. Here is what I have amended in my code: https://github.com/DeaconDesperado/twsrv/blob/ssl/service.py For some reason, any of my scripts that import twisted use ver .12 of OpenSSL despite the fact that the virtualenv has .13 installed. This version raises an attribute error with `set_tlsext_servername_callback` – DeaconDesperado Aug 31 '12 at 19:57
  • 1
    You'll have to figure out how to get your environment to use pyOpenSSL 0.13. The functionality is not available in 0.12. – Jean-Paul Calderone Aug 31 '12 at 20:16
  • 1
    I got 0.13 working now, but I still can't figure where to hook in that callback. The twisted documentation says that the `getContext` method of the ssl factory should return a `Context` object, but isnt' this what the hook for set_tlsext_servername_callback is for? – DeaconDesperado Aug 31 '12 at 20:58
  • 1
    SNI works by replacing the original context with a new one. The first Context is the one that has a servername callback on it. The second Context is the one that has the key/certificate you want to use. If you didn't have the first Context, returned by getContext, there would be nothing to tell OpenSSL that you want your callback invoked when a servername is received from the client. – Jean-Paul Calderone Sep 01 '12 at 20:27
  • 1
    But at what point do you hook in the `set_tlsext_servername_callback`? Not within the `getContext` method of the SSL factory, right? That would repeat it for every request, unless I'm mistaken. The `getContext` method doesn't received a connection instance, so how could I make this do the sni lookup for every request? – DeaconDesperado Sep 04 '12 at 14:29
  • 1
    `set_tlsext_servername_callback` sets a callback on a particular `Context` instance. You only need to set the callback once on a particular `Context` instance. If your `getContext` method returns the same `Context` every time it is called, then setting up that `Context` once is sufficient. If it returns a new `Context` sometimes (or all the time), then you'll need to specify the callback on each of those instances. And you don't need a connection instance to use `set_tlsext_servername_callback`, so the fact that `getContext` is not passed a connection doesn't make any difference. – Jean-Paul Calderone Sep 04 '12 at 15:51
  • As configured it looks as if my callback never gets called. https://github.com/DeaconDesperado/twsrv/blob/ssl/service.py The print on line 17 never runs, so I assumed the hook into the original context from getContext isn't correct? – DeaconDesperado Sep 04 '12 at 19:33
  • Did you check your code for unhandled exceptions? It seems like there's at least one very obvious NameError in your `pickCert`. Perhaps you do understand the pyOpenSSL well enough, but got distracted from fixing basic bugs in your code. – Jean-Paul Calderone Sep 05 '12 at 10:30
  • I rectified the NameError, suprised I didn't catch that. I still can't get the set_tlsext_servername_callback to run no matter how I configure it. I tried updating the os OpenSSL package as well. – DeaconDesperado Sep 05 '12 at 19:58
  • The SNI example in the pyOpenSSL package works for me. So, I don't know what else to say. Perhaps your SSL client doesn't actually support SNI, or is doing SNI wrong? – Jean-Paul Calderone Sep 06 '12 at 10:22
  • Thanks for your help in any event. I have found out I can get the server to summon the callback when I use the `openssl s_client -connect` on cli, but not when I use https: to access the resources, which is the intended use. Is there something I have misunderstood? Is SNI impractical for the browser use case? – DeaconDesperado Sep 06 '12 at 15:15
  • 1
    It appears there is something going on with SNI on Chromium, as Firefox is firing the callback properly. – DeaconDesperado Sep 06 '12 at 18:35
  • 1
    Now I am embarassed - my chromium browser didn't recognize the cert vs the cached on since I had changed it. Everything is now working properly. Thanks for your help. – DeaconDesperado Sep 06 '12 at 18:43
3

Just to add some closure to this one, and for future searches, here is the example code for the echo server from the examples that prints the SNI:

from twisted.internet import ssl, reactor
from twisted.internet.protocol import Factory, Protocol

class Echo(Protocol):
    def dataReceived(self, data):
        self.transport.write(data)

def pick_cert(connection):
    print('Received SNI: ', connection.get_servername())

if __name__ == '__main__':
    factory = Factory()
    factory.protocol = Echo

    with open("keys/ca.pem") as certAuthCertFile:
        certAuthCert = ssl.Certificate.loadPEM(certAuthCertFile.read())

    with open("keys/server.key") as keyFile:
        with open("keys/server.crt") as certFile:
            serverCert = ssl.PrivateCertificate.loadPEM(
                keyFile.read() + certFile.read())

    contextFactory = serverCert.options(certAuthCert)

    ctx = contextFactory.getContext()
    ctx.set_tlsext_servername_callback(pick_cert)

    reactor.listenSSL(8000, factory, contextFactory)
    reactor.run()

And because getting OpenSSL to work can always be tricky, here is the OpenSSL statement you can use to connect to it:

openssl s_client -connect localhost:8000 -servername hello_world -cert keys/client.crt -key keys/client.key

Running the above python code against pyOpenSSL==0.13, and then running the s_client command above, will print this to the screen:

('Received SNI: ', 'hello_world')
Benn
  • 31
  • 1
0

There is now a txsni project that takes care of finding the right certificates per request. https://github.com/glyph/txsni

joeforker
  • 40,459
  • 37
  • 151
  • 246