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 3.8+, 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

Programmatic usage:

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("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!

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 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

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.8+. Note that while trustme is maintained by the Trio project, trustme is happy to work with any networking library.

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
import ssl

# Create our fake certificates
ca = trustme.CA()
server_cert = ca.issue_cert("test-host.example.org")
client_cert = ca.issue_cert("client@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)

CLI reference

All options:

$ 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.

Default configuration:

$ 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!

API reference

class trustme.CA(parent_cert: CA | None = None, path_length: int = 9, organization_name: str | None = None, organization_unit_name: str | None = None, key_type: KeyType = KeyType.ECDSA)

A certificate authority.

property cert_pem: Blob

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

Type:

Blob

property private_key_pem: Blob

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

Type:

Blob

create_child_ca(key_type: KeyType = KeyType.ECDSA) 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_cert(*identities: str, common_name: str | None = None, organization_name: str | None = None, organization_unit_name: str | None = None, not_before: datetime | None = None, not_after: datetime | None = None, key_type: KeyType = KeyType.ECDSA) LeafCert

Issues a certificate. The certificate can be used for either servers or clients.

Parameters:
  • identities (str) –

    The identities that this certificate will be valid for. Most commonly, these are just hostnames, but we accept 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

    • Email address: example@example.com

    These ultimately end up as “Subject Alternative Names”, which are what modern programs are supposed to use when checking identity.

  • common_name (str | None) – 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 (str | None) – Sets the “Organization Name” (O) attribute on the certificate. By default, it will be “trustme” suffixed with a version number.

  • organization_unit_name (str | None) – Sets the “Organization Unit Name” (OU) attribute on the certificate. By default, a random one will be generated.

  • not_before (datetime | None) – Set the validity start date (notBefore) of the certificate. This argument type is datetime.datetime.

  • not_after (datetime | None) – Set the expiry date (notAfter) of the certificate. This argument type is datetime.datetime.

  • key_type (KeyType) – Set the type of key that is used for the certificate. By default this is an ECDSA based key.

Returns:

the newly-generated certificate.

Return type:

LeafCert

configure_trust(ctx: ssl.SSLContext | OpenSSL.SSL.Context) None

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

Parameters:

ctx (Union[ssl.SSLContext, OpenSSL.SSL.Context]) – The SSL context to be modified.

classmethod from_pem(cert_bytes: bytes, private_key_bytes: bytes) CA

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.

Parameters:
  • cert_bytes (bytes) – The bytes of the certificate in PEM format

  • private_key_bytes (bytes) – The bytes of the private key in PEM format

class trustme.LeafCert

A server or client certificate.

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

private_key_pem

The PEM-encoded private key corresponding to this certificate.

Type:

Blob

cert_chain_pems

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.

Type:

list of Blob objects

private_key_and_cert_chain_pem

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

Type:

Blob

configure_cert(ctx: ssl.SSLContext | OpenSSL.SSL.Context) None

Configure the given context object to present this certificate.

Parameters:

ctx (Union[ssl.SSLContext, 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() bytes

Returns the data as a bytes object.

with tempfile(dir: str | None = None) Generator[str, None, 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 encounter 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 | None) – Passed to tempfile.NamedTemporaryFile.

write_to_path(path: str | PathLike[str], append: bool = False) None

Writes the data to the file at the given path.

Parameters:
  • path (str | PathLike[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.

class trustme.KeyType(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)

Type of the key used to generate a certificate

RSA = 0
ECDSA = 1

Change history

Trustme 1.2.0 (2024-10-07)

Features

  • Add support for Python 3.13. (#664)

  • Allow setting of cert’s notBefore attribute (#628)

Bugfixes

  • Add the Authority Key Identifier extension to child CA certificates. (#642)

Deprecations and Removals

  • Remove support for Python 3.8 and PyPy 3.9. (#664)

Trustme 1.1.0 (2023-07-10)

Features

Deprecations and Removals

  • Remove support for Python 3.7. (#609)

Trustme 1.0.0 (2023-04-24)

Features

  • Support for ECDSA keys in certificates and use them by default. The type of key used for certificates can be controlled by the key_type parameter on the multiple methods that generate certificates. ECDSA certificates as they can be generated significantly faster. (#559)

  • Support for Python 3.10 and 3.11 (#372, 574)

Deprecations and Removals

  • Remove support for Python 2. trustme now requires Python>=3.7 (CPython or PyPy). (#346)

Trustme 0.9.0 (2021-08-12)

Features

  • The package is now type annotated. If you use mypy on code which uses trustme, you should be able to remove any exclusions. (#339)

Trustme 0.8.0 (2021-06-08)

Features

  • It’s now possible to set an expiry date on server certificates, either with --expires-on in the CLI or with not_after in trustme.CA.issue_cert. (#293)

  • Support Python 3.10 (#327)

  • Set correct KeyUsage and ExtendedKeyUsage extensions, per CA/B Forum baseline requirements. (#328)

Trustme 0.7.0 (2021-02-10)

Features

  • 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)

Features

  • Allow specifying organization and organization unit in CA and issued certs. (#126)

Trustme 0.5.3 (2019-10-31)

Features

  • Added CA.from_pem to import an existing certificate authority; this allows migrating to trustme step-by-step. (#107)

Trustme 0.5.2 (2019-06-03)

Bugfixes

  • Update to avoid a deprecation warning on cryptography 2.7. (#47)

Trustme 0.5.1 (2019-04-15)

Bugfixes

  • Update key size to 2048 bits, as required by recent Debian. (#45)

Trustme 0.5.0 (2019-01-21)

Features

  • Added CA.create_child_ca() to allow for certificate chains (#3)

  • Added CA.private_key_pem to export CA private keys; this allows signing other certs with the same CA outside of trustme. (#27)

  • CAs now include the KeyUsage and ExtendedKeyUsage extensions configured for SSL certificates. (#30)

  • CA.issue_cert now accepts email addresses as a valid form of identity. (#33)

  • It’s now possible to set the “common name” of generated certs; see CA.issue_cert for details. (#34)

  • CA.issue_server_cert has been renamed to CA.issue_cert, since it supports both server and client certs. To preserve backwards compatibility, the old name is retained as an undocumented alias. (#35)

Bugfixes

  • Make sure cert expiration dates don’t exceed 2038-01-01, to avoid issues on some 32-bit platforms that suffer from the Y2038 problem. (#41)

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.