trustme: #1 quality TLS certs while you wait

https://vignette2.wikia.nocookie.net/jadensadventures/images/1/1e/Kaa%27s_hypnotic_eyes.jpg/revision/latest?cb=20140310173415

You wrote a cool network client or server. It encrypts connections using TLS. Your test suite needs to make TLS connections to itself.

Uh oh. Your test suite probably doesn’t have a valid TLS certificate. Now what?

trustme is a tiny Python package that does one thing: it gives you a fake certificate authority (CA) that you can use to generate fake TLS certs to use in your tests. Well, technically they’re real certs, they’re just signed by your CA, which nobody trusts. But you can trust it. Trust me.

Vital statistics

Install: pip install -U trustme

Documentation: https://trustme.readthedocs.io

Bug tracker and source code: https://github.com/python-trio/trustme

Tested on: Python 2.7 and Python 3.5+, CPython and PyPy

License: MIT or Apache 2, your choice.

Code of conduct: Contributors are requested to follow our code of conduct in all project spaces.

Cheat sheet

import trustme

# ----- Creating certs -----

# Look, you just created your own certificate authority!
ca = trustme.CA()

# And now you issued a cert signed by this fake CA
# https://en.wikipedia.org/wiki/Example.org
server_cert = ca.issue_server_cert(u"test-host.example.org")

# That's it!

# ----- Using your shiny new certs -----

# You can configure SSL context objects to trust this CA:
ca.configure_trust(ssl_context)
# Or configure them to present the server certificate
server_cert.configure_cert(ssl_context)
# You can use standard library or PyOpenSSL context objects here,
# trustme is happy either way.

# ----- or -----

# Save the PEM-encoded data to a file to use in non-Python test
# suites:
ca.cert_pem.write_to_path("ca.pem")
server_cert.private_key_and_cert_chain_pem.write_to_path("server.pem")

# ----- or -----

# Put the PEM-encoded data in a temporary file, for libraries that
# insist on that:
with ca.cert_pem.tempfile() as ca_temp_path:
    requests.get("https://...", verify=ca_temp_path)

FAQ

Should I use these certs for anything real? Certainly not.

Why not just use self-signed certificates? These are more realistic. You don’t have to disable your certificate validation code in your test suite, which is good, because you want to test what you run in production, and you would never disable your certificate validation code in production, right? Plus they’re just as easy to work with. Actually easier, in many cases.

What if I want to test how my code handles some really weird TLS configuration? Sure, I’m happy to extend the API to give more control over the generated certificates, at least as long as it doesn’t turn into a second-rate re-export of everything in cryptography. (If you really need a fully general X.509 library then they do a great job at that.) Let’s talk, or send a PR.

Full working example

Here’s a fully working example you can run to see how trustme works. It demonstrates a simple TLS server and client that connect to each other using trustme-generated certs.

This example requires Trio (pip install -U trio) and Python 3.5+. Note that while trustme is maintained by the Trio project, trustme is happy to work with any networking library, and also supports Python 2.

The key lines are the calls to configure_trust(), configure_cert() – try commenting them out one at a time to see what happens! Also notice that the hostname test-host.example.org appears twice – try changing one of the strings so that the two copies no longer match, and see what happens then!

# trustme-trio-example.py

import trustme
import trio

# Create our fake certificates
ca = trustme.CA()
server_cert = ca.issue_server_cert(u"test-host.example.org")


async def demo_server(server_raw_stream):
    server_ssl_context = trio.ssl.create_default_context(
        trio.ssl.Purpose.CLIENT_AUTH)

    # Set up the server's SSLContext to use our fake server cert
    server_cert.configure_cert(server_ssl_context)

    server_ssl_stream = trio.ssl.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")


async def demo_client(client_raw_stream):
    client_ssl_context = trio.ssl.create_default_context()

    # Set up the client's SSLContext to trust our fake CA, that signed our
    # server cert
    ca.configure_trust(client_ssl_context)

    client_ssl_stream = trio.ssl.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_server_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("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)

API reference

class trustme.CA(parent_cert=None, path_length=9)

A certificate authority.

cert_pem

Blob – The PEM-encoded certificate for this CA. Add this to your trust store to trust this CA.

private_key_pem

Blob – The PEM-encoded private key for this CA. Use this to sign other certificates from this CA.

create_child_ca()

Creates a child certificate authority

Returns:the newly-generated certificate authority
Return type:CA
Raises:ValueError – if the CA path length is 0
issue_server_cert(*hostnames)

Issues a server certificate.

Parameters:*hostnames

The hostname or hostnames that this certificate will be valid for, as a text string (unicode on Python 2, str on Python 3). That string can be in any of the following forms:

  • Regular hostname: example.com
  • Wildcard hostname: *.example.com
  • International Domain Name (IDN): café.example.com
  • IDN in A-label form: xn--caf-dma.example.com
  • IPv4 address: 127.0.0.1
  • IPv6 address: ::1
  • IPv4 network: 10.0.0.0/8
  • IPv6 network: 2001::/16
Returns:the newly-generated server certificate.
Return type:LeafCert
configure_trust(ctx)

Configure the given context object to trust certificates signed by this CA.

Parameters:ctx (ssl.SSLContext or OpenSSL.SSL.Context) – The SSL context to be modified.
class trustme.LeafCert

A server or client certificate.

This type has no public constructor; you get one by calling CA.issue_server_cert or similar.

private_key_pem

Blob – The PEM-encoded private key corresponding to this certificate.

cert_chain_pems

list of Blob objects – The zeroth entry in this list is the actual PEM-encoded certificate, and any entries after that are the rest of the certificate chain needed to reach the root CA.

private_key_and_cert_chain_pem

Blob – A single Blob containing the concatenation of the PEM-encoded private key and the PEM-encoded cert chain.

configure_cert(ctx)

Configure the given context object to present this certificate.

Parameters:ctx (ssl.SSLContext or OpenSSL.SSL.Context) – The SSL context to be modified.
class trustme.Blob

A convenience wrapper for a blob of bytes.

This type has no public constructor. They’re used to provide a handy interface to the PEM-encoded data generated by trustme. For example, see CA.cert_pem or LeafCert.private_key_and_cert_chain_pem.

bytes()

Returns the data as a bytes object.

with tempfile(dir=None) as path

Context manager for writing data to a temporary file.

The file is created when you enter the context manager, and automatically deleted when the context manager exits.

Many libraries have annoying APIs which require that certificates be specified as filesystem paths, so even if you have already the data in memory, you have to write it out to disk and then let them read it back in again. If you encouter such a library, you should probably file a bug. But in the mean time, this context manager makes it easy to give them what they want.

Example

Here’s how to get requests to use a trustme CA (see also):

ca = trustme.CA()
with ca.cert_pem.tempfile() as ca_cert_path:
    requests.get("https://localhost/...", verify=ca_cert_path)
Parameters:dir (str or None) – Passed to tempfile.NamedTemporaryFile.
write_to_path(path, append=False)

Writes the data to the file at the given path.

Parameters:
  • path (str) – The path to write to.
  • append (bool) – If False (the default), replace any existing file with the given name. If True, append to any existing file.

Change history

Trustme 0.4.0 (2017-08-06)

Features

Bugfixes

  • Start doing our own handling of Unicode hostname (IDNs), instead of relying on cryptography to do it; this allows us to correctly handle a broader range of cases, and avoids relying on soon-to-be-deprecated behavior (#17)
  • Generated certs no longer contain a subject:commonName field, to better match CABF guidelines (#18)

Trustme 0.3.0 (2017-08-03)

Bugfixes

  • Don’t crash on Windows (#10)

Misc

Trustme 0.2.0 (2017-08-02)

  • Broke and re-did almost the entire public API. Sorry! Let’s just pretend v0.1.0 never happened.
  • Hey there are docs now though, that should be worth something right?

Trustme 0.1.0 (2017-07-18)

  • Initial release

Acknowledgements

This is basically just a trivial wrapper around the awesome Python cryptography library. Also, Glyph originally wrote most of the tricky bits. I got tired of never being able to remember how this works or find the magic snippets to copy/paste, so I stole the code out of Twisted and wrapped it in a bow.