trustme: #1 quality TLS certs while you wait¶
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
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.
pip install -U trustme
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.
import trustme # ----- Creating certs ----- # Look, you just created your 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_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)
Command line usage:
$ # Certs may be generated from anywhere. Here's where we are: $ pwd /tmp $ # ----- Creating certs ----- $ python -m trustme Generated a certificate for 'localhost', '127.0.0.1', '::1' Configure your server to use the following files: cert=/tmp/server.pem key=/tmp/server.key Configure your client to use the following files: cert=/tmp/client.pem $ # ----- Using certs ----- $ gunicorn --keyfile server.key --certfile server.pem app:app $ curl --cacert client.pem https://localhost:8000/ Hello, world!
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 bizarre TLS configuration? We think trustme hits a sweet spot of ease-of-use and generality as it is. The defaults are carefully chosen to work on all major operating systems and be as fast as possible. We don’t want to turn trustme into a second-rate re-export of everything in cryptography. If you have more complex needs, consider using them directly, possibly starting from the trustme code.
Will you automate installing CA cert into system trust store? No. mkcert already does this well, and we would not have anything to add.
Full working example¶
This example requires Trio (
install -U trio) and Python 3.5+. Note that while
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_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
# trustme-trio-example.py import trustme import trio import ssl # Create our fake certificates ca = trustme.CA() server_cert = ca.issue_cert(u"test-host.example.org") client_cert = ca.issue_cert(u"firstname.lastname@example.org") async def demo_server(server_raw_stream): server_ssl_context = ssl.create_default_context( ssl.Purpose.CLIENT_AUTH) # Set up the server's SSLContext to use our fake server cert server_cert.configure_cert(server_ssl_context) # Set up the server's SSLContext to trust our fake CA, that signed # our client cert, so that it can validate client's cert. ca.configure_trust(server_ssl_context) # Verify that client sent us their TLS cert signed by a trusted CA server_ssl_context.verify_mode = ssl.CERT_REQUIRED 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 = ssl.create_default_context() # Set up the client's SSLContext to trust our fake CA, that signed # our server cert, so that it can validate server's cert. ca.configure_trust(client_ssl_context) # Set up the client's SSLContext to use our fake client cert client_cert.configure_cert(client_ssl_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)
$ python -m trustme --help usage: trustme [-h] [-d DIR] [-i [IDENTITIES [IDENTITIES ...]]] [--common-name COMMON_NAME] [-q] optional arguments: -h, --help Show this help message and exit. -d DIR, --dir DIR Directory where certificates and keys are written to. Defaults to cwd. -i [IDENTITIES [IDENTITIES ...]], --identities [IDENTITIES [IDENTITIES ...]] Identities for the certificate. Defaults to 'localhost 127.0.0.1 ::1'. --common-name COMMON_NAME Also sets the deprecated 'commonName' field. -q, --quiet Doesn't print out helpful information for humans.
$ cd /tmp/ $ python -m trustme Generated a certificate for 'localhost', '127.0.0.1', '::1' Configure your server to use the following files: cert=/tmp/server.pem key=/tmp/server.key Configure your client to use the following files: cert=/tmp/client.pem
Designate different identities:
$ python -m trustme -i www.example.org example.org Generated a certificate for 'www.example.org', 'example.org' Configure your server to use the following files: cert=/tmp/server.pem key=/tmp/server.key Configure your client to use the following files: cert=/tmp/client.pem
Generate files into a directory:
$ mkdir /tmp/a $ python -m trustme -d /tmp/a Generated a certificate for 'localhost', '127.0.0.1', '::1' Configure your server to use the following files: cert=/tmp/a/server.pem key=/tmp/a/server.key Configure your client to use the following files: cert=/tmp/a/client.pem
Configure certs for server/client:
$ gunicorn --keyfile /tmp/a/server.key --certfile /tmp/a/server.pem app:app $ curl --cacert /tmp/a/client.pem https://localhost:8000 Hello, world!
- class trustme.CA(parent_cert=None, path_length=9, organization_name=None, organization_unit_name=None)¶
A certificate authority.
- property cert_pem¶
The PEM-encoded certificate for this CA. Add this to your trust store to trust this CA.
- property private_key_pem¶
The PEM-encoded private key for this CA. Use this to sign other certificates from this CA.
Creates a child certificate authority
- issue_cert(*identities, common_name=None, organization_name=None, organization_unit_name=None, not_after=None)¶
Issues a certificate. The certificate can be used for either servers or clients.
All arguments must be text strings (
unicodeon Python 2,
stron Python 3).
The identities that this certificate will be valid for. Most commonly, these are just hostnames, but we accept any of the following forms:
International Domain Name (IDN):
IDN in A-label form:
These ultimately end up as “Subject Alternative Names”, which are what modern programs are supposed to use when checking identity.
common_name – Sets the “Common Name” of the certificate. This is a legacy field that used to be used to check identity. It’s an arbitrary string with poorly-defined semantics, so modern programs are supposed to ignore it. But it might be useful if you need to test how your software handles legacy or buggy certificates.
organization_name – Sets the “Organization Name” (O) attribute on the certificate. By default, it will be “trustme” suffixed with a version number.
organization_unit_name – Sets the “Organization Unit Name” (OU) attribute on the certificate. By default, a random one will be generated.
not_after – Set the expiry date (notAfter) of the certificate. This argument type is
the newly-generated certificate.
- Return type
Configure the given context object to trust certificates signed by this CA.
- classmethod from_pem(cert_bytes, private_key_bytes)¶
Build a CA from existing cert and private key.
This is useful if your test suite has an existing certificate authority and you’re not ready to switch completely to trustme just yet.
- class trustme.LeafCert¶
A server or client certificate.
This type has no public constructor; you get one by calling
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.
Blobcontaining the concatenation of the PEM-encoded private key and the PEM-encoded cert chain.
- class trustme.Blob¶
A convenience wrapper for a blob of bytes.
- 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.
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)
Trustme 0.8.0 (2021-06-08)¶
Trustme 0.7.0 (2021-02-10)¶
trustme can now be used a command line interface with
python -m trustme. Get the help with
python -m trustme --help. (#265)
Trustme 0.6.0 (2019-12-19)¶
Allow specifying organization and organization unit in CA and issued certs. (#126)
Trustme 0.5.3 (2019-10-31)¶
Trustme 0.5.2 (2019-06-03)¶
Update to avoid a deprecation warning on cryptography 2.7. (#47)
Trustme 0.5.1 (2019-04-15)¶
Update key size to 2048 bits, as required by recent Debian. (#45)
Trustme 0.5.0 (2019-01-21)¶
CAs now include the KeyUsage and ExtendedKeyUsage extensions configured for SSL certificates. (#30)
Trustme 0.4.0 (2017-08-06)¶
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)¶
Don’t crash on Windows (#10)
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)¶
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.