python-trio/trustme

feature request (with provided source code) - training wheels

remdragon opened this issue · 4 comments

I spent a couple hours try to figure out how to use your amazing library to do server certificates only.

Once I finally got it working, I decided to encapsulate the logic and I came up with the following.

Perhaps you would be interested in adding something like this to your library:

# trustme.training_wheels.py
import ssl
import trustme

class ServerOnly:
	def __init__ ( self, *,
		server_hostname: str, # ex: 'test-host.example.org'
	) -> None:
		self.server_hostname = server_hostname
		self.ca = trustme.CA()
		self.server_cert = self.ca.issue_cert ( self.server_hostname )
	
	def server_context ( self ) -> ssl.SSLContext:
		ctx = ssl.create_default_context()
		ctx.check_hostname = False
		self.server_cert.configure_cert ( ctx )
		self.ca.configure_trust ( ctx )
		ctx.verify_mode = ssl.CERT_NONE
		return ctx
	
	def client_context ( self ) -> ssl.SSLContext:
		ctx = ssl.create_default_context()
		self.ca.configure_trust ( ctx )
		return ctx

class ClientServer:
	def __init__ ( self, *,
		client_hostname: str, # ex: 'client@example.org'
		server_hostname: str, # ex: 'test-host.example.org'
	) -> None:
		self.client_hostname = client_hostname
		self.server_hostname = server_hostname
		self.ca = trustme.CA()
		self.client_cert = self.ca.issue_cert ( self.client_hostname )
		self.server_cert = self.ca.issue_cert ( self.server_hostname )
	
	def server_context ( self ) -> ssl.SSLContext:
		ctx = ssl.create_default_context ( ssl.Purpose.CLIENT_AUTH )
		self.server_cert.configure_cert ( ctx )
		self.ca.configure_trust ( ctx )
		ctx.verify_mode = ssl.CERT_REQUIRED
		return ctx
	
	def client_context ( self ) -> ssl.SSLContext:
		ctx = ssl.create_default_context()
		self.ca.configure_trust ( ctx )
		self.client_cert.configure_cert ( ctx )
		return ctx

And then new users could use it like so:

# trustme-trio-example.py

import trustme.training_wheels
import trio
import ssl

# Create our fake certificates
trust = trustme.training_wheels.ClientServer (
	client_hostname = 'client@example.org',
	server_hostname = 'test-host.example.org',
)

async def demo_server(server_raw_stream):
    server_ssl_context = trust.server_context()

    server_ssl_stream = trio.SSLStream(
        server_raw_stream,
        server_ssl_context,
        server_side=True,
    )

    # Send some data to check that the connection is really working
    await server_ssl_stream.send_all(b"x")
    print("Server successfully sent data over the encrypted channel!")
    print("Client cert looks like:", server_ssl_stream.getpeercert())


async def demo_client(client_raw_stream):
    client_ssl_context = trust.client_context()

    client_ssl_stream = trio.SSLStream(
        client_raw_stream,
        client_ssl_context,
        # Tell the client that it's looking for a trusted cert for this
        # particular hostname (must match what we passed to issue_cert)
        server_hostname="test-host.example.org",
    )

    assert await client_ssl_stream.receive_some(1) == b"x"
    print("Client successfully received data over the encrypted channel!")
    print("Server cert looks like:", client_ssl_stream.getpeercert())


async def main():
    from trio.testing import memory_stream_pair
    server_raw_stream, client_raw_stream = memory_stream_pair()

    async with trio.open_nursery() as nursery:
        nursery.start_soon(demo_server, server_raw_stream)
        nursery.start_soon(demo_client, client_raw_stream)


trio.run(main)

I'm glad that you found trustme useful, and am sorry that it took so much time to be able to use it correctly.

The reason trustme is a good abstraction is that it moves operations from the "x509 level" to the "ssl standard module level". It's still difficult to use for new users, but I think it's mainly for users that are not familiar with certificates (as I was not long ago), not because the API is difficult to use or too verbose.

Thanks for providing your source code, it makes your proposal much more explicit. However, for the reasons above, I'm not sure it would actually help new users. It's difficult for us to be sure of that, as, well, we're no longer new users: you learned a lot by creating training_wheels.py. But it does not seem to save a lot of code, and it does not seem to operate at a different abstraction level.

In other words, I'd be tempted to reject this proposal, but would be happy to know what others think.

Again, thank you for the interest and provided a very fleshed out piece of code. I'm also leaning towards -1 on this as well due to already having a full working example in the documentation. Is this section sufficient or should it be augmented?