././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1628798670.3974829 trustme-0.9.0/0000755000076500000240000000000000000000000012572 5ustar00quentinstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1576764298.0 trustme-0.9.0/CODE_OF_CONDUCT.md0000644000076500000240000000016600000000000015374 0ustar00quentinstaffThe Trio code of conduct applies to this project. See: https://trio.readthedocs.io/en/latest/code-of-conduct.html ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1576764298.0 trustme-0.9.0/LICENSE0000644000076500000240000000027100000000000013577 0ustar00quentinstaffThis software is made available under the terms of *either* of the licenses found in LICENSE.APACHE2 or LICENSE.MIT. Contributions to are made under the terms of *both* these licenses. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1576764298.0 trustme-0.9.0/LICENSE.APACHE20000644000076500000240000002613600000000000014611 0ustar00quentinstaff Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1576764298.0 trustme-0.9.0/LICENSE.MIT0000644000076500000240000000202600000000000014227 0ustar00quentinstaffThe MIT License (MIT) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1576764298.0 trustme-0.9.0/MANIFEST.in0000644000076500000240000000026400000000000014332 0ustar00quentinstaffinclude LICENSE LICENSE.MIT LICENSE.APACHE2 include README.rst CODE_OF_CONDUCT.md include test-requirements.txt recursive-include docs * recursive-include tests * prune docs/build ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1628798670.3979592 trustme-0.9.0/PKG-INFO0000644000076500000240000001476100000000000013700 0ustar00quentinstaffMetadata-Version: 1.1 Name: trustme Version: 0.9.0 Summary: #1 quality TLS certs while you wait, for the discerning tester Home-page: https://github.com/python-trio/trustme Author: Nathaniel J. Smith Author-email: njs@pobox.com License: MIT -or- Apache License 2.0 Description: .. note that this README gets 'include'ed into the main documentation ============================================== trustme: #1 quality TLS certs while you wait ============================================== .. image:: https://vignette2.wikia.nocookie.net/jadensadventures/images/1/1e/Kaa%27s_hypnotic_eyes.jpg/revision/latest?cb=20140310173415 :width: 200px :align: right 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 =========== Programmatic usage: .. code-block:: python 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: .. code-block:: console $ # 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. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Topic :: System :: Networking Classifier: Topic :: Security :: Cryptography Classifier: Topic :: Software Development :: Testing ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1615922707.0 trustme-0.9.0/README.rst0000644000076500000240000001046000000000000014262 0ustar00quentinstaff.. note that this README gets 'include'ed into the main documentation ============================================== trustme: #1 quality TLS certs while you wait ============================================== .. image:: https://vignette2.wikia.nocookie.net/jadensadventures/images/1/1e/Kaa%27s_hypnotic_eyes.jpg/revision/latest?cb=20140310173415 :width: 200px :align: right 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 =========== Programmatic usage: .. code-block:: python 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: .. code-block:: console $ # 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. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1628798670.370821 trustme-0.9.0/docs/0000755000076500000240000000000000000000000013522 5ustar00quentinstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1576764298.0 trustme-0.9.0/docs/Makefile0000644000076500000240000000114300000000000015161 0ustar00quentinstaff# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = python -msphinx SPHINXPROJ = trustme SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1576764298.0 trustme-0.9.0/docs/make.bat0000644000076500000240000000145100000000000015130 0ustar00quentinstaff@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=python -msphinx ) set SOURCEDIR=source set BUILDDIR=build set SPHINXPROJ=trustme if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The Sphinx module was not found. Make sure you have Sphinx installed, echo.then set the SPHINXBUILD environment variable to point to the full echo.path of the 'sphinx-build' executable. Alternatively you may add the echo.Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1628798670.3749979 trustme-0.9.0/docs/source/0000755000076500000240000000000000000000000015022 5ustar00quentinstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1628798670.3764336 trustme-0.9.0/docs/source/_static/0000755000076500000240000000000000000000000016450 5ustar00quentinstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1576764298.0 trustme-0.9.0/docs/source/_static/.gitkeep0000644000076500000240000000000000000000000020067 0ustar00quentinstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1628798670.3773646 trustme-0.9.0/docs/source/_templates/0000755000076500000240000000000000000000000017157 5ustar00quentinstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1576764298.0 trustme-0.9.0/docs/source/_templates/need-help.html0000644000076500000240000000032400000000000021705 0ustar00quentinstaff

Need help?

Try chat or StackOverflow.
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1576764298.0 trustme-0.9.0/docs/source/conf.py0000644000076500000240000001227100000000000016324 0ustar00quentinstaff#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # trustme documentation build configuration file, created by # sphinx-quickstart on Tue Jul 18 01:46:01 2017. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys sys.path.insert(0, os.path.abspath('../..')) # Warn about all references to unknown targets nitpicky = True html_sidebars = { "**": [ "localtoc.html", "relations.html", "searchbox.html", "need-help.html", ], } # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', #'sphinx.ext.viewcode', 'sphinx.ext.napoleon', 'sphinxcontrib_trio', ] intersphinx_mapping = { "python": ('https://docs.python.org/3', None), "pyopenssl": ('https://www.pyopenssl.org/en/stable/', None), "trio": ('https://trio.readthedocs.io/en/latest/', None), } autodoc_member_order = "bysource" # Tell sphinx to treat bare backticks like `foo` as :py:obj:`foo` default_role = 'py:obj' # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'trustme' copyright = '2017, Nathaniel J. Smith' author = 'Nathaniel J. Smith' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # import trustme # The short X.Y version. version = trustme.__version__ # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' highlight_language = 'python3' # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. htmlhelp_basename = 'trustmedoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'trustme.tex', 'trustme Documentation', 'Nathaniel J. Smith', 'manual'), ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'trustme', 'trustme Documentation', [author], 1) ] # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'trustme', 'trustme Documentation', author, 'trustme', 'One line description of project.', 'Miscellaneous'), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628798349.0 trustme-0.9.0/docs/source/index.rst0000644000076500000240000002027000000000000016664 0ustar00quentinstaff.. module:: trustme .. include:: ../../README.rst Full working example ==================== Here's a fully working example you can run to see how :mod:`trustme` works. It demonstrates a simple TLS server and client that connect to each other using :mod:`trustme`\-generated certs. This example requires `Trio `__ (``pip install -U trio``) and Python 3.5+. Note that while :mod:`trustme` is maintained by the Trio project, :mod:`trustme` is happy to work with any networking library, and also supports Python 2. The key lines are the calls to :meth:`~CA.configure_trust`, :meth:`~LeafCert.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! .. literalinclude:: trustme-trio-example.py CLI reference ============= **All options:** .. code-block:: console $ 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:** .. code-block:: console $ 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:** .. code-block:: console $ 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:** .. code-block:: console $ 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:** .. code-block:: console $ 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 ============= .. autoclass:: CA :members: :exclude-members: issue_server_cert .. autoclass:: LeafCert() :members: .. autoclass:: Blob() .. automethod:: bytes .. automethod:: tempfile :with: path .. automethod:: write_to_path Change history ============== .. towncrier release notes start 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 :attr:`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 :meth:`CA.create_child_ca` to allow for certificate chains (`#3 `__) - Added :attr:`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 ~~~~~~~~ - :meth:`CA.issue_cert` now accepts IP addresses and IP networks. (`#19 `__) 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 ~~~~ - `#11 `__, `#12 `__ 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1576764298.0 trustme-0.9.0/docs/source/trustme-trio-example.py0000644000076500000240000000446700000000000021516 0ustar00quentinstaff# 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"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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628798285.0 trustme-0.9.0/pyproject.toml0000644000076500000240000000265700000000000015520 0ustar00quentinstaff[tool.towncrier] # Usage: # - PRs should drop a file like "issuenumber.feature" in newsfragments # (or "bugfix", "doc", "removal", "misc"; misc gets no text, we can # customize this) # - At release time after bumping version number, run: towncrier # (or towncrier --draft) # - Make sure to use a version with the PRs mentioned below merged. # You probably want https://github.com/hawkowl/towncrier/pull/69 too. # Right now on my laptop it's # PYTHONPATH=~/src/towncrier/src ~/src/towncrier/bin/towncrier # with the merge-64-66-69 branch checked out. package = "trustme" filename = "docs/source/index.rst" directory = "newsfragments" # Requires https://github.com/hawkowl/towncrier/pull/64 underlines = ["-", "~", "^"] # Requires https://github.com/hawkowl/towncrier/pull/66 issue_format = "`#{issue} `__" [tool.mypy] check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true no_implicit_reexport = true show_error_codes = true strict_equality = true warn_redundant_casts = true warn_return_any = true warn_unreachable = true warn_unused_configs = true # Some ignores are only for python2/python3. # warn_unused_ignores = true [[tool.mypy.overrides]] module = "pytest" ignore_missing_imports = true ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1628798670.3994179 trustme-0.9.0/setup.cfg0000644000076500000240000000010300000000000014405 0ustar00quentinstaff[bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628798285.0 trustme-0.9.0/setup.py0000644000076500000240000000325200000000000014306 0ustar00quentinstafffrom setuptools import setup, find_packages # defines __version__ exec(open("trustme/_version.py").read()) setup( name="trustme", version=__version__, description= "#1 quality TLS certs while you wait, for the discerning tester", long_description=open("README.rst").read(), author="Nathaniel J. Smith", author_email="njs@pobox.com", license="MIT -or- Apache License 2.0", packages=find_packages(), package_data={ 'trustme': ['py.typed'], }, url="https://github.com/python-trio/trustme", install_requires=[ "cryptography", # cryptography depends on both of these too, so we should declare our # dependencies to be accurate, but they don't actually cost anything: "idna", "ipaddress; python_version < '3.3'", ], classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Topic :: System :: Networking", "Topic :: Security :: Cryptography", "Topic :: Software Development :: Testing", ]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623175100.0 trustme-0.9.0/test-requirements.txt0000644000076500000240000000364000000000000017036 0ustar00quentinstaff# # This file is autogenerated by pip-compile # To update, run: # # pip-compile test-requirements.in # atomicwrites==1.4.0 # via pytest attrs==21.2.0 # via # pytest # service-identity backports.functools-lru-cache==1.6.4 # via wcwidth cffi==1.14.5 # via cryptography configparser==4.0.2 # via importlib-metadata contextlib2==0.6.0.post1 # via # importlib-metadata # zipp coverage==5.5 # via pytest-cov cryptography==2.9.2 # via # -r test-requirements.in # pyopenssl # service-identity enum34==1.1.10 # via cryptography funcsigs==1.0.2 # via pytest futures==3.1.1 ; python_version < "3.2" # via -r test-requirements.in idna==2.10 ; python_version < "3" # via -r test-requirements.in importlib-metadata==2.0.0 # via # pluggy # pytest ipaddress==1.0.23 # via # cryptography # service-identity more-itertools==5.0.0 ; python_version < "3" # via # -r test-requirements.in # pytest packaging==20.9 # via pytest pathlib2==2.3.5 # via # importlib-metadata # pytest pluggy==0.13.1 # via pytest py==1.10.0 # via pytest pyasn1-modules==0.2.8 # via service-identity pyasn1==0.4.8 # via # pyasn1-modules # service-identity pycparser==2.20 # via cffi pyopenssl==19.1.0 # via -r test-requirements.in pyparsing==2.4.7 # via packaging pytest-cov==2.12.1 # via -r test-requirements.in pytest==4.6.3 # via # -r test-requirements.in # pytest-cov scandir==1.10.0 # via pathlib2 service-identity==21.1.0 # via -r test-requirements.in six==1.16.0 # via # cryptography # more-itertools # pathlib2 # pyopenssl # pytest # service-identity toml==0.10.2 # via pytest-cov wcwidth==0.2.5 # via pytest zipp==1.2.0 ; python_version < "3" # via # -r test-requirements.in # importlib-metadata ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1628798670.3804646 trustme-0.9.0/tests/0000755000076500000240000000000000000000000013734 5ustar00quentinstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628798285.0 trustme-0.9.0/tests/test_cli.py0000644000076500000240000000625700000000000016126 0ustar00quentinstaff# -*- coding: utf-8 -*- import subprocess import sys import py import pytest from trustme._cli import main TYPE_CHECKING = False if TYPE_CHECKING: # pragma: no cover from typing import Any def test_trustme_cli(tmpdir): # type: (py.path.local) -> None with tmpdir.as_cwd(): main(argv=[]) assert tmpdir.join("server.key").check(exists=1) assert tmpdir.join("server.pem").check(exists=1) assert tmpdir.join("client.pem").check(exists=1) def test_trustme_cli_e2e(tmpdir): # type: (py.path.local) -> None with tmpdir.as_cwd(): rv = subprocess.call([sys.executable, "-m", "trustme"]) assert rv == 0 assert tmpdir.join("server.key").check(exists=1) assert tmpdir.join("server.pem").check(exists=1) assert tmpdir.join("client.pem").check(exists=1) def test_trustme_cli_directory(tmpdir): # type: (py.path.local) -> None subdir = tmpdir.mkdir("sub") main(argv=["-d", str(subdir)]) assert subdir.join("server.key").check(exists=1) assert subdir.join("server.pem").check(exists=1) assert subdir.join("client.pem").check(exists=1) def test_trustme_cli_directory_does_not_exist(tmpdir): # type: (py.path.local) -> None notdir = tmpdir.join("notdir") with pytest.raises(ValueError, match="is not a directory"): main(argv=["-d", str(notdir)]) def test_trustme_cli_identities(tmpdir): # type: (py.path.local) -> None with tmpdir.as_cwd(): main(argv=["-i", "example.org", "www.example.org"]) assert tmpdir.join("server.key").check(exists=1) assert tmpdir.join("server.pem").check(exists=1) assert tmpdir.join("client.pem").check(exists=1) def test_trustme_cli_identities_empty(tmpdir): # type: (py.path.local) -> None with pytest.raises(ValueError, match="at least one identity"): main(argv=["-i"]) def test_trustme_cli_common_name(tmpdir): # type: (py.path.local) -> None with tmpdir.as_cwd(): main(argv=["--common-name", "localhost"]) assert tmpdir.join("server.key").check(exists=1) assert tmpdir.join("server.pem").check(exists=1) assert tmpdir.join("client.pem").check(exists=1) def test_trustme_cli_expires_on(tmpdir): # type: (py.path.local) -> None with tmpdir.as_cwd(): main(argv=["--expires-on", "2035-03-01"]) assert tmpdir.join("server.key").check(exists=1) assert tmpdir.join("server.pem").check(exists=1) assert tmpdir.join("client.pem").check(exists=1) def test_trustme_cli_invalid_expires_on(tmpdir): # type: (py.path.local) -> None with tmpdir.as_cwd(): with pytest.raises(ValueError, match="does not match format"): main(argv=["--expires-on", "foobar"]) assert tmpdir.join("server.key").check(exists=0) assert tmpdir.join("server.pem").check(exists=0) assert tmpdir.join("client.pem").check(exists=0) def test_trustme_cli_quiet(capsys, tmpdir): # type: (Any, py.path.local) -> None with tmpdir.as_cwd(): main(argv=["-q"]) assert tmpdir.join("server.key").check(exists=1) assert tmpdir.join("server.pem").check(exists=1) assert tmpdir.join("client.pem").check(exists=1) captured = capsys.readouterr() assert not captured.out ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628798285.0 trustme-0.9.0/tests/test_trustme.py0000644000076500000240000004240500000000000017055 0ustar00quentinstaff# -*- coding: utf-8 -*- import py import pytest import sys import ssl import socket import threading import datetime from concurrent.futures import ThreadPoolExecutor # type: ignore[import] from ipaddress import IPv4Address, IPv6Address, IPv4Network, IPv6Network from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.serialization import ( Encoding, PublicFormat, load_pem_private_key) import OpenSSL.SSL import service_identity.pyopenssl # type: ignore[import] import trustme from trustme import CA, LeafCert TYPE_CHECKING = False if TYPE_CHECKING: # pragma: no cover from typing import Callable, Optional, Text, Union SslSocket = Union[ssl.SSLSocket, OpenSSL.SSL.Connection] def _path_length(ca_cert): # type: (x509.Certificate) -> Optional[int] bc = ca_cert.extensions.get_extension_for_class(x509.BasicConstraints) return bc.value.path_length def assert_is_ca(ca_cert): # type: (x509.Certificate) -> None bc = ca_cert.extensions.get_extension_for_class(x509.BasicConstraints) assert bc.value.ca is True assert bc.critical is True ku = ca_cert.extensions.get_extension_for_class(x509.KeyUsage) assert ku.value.key_cert_sign is True assert ku.value.crl_sign is True assert ku.critical is True with pytest.raises(x509.ExtensionNotFound): ca_cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage) def assert_is_leaf(leaf_cert): # type: (x509.Certificate) -> None bc = leaf_cert.extensions.get_extension_for_class(x509.BasicConstraints) assert bc.value.ca is False assert bc.critical is True ku = leaf_cert.extensions.get_extension_for_class(x509.KeyUsage) assert ku.value.digital_signature is True assert ku.value.key_encipherment is True assert ku.value.key_cert_sign is False assert ku.value.crl_sign is False assert ku.critical is True eku = leaf_cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage) assert eku.value == x509.ExtendedKeyUsage([ x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH, x509.oid.ExtendedKeyUsageOID.SERVER_AUTH, x509.oid.ExtendedKeyUsageOID.CODE_SIGNING ]) assert eku.critical is True def test_basics(): # type: () -> None ca = CA() today = datetime.datetime.today() assert b"BEGIN RSA PRIVATE KEY" in ca.private_key_pem.bytes() assert b"BEGIN CERTIFICATE" in ca.cert_pem.bytes() private_key = load_pem_private_key( ca.private_key_pem.bytes(), password=None, backend=default_backend()) ca_cert = x509.load_pem_x509_certificate( ca.cert_pem.bytes(), default_backend()) assert ca_cert.not_valid_before <= today <= ca_cert.not_valid_after public_key1 = private_key.public_key().public_bytes( Encoding.PEM, PublicFormat.PKCS1) public_key2 = ca_cert.public_key().public_bytes( Encoding.PEM, PublicFormat.PKCS1) assert public_key1 == public_key2 assert ca_cert.issuer == ca_cert.subject assert_is_ca(ca_cert) with pytest.raises(ValueError): ca.issue_cert() server = ca.issue_cert(u"test-1.example.org", u"test-2.example.org") assert b"PRIVATE KEY" in server.private_key_pem.bytes() assert b"BEGIN CERTIFICATE" in server.cert_chain_pems[0].bytes() assert len(server.cert_chain_pems) == 1 assert server.private_key_pem.bytes() in server.private_key_and_cert_chain_pem.bytes() for blob in server.cert_chain_pems: assert blob.bytes() in server.private_key_and_cert_chain_pem.bytes() server_cert = x509.load_pem_x509_certificate( server.cert_chain_pems[0].bytes(), default_backend()) assert server_cert.not_valid_before <= today <= server_cert.not_valid_after assert server_cert.issuer == ca_cert.subject assert_is_leaf(server_cert) san = server_cert.extensions.get_extension_for_class(x509.SubjectAlternativeName) hostnames = san.value.get_values_for_type(x509.DNSName) assert hostnames == [u"test-1.example.org", u"test-2.example.org"] def test_ca_custom_names(): # type: () -> None ca = CA( organization_name=u'python-trio', organization_unit_name=u'trustme', ) ca_cert = x509.load_pem_x509_certificate( ca.cert_pem.bytes(), default_backend(), ) assert { 'O=python-trio', 'OU=trustme', }.issubset({ rdn.rfc4514_string() for rdn in ca_cert.subject.rdns }) def test_issue_cert_custom_names(): # type: () -> None ca = CA() leaf_cert = ca.issue_cert( u'example.org', organization_name=u'python-trio', organization_unit_name=u'trustme', ) cert = x509.load_pem_x509_certificate( leaf_cert.cert_chain_pems[0].bytes(), default_backend(), ) assert { 'O=python-trio', 'OU=trustme', }.issubset({ rdn.rfc4514_string() for rdn in cert.subject.rdns }) def test_issue_cert_custom_not_after(): # type: () -> None now = datetime.datetime.now() expires = datetime.datetime(2025, 12, 1, 8, 10, 10) ca = CA() leaf_cert = ca.issue_cert( u'example.org', organization_name=u'python-trio', organization_unit_name=u'trustme', not_after=expires, ) cert = x509.load_pem_x509_certificate( leaf_cert.cert_chain_pems[0].bytes(), default_backend(), ) for t in ["year", "month", "day", "hour", "minute", "second"]: assert getattr(cert.not_valid_after, t) == getattr(expires, t) def test_intermediate(): # type: () -> None ca = CA() ca_cert = x509.load_pem_x509_certificate( ca.cert_pem.bytes(), default_backend()) assert_is_ca(ca_cert) assert ca_cert.issuer == ca_cert.subject assert _path_length(ca_cert) == 9 child_ca = ca.create_child_ca() child_ca_cert = x509.load_pem_x509_certificate( child_ca.cert_pem.bytes(), default_backend()) assert_is_ca(child_ca_cert) assert child_ca_cert.issuer == ca_cert.subject assert _path_length(child_ca_cert) == 8 child_server = child_ca.issue_cert(u"test-host.example.org") assert len(child_server.cert_chain_pems) == 2 child_server_cert = x509.load_pem_x509_certificate( child_server.cert_chain_pems[0].bytes(), default_backend()) assert child_server_cert.issuer == child_ca_cert.subject assert_is_leaf(child_server_cert) def test_path_length(): # type: () -> None ca = CA() ca_cert = x509.load_pem_x509_certificate( ca.cert_pem.bytes(), default_backend()) assert _path_length(ca_cert) == 9 child_ca = ca for i in range(9): child_ca = child_ca.create_child_ca() # Can't create new child CAs anymore child_ca_cert = x509.load_pem_x509_certificate( child_ca.cert_pem.bytes(), default_backend()) assert _path_length(child_ca_cert) == 0 with pytest.raises(ValueError): child_ca.create_child_ca() def test_unrecognized_context_type(): # type: () -> None ca = CA() server = ca.issue_cert(u"test-1.example.org") with pytest.raises(TypeError): ca.configure_trust(None) # type: ignore[arg-type] with pytest.raises(TypeError): server.configure_cert(None) # type: ignore[arg-type] def test_blob(tmpdir): # type: (py.path.local) -> None test_data = b"xyzzy" b = trustme.Blob(test_data) # bytes assert b.bytes() == test_data # write_to_path b.write_to_path(str(tmpdir / "test1")) with (tmpdir / "test1").open("rb") as f: assert f.read() == test_data # append=False overwrites with (tmpdir / "test2").open("wb") as f: f.write(b"asdf") b.write_to_path(str(tmpdir / "test2")) with (tmpdir / "test2").open("rb") as f: assert f.read() == test_data # append=True appends with (tmpdir / "test2").open("wb") as f: f.write(b"asdf") b.write_to_path(str(tmpdir / "test2"), append=True) with (tmpdir / "test2").open("rb") as f: assert f.read() == b"asdf" + test_data # tempfile with b.tempfile(dir=str(tmpdir)) as path: assert path.startswith(str(tmpdir)) assert path.endswith(".pem") with open(path, "rb") as f: assert f.read() == test_data def test_ca_from_pem(tmpdir): # type: (py.path.local) -> None ca1 = trustme.CA() ca2 = trustme.CA.from_pem(ca1.cert_pem.bytes(), ca1.private_key_pem.bytes()) assert ca1._certificate == ca2._certificate assert ca1.private_key_pem.bytes() == ca2.private_key_pem.bytes() def check_connection_end_to_end(wrap_client, wrap_server): # type: (Callable[[CA, socket.socket, Text], SslSocket], Callable[[LeafCert, socket.socket], SslSocket]) -> None # Client side def fake_ssl_client(ca, raw_client_sock, hostname): # type: (CA, socket.socket, Text) -> None try: wrapped_client_sock = wrap_client(ca, raw_client_sock, hostname) # Send and receive some data to prove the connection is good wrapped_client_sock.send(b"x") assert wrapped_client_sock.recv(1) == b"y" wrapped_client_sock.close() except: # pragma: no cover sys.excepthook(*sys.exc_info()) raise finally: raw_client_sock.close() # Server side def fake_ssl_server(server_cert, raw_server_sock): # type: (LeafCert, socket.socket) -> None try: wrapped_server_sock = wrap_server(server_cert, raw_server_sock) # Prove that we're connected assert wrapped_server_sock.recv(1) == b"x" wrapped_server_sock.send(b"y") wrapped_server_sock.close() except: # pragma: no cover sys.excepthook(*sys.exc_info()) raise finally: raw_server_sock.close() def doit(ca, hostname, server_cert): # type: (CA, Text, LeafCert) -> None # socketpair and ssl don't work together on py2, because... reasons. # So we need to do this the hard way. listener = socket.socket() listener.bind(("127.0.0.1", 0)) listener.listen(1) raw_client_sock = socket.socket() raw_client_sock.connect(listener.getsockname()) raw_server_sock, _ = listener.accept() listener.close() with ThreadPoolExecutor(2) as tpe: f1 = tpe.submit(fake_ssl_client, ca, raw_client_sock, hostname) f2 = tpe.submit(fake_ssl_server, server_cert, raw_server_sock) f1.result() f2.result() ca = CA() intermediate_ca = ca.create_child_ca() hostname = u"my-test-host.example.org" # Should work doit(ca, hostname, ca.issue_cert(hostname)) # Should work doit(ca, hostname, intermediate_ca.issue_cert(hostname)) # To make sure that the above success actually required that the # CA and cert logic is all working, make sure that the same code # fails if the certs or CA aren't right: # Bad hostname fails with pytest.raises(Exception): doit(ca, u"asdf.example.org", ca.issue_cert(hostname)) # Bad CA fails bad_ca = CA() with pytest.raises(Exception): doit(bad_ca, hostname, ca.issue_cert(hostname)) def test_stdlib_end_to_end(): # type: () -> None def wrap_client(ca, raw_client_sock, hostname): # type: (CA, socket.socket, Text) -> ssl.SSLSocket ctx = ssl.create_default_context() ca.configure_trust(ctx) # Type ignore for Python 2: wants str, got unicode, but I guess unicode also works. wrapped_client_sock = ctx.wrap_socket( raw_client_sock, server_hostname=hostname) # type: ignore[arg-type] print("Client got server cert:", wrapped_client_sock.getpeercert()) peercert = wrapped_client_sock.getpeercert() assert peercert is not None san = peercert["subjectAltName"] assert san == (("DNS", "my-test-host.example.org"),) return wrapped_client_sock def wrap_server(server_cert, raw_server_sock): # type: (LeafCert, socket.socket) -> ssl.SSLSocket ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) server_cert.configure_cert(ctx) wrapped_server_sock = ctx.wrap_socket( raw_server_sock, server_side=True) print("server encrypted with:", wrapped_server_sock.cipher()) return wrapped_server_sock check_connection_end_to_end(wrap_client, wrap_server) def test_pyopenssl_end_to_end(): # type: () -> None def wrap_client(ca, raw_client_sock, hostname): # type: (CA, socket.socket, Text) -> OpenSSL.SSL.Connection # Cribbed from example at # https://service-identity.readthedocs.io/en/stable/api.html#service_identity.pyopenssl.verify_hostname ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) ctx.set_verify(OpenSSL.SSL.VERIFY_PEER, lambda conn, cert, errno, depth, ok: bool(ok)) ca.configure_trust(ctx) conn = OpenSSL.SSL.Connection(ctx, raw_client_sock) conn.set_connect_state() conn.do_handshake() service_identity.pyopenssl.verify_hostname(conn, hostname) return conn def wrap_server(server_cert, raw_server_sock): # type: (LeafCert, socket.socket) -> OpenSSL.SSL.Connection ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) server_cert.configure_cert(ctx) conn = OpenSSL.SSL.Connection(ctx, raw_server_sock) conn.set_accept_state() conn.do_handshake() return conn check_connection_end_to_end(wrap_client, wrap_server) def test_identity_variants(): # type: () -> None ca = CA() for bad in [b"example.org", bytearray(b"example.org"), 123]: with pytest.raises(TypeError): ca.issue_cert(bad) # type: ignore[arg-type] cases = { # Traditional ascii hostname u"example.org": x509.DNSName(u"example.org"), # Wildcard u"*.example.org": x509.DNSName(u"*.example.org"), # IDN u"éxamplë.org": x509.DNSName(u"xn--xampl-9rat.org"), u"xn--xampl-9rat.org": x509.DNSName(u"xn--xampl-9rat.org"), # IDN + wildcard u"*.éxamplë.org": x509.DNSName(u"*.xn--xampl-9rat.org"), u"*.xn--xampl-9rat.org": x509.DNSName(u"*.xn--xampl-9rat.org"), # IDN that acts differently in IDNA-2003 vs IDNA-2008 u"faß.de": x509.DNSName(u"xn--fa-hia.de"), u"xn--fa-hia.de": x509.DNSName(u"xn--fa-hia.de"), # IDN with non-permissable character (uppercase K) # (example taken from idna package docs) u"Königsgäßchen.de": x509.DNSName(u"xn--knigsgchen-b4a3dun.de"), # IP addresses u"127.0.0.1": x509.IPAddress(IPv4Address(u"127.0.0.1")), u"::1": x509.IPAddress(IPv6Address(u"::1")), # Check normalization u"0000::1": x509.IPAddress(IPv6Address(u"::1")), # IP networks u"127.0.0.0/24": x509.IPAddress(IPv4Network(u"127.0.0.0/24")), u"2001::/16": x509.IPAddress(IPv6Network(u"2001::/16")), # Check normalization u"2001:0000::/16": x509.IPAddress(IPv6Network(u"2001::/16")), # Email address u"example@example.com": x509.RFC822Name(u"example@example.com"), } for hostname, expected in cases.items(): # Can't repr the got or expected values here, at least until # cryptography v2.1 is out, because in v2.0 on py2, DNSName.__repr__ # blows up on IDNs. print("testing: {!r}".format(hostname)) pem = ca.issue_cert(hostname).cert_chain_pems[0].bytes() cert = x509.load_pem_x509_certificate(pem, default_backend()) san = cert.extensions.get_extension_for_class( x509.SubjectAlternativeName ) assert_is_leaf(cert) got = list(san.value)[0] assert got == expected def test_backcompat(): # type: () -> None ca = CA() # We can still use the old name ca.issue_server_cert(u"example.com") def test_CN(): # type: () -> None ca = CA() # Since we have to emulate kwonly args here, I guess we should test the # emulation logic with pytest.raises(TypeError): ca.issue_cert(comon_nam=u"wrong kwarg name") # type: ignore[call-arg] # Must be unicode with pytest.raises(TypeError): ca.issue_cert(common_name=b"bad kwarg value") # type: ignore[arg-type] # Default is no common name pem = ca.issue_cert(u"example.com").cert_chain_pems[0].bytes() cert = x509.load_pem_x509_certificate(pem, default_backend()) common_names = cert.subject.get_attributes_for_oid( x509.oid.NameOID.COMMON_NAME ) assert common_names == [] # Common name on its own is valid pem = ca.issue_cert(common_name=u"woo").cert_chain_pems[0].bytes() cert = x509.load_pem_x509_certificate(pem, default_backend()) common_names = cert.subject.get_attributes_for_oid( x509.oid.NameOID.COMMON_NAME ) assert common_names[0].value == u"woo" # Common name + SAN pem = ca.issue_cert(u"example.com", common_name=u"woo").cert_chain_pems[0].bytes() cert = x509.load_pem_x509_certificate(pem, default_backend()) san = cert.extensions.get_extension_for_class( x509.SubjectAlternativeName ) assert list(san.value)[0] == x509.DNSName(u"example.com") common_names = cert.subject.get_attributes_for_oid( x509.oid.NameOID.COMMON_NAME ) assert common_names[0].value == u"woo" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1628798670.3879387 trustme-0.9.0/trustme/0000755000076500000240000000000000000000000014275 5ustar00quentinstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628798285.0 trustme-0.9.0/trustme/__init__.py0000644000076500000240000005101500000000000016410 0ustar00quentinstaff# -*- coding: utf-8 -*- import datetime import ssl from base64 import urlsafe_b64encode from tempfile import NamedTemporaryFile from contextlib import contextmanager import os import ipaddress import idna # type: ignore[import] from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.serialization import ( PrivateFormat, NoEncryption ) from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID from cryptography.hazmat.primitives.serialization import Encoding from cryptography.hazmat.primitives.serialization import load_pem_private_key from ._version import __version__ TYPE_CHECKING = False if TYPE_CHECKING: # pragma: no cover from typing import Generator, List, Optional, Text, Union import OpenSSL.SSL __all__ = ["CA"] # Python 2/3 annoyingness try: unicode except NameError: unicode = str # On my laptop, making a CA + server certificate using 2048 bit keys takes ~160 # ms, and using 4096 bit keys takes ~2 seconds. We want tests to run in 160 ms, # not 2 seconds. And we can't go lower, since Debian (and probably others) # by default reject any keys with <2048 bits (see #45). _KEY_SIZE = 2048 # Default certificate expiry date: # OpenSSL on Windows fails if you try to give it a date after # ~3001-01-19: # https://github.com/pyca/cryptography/issues/3194 # Some versions of cryptography on 32-bit platforms fail if you give # them dates after ~2038-01-19: # https://github.com/pyca/cryptography/pull/4658 DEFAULT_EXPIRY = datetime.datetime(2038, 1, 1) def _name(name, organization_name=None, common_name=None): # type: (Text, Optional[Text], Optional[Text]) -> x509.Name name_pieces = [ x509.NameAttribute( NameOID.ORGANIZATION_NAME, organization_name or u"trustme v{}".format(__version__), ), x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, name), ] if common_name is not None: name_pieces.append( x509.NameAttribute(NameOID.COMMON_NAME, common_name) ) return x509.Name(name_pieces) def random_text(): # type: () -> Text return urlsafe_b64encode(os.urandom(12)).decode("ascii") def _smells_like_pyopenssl(ctx): # type: (object) -> bool return getattr(ctx, "__module__", "").startswith("OpenSSL") # type: ignore[no-any-return] def _cert_builder_common(subject, issuer, public_key, not_after=None): # type: (x509.Name, x509.Name, rsa.RSAPublicKey, Optional[datetime.datetime]) -> x509.CertificateBuilder not_after = not_after if not_after else DEFAULT_EXPIRY return ( x509.CertificateBuilder() .subject_name(subject) .issuer_name(issuer) .public_key(public_key) .not_valid_before(datetime.datetime(2000, 1, 1)) .not_valid_after(not_after) .serial_number(x509.random_serial_number()) .add_extension( x509.SubjectKeyIdentifier.from_public_key(public_key), critical=False, ) ) def _identity_string_to_x509(identity): # type: (Text) -> x509.GeneralName # Because we are a DWIM library for lazy slackers, we cheerfully pervert # the cryptography library's carefully type-safe API, and silently DTRT # for any of the following identity types: # # - "example.org" # - "example.org" # - "éxamplë.org" # - "xn--xampl-9rat.org" # - "xn--xampl-9rat.org" # - "127.0.0.1" # - "::1" # - "10.0.0.0/8" # - "2001::/16" # - "example@example.org" # # plus wildcard variants of the identities. if not isinstance(identity, unicode): raise TypeError("identities must be text (unicode on py2, str on py3)") if u"@" in identity: return x509.RFC822Name(identity) # Have to try ip_address first, because ip_network("127.0.0.1") is # interpreted as being the network 127.0.0.1/32. Which I guess would be # fine, actually, but why risk it. try: return x509.IPAddress(ipaddress.ip_address(identity)) except ValueError: try: return x509.IPAddress(ipaddress.ip_network(identity)) except ValueError: pass # Encode to an A-label, like cryptography wants if identity.startswith("*."): alabel_bytes = b"*." + idna.encode(identity[2:], uts46=True) else: alabel_bytes = idna.encode(identity, uts46=True) # Then back to text, which is mandatory on cryptography 2.0 and earlier, # and may or may not be deprecated in cryptography 2.1. alabel = alabel_bytes.decode("ascii") return x509.DNSName(alabel) class Blob(object): """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`. """ def __init__(self, data): # type: (bytes) -> None self._data = data def bytes(self): # type: () -> bytes """Returns the data as a `bytes` object. """ return self._data def write_to_path(self, path, append=False): # type: (str, bool) -> None """Writes the data to the file at the given path. Args: 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. """ if append: mode = "ab" else: mode = "wb" with open(path, mode) as f: f.write(self._data) @contextmanager def tempfile(self, dir=None): # type: (Optional[str]) -> Generator[str, None, None] """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) Args: dir (str or None): Passed to `tempfile.NamedTemporaryFile`. """ # On Windows, you can't re-open a NamedTemporaryFile that's still # open. Which seems like it completely defeats the purpose of having a # NamedTemporaryFile? Oh well... # https://bugs.python.org/issue14243 # Type ignore temporarily needed for Python 2: # https://github.com/python/typeshed/pull/5836 f = NamedTemporaryFile(suffix=".pem", dir=dir, delete=False) # type: ignore[arg-type] try: f.write(self._data) f.close() yield f.name finally: f.close() # in case write() raised an error os.unlink(f.name) class CA(object): """A certificate authority.""" _certificate = None # type: x509.Certificate def __init__( self, parent_cert=None, path_length=9, organization_name=None, organization_unit_name=None, ): # type: (Optional[CA], int, Optional[Text], Optional[Text]) -> None self.parent_cert = parent_cert self._private_key = rsa.generate_private_key( public_exponent=65537, key_size=_KEY_SIZE, backend=default_backend() ) self._path_length = path_length name = _name( organization_unit_name or u"Testing CA #" + random_text(), organization_name=organization_name, ) issuer = name sign_key = self._private_key if parent_cert is not None: sign_key = parent_cert._private_key parent_certificate = parent_cert._certificate issuer = parent_certificate.subject self._certificate = ( _cert_builder_common(name, issuer, self._private_key.public_key()) .add_extension( x509.BasicConstraints(ca=True, path_length=path_length), critical=True, ) .add_extension( x509.KeyUsage( digital_signature=True, # OCSP content_commitment=False, key_encipherment=False, data_encipherment=False, key_agreement=False, key_cert_sign=True, # sign certs crl_sign=True, # sign revocation lists encipher_only=False, decipher_only=False), critical=True ) .sign( private_key=sign_key, algorithm=hashes.SHA256(), backend=default_backend(), ) ) @property def cert_pem(self): # type: () -> Blob """`Blob`: The PEM-encoded certificate for this CA. Add this to your trust store to trust this CA.""" return Blob(self._certificate.public_bytes(Encoding.PEM)) @property def private_key_pem(self): # type: () -> Blob """`Blob`: The PEM-encoded private key for this CA. Use this to sign other certificates from this CA.""" return Blob( self._private_key.private_bytes( Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption() ) ) def create_child_ca(self): # type: () -> CA """Creates a child certificate authority Returns: CA: the newly-generated certificate authority Raises: ValueError: if the CA path length is 0 """ if self._path_length == 0: raise ValueError("Can't create child CA: path length is 0") path_length = self._path_length - 1 return CA(parent_cert=self, path_length=path_length) def issue_cert(self, *identities, **kwargs): # type: (Text, Optional[Union[Text, datetime.datetime]]) -> LeafCert # PY3: (str, Optional[str], Optional[str], Optional[str], Optional[datetime.datetime]) -> LeafCert """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 (``unicode`` on Python 2, ``str`` on Python 3). Args: identities: 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: 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 `datetime.datetime`. Returns: LeafCert: the newly-generated certificate. """ common_name = kwargs.pop("common_name", None) # type: Optional[Text] # type: ignore[assignment] organization_name = kwargs.pop("organization_name", None) # type: Optional[Text] # type: ignore[assignment] organization_unit_name = kwargs.pop("organization_unit_name", None) # type: Optional[Text] # type: ignore[assignment] not_after = kwargs.pop("not_after", None) # type: Optional[datetime.datetime] # type: ignore[assignment] if kwargs: raise TypeError("unrecognized keyword arguments {}".format(kwargs)) if not identities and common_name is None: raise ValueError( "Must specify at least one identity or common name" ) key = rsa.generate_private_key( public_exponent=65537, key_size=_KEY_SIZE, backend=default_backend() ) ski_ext = self._certificate.extensions.get_extension_for_class( x509.SubjectKeyIdentifier) ski = ski_ext.value # Workaround a bug in cryptography 2.6 and earlier, where you have to # pass the extension object instead of the actual SKI object try: # The new way aki = x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ski) except AttributeError: # The old way aki = x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ski_ext) # type: ignore[arg-type] cert = ( _cert_builder_common( _name( organization_unit_name or u"Testing cert #" + random_text(), organization_name=organization_name, common_name=common_name, ), self._certificate.subject, key.public_key(), not_after=not_after, ) .add_extension( x509.BasicConstraints(ca=False, path_length=None), critical=True, ) .add_extension(aki, critical=False) .add_extension( x509.SubjectAlternativeName( [_identity_string_to_x509(ident) for ident in identities] ), critical=True, ) .add_extension( x509.KeyUsage( digital_signature=True, content_commitment=False, key_encipherment=True, data_encipherment=False, key_agreement=False, key_cert_sign=False, crl_sign=False, encipher_only=False, decipher_only=False), critical=True ) .add_extension( x509.ExtendedKeyUsage([ ExtendedKeyUsageOID.CLIENT_AUTH, ExtendedKeyUsageOID.SERVER_AUTH, ExtendedKeyUsageOID.CODE_SIGNING, ]), critical=True ) .sign( private_key=self._private_key, algorithm=hashes.SHA256(), backend=default_backend(), ) ) chain_to_ca = [] ca = self while ca.parent_cert is not None: chain_to_ca.append(ca._certificate.public_bytes(Encoding.PEM)) ca = ca.parent_cert return LeafCert( key.private_bytes( Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption(), ), cert.public_bytes(Encoding.PEM), chain_to_ca, ) # For backwards compatibility issue_server_cert = issue_cert def configure_trust(self, ctx): # type: (Union[ssl.SSLContext, OpenSSL.SSL.Context]) -> None """Configure the given context object to trust certificates signed by this CA. Args: ctx (ssl.SSLContext or OpenSSL.SSL.Context): The SSL context to be modified. """ if isinstance(ctx, ssl.SSLContext): ctx.load_verify_locations( cadata=self.cert_pem.bytes().decode("ascii")) elif _smells_like_pyopenssl(ctx): from OpenSSL import crypto cert = crypto.load_certificate( crypto.FILETYPE_PEM, self.cert_pem.bytes()) store = ctx.get_cert_store() store.add_cert(cert) else: raise TypeError( "unrecognized context type {!r}" .format(ctx.__class__.__name__)) @classmethod def from_pem(cls, cert_bytes, private_key_bytes): # type: (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. Args: cert_bytes (bytes): The bytes of the certificate in PEM format private_key_bytes (bytes): The bytes of the private key in PEM format """ ca = cls() ca.parent_cert = None ca._certificate = x509.load_pem_x509_certificate( cert_bytes, backend=default_backend()) ca._private_key = load_pem_private_key( private_key_bytes, password=None, backend=default_backend()) return ca class LeafCert(object): """A server or client certificate. This type has no public constructor; you get one by calling `CA.issue_cert` or similar. Attributes: 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. """ def __init__(self, private_key_pem, server_cert_pem, chain_to_ca): # type: (bytes, bytes, List[bytes]) -> None self.private_key_pem = Blob(private_key_pem) self.cert_chain_pems = [ Blob(pem) for pem in [server_cert_pem] + chain_to_ca] self.private_key_and_cert_chain_pem = ( Blob(private_key_pem + server_cert_pem + b''.join(chain_to_ca))) def configure_cert(self, ctx): # type: (Union[ssl.SSLContext, OpenSSL.SSL.Context]) -> None """Configure the given context object to present this certificate. Args: ctx (ssl.SSLContext or OpenSSL.SSL.Context): The SSL context to be modified. """ if isinstance(ctx, ssl.SSLContext): # Currently need a temporary file for this, see: # https://bugs.python.org/issue16487 with self.private_key_and_cert_chain_pem.tempfile() as path: ctx.load_cert_chain(path) elif _smells_like_pyopenssl(ctx): from OpenSSL.crypto import ( load_privatekey, load_certificate, FILETYPE_PEM, ) key = load_privatekey(FILETYPE_PEM, self.private_key_pem.bytes()) ctx.use_privatekey(key) cert = load_certificate(FILETYPE_PEM, self.cert_chain_pems[0].bytes()) ctx.use_certificate(cert) for pem in self.cert_chain_pems[1:]: cert = load_certificate(FILETYPE_PEM, pem.bytes()) ctx.add_extra_chain_cert(cert) else: raise TypeError( "unrecognized context type {!r}" .format(ctx.__class__.__name__)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1612938165.0 trustme-0.9.0/trustme/__main__.py0000644000076500000240000000007000000000000016364 0ustar00quentinstaff# -*- coding: utf-8 -*- from ._cli import main main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628798285.0 trustme-0.9.0/trustme/_cli.py0000644000076500000240000000602400000000000015557 0ustar00quentinstaff# -*- coding: utf-8 -*- import argparse import os import trustme import sys from datetime import datetime TYPE_CHECKING = False if TYPE_CHECKING: # pragma: no cover from typing import List, Optional # Python 2/3 annoyingness try: unicode except NameError: # pragma: no cover unicode = str # ISO 8601 DATE_FORMAT = '%Y-%m-%d' def main(argv=None): # type: (Optional[List[str]]) -> None if argv is None: argv = sys.argv[1:] parser = argparse.ArgumentParser(prog="trustme") parser.add_argument( "-d", "--dir", default=os.getcwd(), help="Directory where certificates and keys are written to. Defaults to cwd.", ) parser.add_argument( "-i", "--identities", nargs="*", default=("localhost", "127.0.0.1", "::1"), help="Identities for the certificate. Defaults to 'localhost 127.0.0.1 ::1'.", ) parser.add_argument( "--common-name", nargs=1, default=None, help="Also sets the deprecated 'commonName' field (only for the first identity passed).", ) parser.add_argument( "-x", "--expires-on", default=None, help="Set the date the certificate will expire on (in YYYY-MM-DD format).", metavar='YYYY-MM-DD', ) parser.add_argument( "-q", "--quiet", action="store_true", help="Doesn't print out helpful information for humans.", ) args = parser.parse_args(argv) cert_dir = args.dir identities = [unicode(identity) for identity in args.identities] common_name = unicode(args.common_name[0]) if args.common_name else None expires_on = None if args.expires_on is None else datetime.strptime(args.expires_on, DATE_FORMAT) quiet = args.quiet if not os.path.isdir(cert_dir): raise ValueError("--dir={} is not a directory".format(cert_dir)) if len(identities) < 1: raise ValueError("Must include at least one identity") # Generate the CA certificate ca = trustme.CA() cert = ca.issue_cert(*identities, common_name=common_name, not_after=expires_on) # Write the certificate and private key the server should use server_key = os.path.join(cert_dir, "server.key") server_cert = os.path.join(cert_dir, "server.pem") cert.private_key_pem.write_to_path(path=server_key) with open(server_cert, mode="w") as f: f.truncate() for blob in cert.cert_chain_pems: blob.write_to_path(path=server_cert, append=True) # Write the certificate the client should trust client_cert = os.path.join(cert_dir, "client.pem") ca.cert_pem.write_to_path(path=client_cert) if not quiet: idents = "', '".join(identities) print("Generated a certificate for '{}'".format(idents)) print("Configure your server to use the following files:") print(" cert={}".format(server_cert)) print(" key={}".format(server_key)) print("Configure your client to use the following files:") print(" cert={}".format(client_cert)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628798343.0 trustme-0.9.0/trustme/_version.py0000644000076500000240000000002600000000000016471 0ustar00quentinstaff__version__ = "0.9.0" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628798285.0 trustme-0.9.0/trustme/py.typed0000644000076500000240000000000000000000000015762 0ustar00quentinstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1628798670.3964808 trustme-0.9.0/trustme.egg-info/0000755000076500000240000000000000000000000015767 5ustar00quentinstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628798669.0 trustme-0.9.0/trustme.egg-info/PKG-INFO0000644000076500000240000001476100000000000017075 0ustar00quentinstaffMetadata-Version: 1.1 Name: trustme Version: 0.9.0 Summary: #1 quality TLS certs while you wait, for the discerning tester Home-page: https://github.com/python-trio/trustme Author: Nathaniel J. Smith Author-email: njs@pobox.com License: MIT -or- Apache License 2.0 Description: .. note that this README gets 'include'ed into the main documentation ============================================== trustme: #1 quality TLS certs while you wait ============================================== .. image:: https://vignette2.wikia.nocookie.net/jadensadventures/images/1/1e/Kaa%27s_hypnotic_eyes.jpg/revision/latest?cb=20140310173415 :width: 200px :align: right 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 =========== Programmatic usage: .. code-block:: python 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: .. code-block:: console $ # 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. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Topic :: System :: Networking Classifier: Topic :: Security :: Cryptography Classifier: Topic :: Software Development :: Testing ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628798669.0 trustme-0.9.0/trustme.egg-info/SOURCES.txt0000644000076500000240000000112100000000000017646 0ustar00quentinstaffCODE_OF_CONDUCT.md LICENSE LICENSE.APACHE2 LICENSE.MIT MANIFEST.in README.rst pyproject.toml setup.cfg setup.py test-requirements.txt docs/Makefile docs/make.bat docs/source/conf.py docs/source/index.rst docs/source/trustme-trio-example.py docs/source/_static/.gitkeep docs/source/_templates/need-help.html tests/test_cli.py tests/test_trustme.py trustme/__init__.py trustme/__main__.py trustme/_cli.py trustme/_version.py trustme/py.typed trustme.egg-info/PKG-INFO trustme.egg-info/SOURCES.txt trustme.egg-info/dependency_links.txt trustme.egg-info/requires.txt trustme.egg-info/top_level.txt././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628798669.0 trustme-0.9.0/trustme.egg-info/dependency_links.txt0000644000076500000240000000000100000000000022035 0ustar00quentinstaff ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628798669.0 trustme-0.9.0/trustme.egg-info/requires.txt0000644000076500000240000000006700000000000020372 0ustar00quentinstaffcryptography idna [:python_version < "3.3"] ipaddress ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628798669.0 trustme-0.9.0/trustme.egg-info/top_level.txt0000644000076500000240000000001000000000000020510 0ustar00quentinstafftrustme