jetforce
jetforce copied to clipboard
Using multiple TLS certificates
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.
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.
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 separatecertfile
/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.
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?
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).
Ah okay, that makes sense. Thanks for the explanation!