Status20 Success
Metatext/plain
# Transient TLS certificates

I've spent a fair amount of time now
trying to get my python server to accept
transient TLS client certificates, and
have come to the unfortunate conclusion
that it's simply not possible using the
standard library python bindings for
OpenSSL.

Python's ssl module provides three
verification modes that can be used when
setting up an SSL context for a server
socket [1]. These mirror the flags that
are defined in the underlying OpenSSL
library.

1. ssl.CERT_NONE

   No certificate is requested from the
   client.

2. ssl.CERT_OPTIONAL

   A client certificate request is sent
   to the client. The client may either
   ignore the request or send a
   certificate in order perform TLS
   client cert authentication. If the
   client chooses to send a certificate,
   it is verified. Any verification
   error immediately aborts the TLS
   handshake.

3. ssl.CERT_REQUIRED

   This mode provides mandatory TLS
   client cert authentication. A client
   certificate request is sent to the
   client and the client must provide a
   valid and trusted certificate.

None of these verification modes are
sufficient for using self-signed client
certificates. I can optionally request
the cert from the client, but if the
client provides a cert that isn't
recognized by my CA chain, the handshake
will immediately abort. What I really
need is a modified version of
CERT_OPTIONAL that sends a client
certificate request, but does not verify
the certificate.

The OpenSSL library itself provides a
work-around for this by exposing a hook
into the certificate verification
process using
SSL_CTX_set_cert_verify_callback [2].
If was writing my server in C, I could
set this method to a dummy function that
always returned true. And that would
allow me to accept unverified client
certificates. Unfortunately this
function is not exposed at the python
level, for some unknown reason (probably
because nobody ever bothered to
implement it).

I asked Sean how he accomplished this on
his gemini server's demo TLS page [3].

It turns out he is using Lua bindings
for LibreSSL, which is a fork of
OpenSSL. LibreSSL provides this handy
new setting called
config_insecure_noverifycert [4] that
allows client certificates to be
received without verifying them. This
setting is essentially a cleaner way to
accomplish what I described above of
short-circuiting the verify_callback to
always return true. I couldn't find docs
for LibreSSL, but the code itself is
pretty easy to follow along with [5].

So my conclusion is that accepting
unverified client certificates is...

- trivial in LibreSSL using
  ``config_insecure_noverifycert``
- confusing but possible in OpenSSL with
  ``SSL_CTX_set_cert_verify_callback``
- impossible using the python 3.7
  standard library ``ssl`` module

There might be other ways forward in
python by using an alternate ssl library
like pyOpenSSL [6]. But this is getting
way too complicated for me.  I would
rather keep Jetforce as an easy to
setup, zero-dependency server and simply
say that transient client certificates
are a feature that I can't support.

[1] https://docs.python.org/3/library/ssl.html#constants
[2] https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_cert_verify_callback.html
[3] gemini.conman.org/private/
[4] https://github.com/libressl-portable/openbsd/blob/c75a218cf8e99e07f144da7643d1c70958bca09a/src/lib/libtls/tls_config.c#L755
[5] https://github.com/libressl-portable/openbsd/blob/17e8de802aae447e78f675ddad85893b2963d510/src/lib/libtls/tls.c#L457
[6] https://www.pyopenssl.org/en/stable/api.html