[Security-sig] Unified TLS API for Python: Round 2

Christian Heimes christian at cheimes.de
Thu Jan 26 11:06:16 EST 2017


On 2017-01-26 15:23, Nick Coghlan wrote:
> On 26 January 2017 at 10:50, Cory Benfield <cory at lukasa.co.uk> wrote:
>>
>>> On 26 Jan 2017, at 07:49, Nick Coghlan <ncoghlan at gmail.com> wrote:
>>>
>>> Option 4: tls.TLSError, tls.WantReadError, tls.WantWriteError are
>>> defined as inheriting from ssl.SSLError, ssl.SSLWantReadError, and
>>> ssl.SSLWantWriteError *if* the latter are defined
>>>
>>> Option 5: as with Option 4, but the "ssl" module is also changed such
>>> that it *always* defines at least ssl.SSLError, ssl.SSLWantReadError,
>>> and ssl.SSLWantWriteError (and perhaps some of the other APIs that can
>>> be emulated atop the new tls abstraction), even if OpenSSL itself is
>>> unavailable
>>
>> Here’s my problem with this:
>>
>> try:
>>     socket.recv(8192)
>> except tls.WantWriteError:
>>     socket.write(some_buffer)
>>
>> This code does not work with the legacy ssl module, because isinstance(ssl.SSLWantWriteError, tls.WantWriteError) is false. This means that we need to write a shim over the legacy ssl module that wraps *all* API calls, catches all exceptions and then translates them into subclasses of the tls error classes. That seems entirely batty to me.
> 
> OK, so we have two competing problems here:
> 
> 1. How do we write *new* API client code that is agnostic to whether
> or not the security implementation is traditional ssl or a new tls
> backend?
> 2. How does a library that exports the ssl exceptions migrate to using
> a tls backend instead, without having to catch and rewrap tls
> exceptions in the legacy ones?
> 
> Meeting both constraints at the same time would require exception
> *aliases* rather than one set of exceptions inheriting from the other,
> such that we get:
> 
>     assert tls.TLSError is ssl.SSLError
>     assert tls.WantWriteError is ssl.SSLWantWriteError
>     assert tls.WantReadError is ssl.SSLWantReadError
> 
> In an ideal world, that could be handled just by having the ssl module
> import the new tls module and alias the exceptions accordingly.
> Talking to Christian about it, things might be a little messier in
> practice due to the way the _ssl/ssl C/Python split works, but there
> shouldn't be any insurmountable barriers to going down the exception
> aliasing path in 3.7+.
> 
> Backports to older versions would still need to contend with these
> being different exception objects, but one possible way of tackling
> that would be to inject a dummy ssl module into sys.modules before the
> regular one had a chance to be imported.

For technical reasons it is beneficial to define SSL exception in C
code. I don't want to force people to load _ssl to avoid the overhead of
OpenSSL loading and initialization. After some brain storming with Nick
I came up with the idea to define the exceptions in _socket. The _ssl
module imports a PyCapsule from _socket any way. I can just piggyback on
the PyCapsule. For maximum compatibility with Python 2, the tls module
should subclass from socket.error anyway, too.

Python 2:

>>> import ssl
>>> ssl.SSLError.__mro__
(<class 'ssl.SSLError'>, <class 'socket.error'>, <type
'exceptions.IOError'>, <type 'exceptions.EnvironmentError'>, <type
'exceptions.StandardError'>, <type 'exceptions.Exception'>, <type
'exceptions.BaseException'>, <type 'object'>)

Python 3:

>>> import ssl, socket
>>> ssl.SSLError.__mro__
(<class 'ssl.SSLError'>, <class 'OSError'>, <class 'Exception'>, <class
'BaseException'>, <class 'object'>)
>>> socket.error
<class 'OSError'>


This import block should (hopefully) cover all bases:

try:
    # CPython 3.7+
    from _socket import SSLError as TLSError
    from _socket import SSLWantWriteError as WantWriteError
    from _socket import SSLWantReadError as WantReadError
except ImportError:
    # CPython < 3.7 and other implementations
    try:
        from ssl import SSLError as TLSError
        from ssl import SSLWantWriteError as WantWriteError
        from ssl import SSLWantReadError as WantReadError
    except ImportError:
        # ssl module is not available
        # Python 2: socket.error is a subclass of IOError
        # Python 3: socket.error is just an alias of OSError
        import socket
        class TLSError(socket.error):
            pass
        class WantWriteError(TLSError):
            pass
        class WantReadError(TLSError):
            pass

Christian


More information about the Security-SIG mailing list