axTLS-based modussl: ussl.wrap_socket silently accepts invalid certificates
Investigating https://github.com/micropython/micropython-lib/issues/69, I found the current SSL/TLS socket creation code at https://github.com/micropython/micropython/blob/d19e4f0ba4df487b4ebd36b5fe6a16e68c0afe77/extmod/modussl.c#L49
If I'm reading that correctly:
- Wrapping a socket without providing any certificate verification details results in no verification being performed;
- Even if verification details are provided, they're still ignored
This makes the documentation at http://docs.micropython.org/en/latest/library/ussl.html#ussl.ssl.wrap_socket thoroughly misleading, as even if the additional arguments are passed in, they won't be processed.
I realise actually implementing this will require a significant amount of work, so my request at this point would be for passing in unsupported arguments to result in a hard failure, rather than silently appearing to succeed without actually providing the claimed security guarantees.
RFC: change ussl to implement SSLContext
Currently MicroPython supports SSL/TLS connections using ussl.wrap_socket(). CPython long ago replaced this function with ssl.SSLContext:
Since Python 3.2 and 2.7.9, it is recommended to use the SSLContext.wrap_socket() of an SSLContext instance to wrap sockets as SSLSocket objects.
And in CPython ssl.wrap_socket() is deprecated since CPython version 3.7.
I propose that MicroPython also switch to using SSLContext. The reasons to change are:
- To more closely match CPython (it's hard to get TLS correct and having things match CPython makes it easier to write tests to compare uPy and CPy).
- To support more sophisticated use of TLS connections (
SSLContextprovide more control/options). - So that
uasynciocan support TLS clients and servers (CPythonasynciorequires passing in anSSLContextobject toasyncio.start_server()andasyncio.open_connection()). - To support
server_hostnamein a CPython-compatible way.
One other benefit of using SSLContext is that it can be used to preallocate the large TLS buffers (16K) needed for a wrapped socket, and the buffer can be reused over and over as long as only one wrapped socket is needed at a time. This will help to reduce memory fragmentation and guarantee that sockets can be wrapped without running out of memory.
The proposed ussl module would now look like this:
# constants
PROTOCOL_TLS_CLIENT
PROTOCOL_TLS_SERVER
class SSLSocket:
# same as existing ssl socket object
class SSLContext:
def __init__(self, protocol):
# protocol is PROTOCOL_TLS_CLIENT or PROTOCOL_TLS_SERVER
def load_cert_chain(self, certfile, keyfile):
# load certificate/key from a file
def wrap_socket(self, sock, *, server_side=False, do_handshake_on_connect=True, server_hostname=None):
# wrap a socket and return an SSLSocket
def wrap_socket(...):
# existing function for backwards compatibility
Things to discuss/decide:
- Should probably keep the existing
ussl.wrap_socket()for backwards compatibility for a couple of releases. It could print a deprecation warning. load_cert_chain()takes filenames as arguments and loads the key/cert from a file. This is different to the existingkey/certargs inussl.wrap_socket()(which are not CPython compatible) that take a str/bytes object containing the key/cert data. The question is if we should add, as a MicroPython-extension, a way to pass in data directly like this, and if yes how that would be done. For example add keyword-only args likeload_cert_chain(self, certfile, keyfile, *, keydata, certdata), or a new method likeload_cert_chain_data(self, keydata, certdata).- Whether to implement it in C or Python.