jetforce icon indicating copy to clipboard operation
jetforce copied to clipboard

Using multiple TLS certificates

Open makew0rld opened this issue 3 years ago • 5 comments

I saw this on the README:

Jetforce does not (yet) support virtual hosting at the TLS-layer using SNI. This means that you cannot return different server TLS certificates for different domains.

What work is required to achieve this for Jetforce? I wouldn't mind trying to help with this.

makew0rld avatar Jul 06 '20 21:07 makew0rld

Also as another workaround, it'd be nice to see wildcard certs mentioned in the README, where CN=*.example.com and also -addext "subjectAltName = DNS:example.com" which will allow any subdomain of example.com to be used.

Edit: Example command for example.com as the root domain:

openssl req -new -subj "/CN=*.example.com" -addext "subjectAltName = DNS:example.com, DNS:*.example.com" -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -days 1825 -nodes -out cert.pem -keyout key.pem

This is in line with other cert recommendations such as using a 5 year cert, and using a 256-bit EC key which is nice and small.

makew0rld avatar Jul 06 '20 21:07 makew0rld

All of the TLS configuration happens in this class:

https://github.com/michael-lazar/jetforce/blob/efe41ece85d48bae928492c3ded50a190fca7dd6/jetforce/tls.py#L84

Currently, the class is initialized with a single cert/key pair. It generates a single SSL.Context object which is cached inside of GeminiCertificateOptions._context. This context is then used by default for every connection.

I think the general approach for adding SNI should be something like this

  • Update the GeminiCertificateOptions class to allow defining separate certfile/keyfile files per hostname.
  • Update GeminiCertificateOptions._makeContext(hostname=None) to allow generating an SSL context based on a hostname.
  • Update GeminiCertificateOptions.sni_callback() to look at the hostname for the connection, and then inject the correct context based on the above method.

I would like keep the default behavior as close to the Twisted standard convention as possible. So I'm drawn towards keeping the default context as-is:

GeminiCertficateOptions._context: OpenSSL.SSL.Context

And then adding an additional cache for hostname-specific contexts:

GeminiCertficateOptions._sni_context: Dict[str, OpenSSL.SSL.Context]

Then, the lookup would turn into something like this

def get_context(self, hostname)
    return self._sni_context.get(hostname, self._context)

Currently the "default" certificate is passed into the GeminiCertificateOptions(...) constructor. Maybe we can keep it that way and add a different method to specify SNI-based certificates:

cert_options = GeminiCertificateOptions(default_key, default_cert, ...)
cert_options.register_alternate_context(hostname1, certfile1, keyfile1)
cert_options.register_alternate_context(hostname2, certfile2, keyfile2)

I'm not married to this approach though, and I'm open to alternatives if they end up being more straightforward.

michael-lazar avatar Jul 07 '20 01:07 michael-lazar

Oh okay. Looks like there's a lot to learn here ha! I thought there would be something like GeminiCertificateOptions for each app, is there a reason why it's not like that?

makew0rld avatar Jul 07 '20 16:07 makew0rld

Oh okay. Looks like there's a lot to learn here ha!

Yea.. I still have a lot to learn about it too 😄

I thought there would be something like GeminiCertificateOptions for each app, is there a reason why it's not like that?

I think the reason is because you need to establish the OpenSSL socket in order to accept the initial TLS client hello handshake, before you even get the the part of the handshake where the SNI becomes available. So you need some kind of initial SSL context already in place to handle the connection.

The python standard library ssl documentation describes the same thing (link).

michael-lazar avatar Jul 07 '20 20:07 michael-lazar

Ah okay, that makes sense. Thanks for the explanation!

makew0rld avatar Jul 07 '20 20:07 makew0rld